Sollet Chrome Extension v0 (#127)
* init * ui changes * injection * upd * adding connections page * upd extension * merge * prettier * fix * fix * fix
This commit is contained in:
parent
2664ed7d11
commit
754dd37595
|
@ -24,3 +24,6 @@ yarn-error.log*
|
|||
|
||||
.idea
|
||||
.eslintcache
|
||||
|
||||
# generate with `build:extension` script
|
||||
extension/build/*
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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 }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -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'"
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
);
|
||||
},
|
||||
};
|
11
package.json
11
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": [
|
||||
|
|
37
src/App.js
37
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 = (
|
||||
<NavigationFrame>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<PageContents />
|
||||
</Suspense>
|
||||
</NavigationFrame>
|
||||
);
|
||||
|
||||
if (isExtension) {
|
||||
appElement = (
|
||||
<ConnectedWalletsProvider>
|
||||
<PageProvider>{appElement}</PageProvider>
|
||||
</ConnectedWalletsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<ThemeProvider theme={theme}>
|
||||
|
@ -41,13 +63,7 @@ export default function App() {
|
|||
|
||||
<ConnectionProvider>
|
||||
<SnackbarProvider maxSnack={5} autoHideDuration={8000}>
|
||||
<WalletProvider>
|
||||
<NavigationFrame>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<PageContents />
|
||||
</Suspense>
|
||||
</NavigationFrame>
|
||||
</WalletProvider>
|
||||
<WalletProvider>{appElement}</WalletProvider>
|
||||
</SnackbarProvider>
|
||||
</ConnectionProvider>
|
||||
</ThemeProvider>
|
||||
|
@ -57,11 +73,16 @@ export default function App() {
|
|||
|
||||
function PageContents() {
|
||||
const wallet = useWallet();
|
||||
const [page] = usePage();
|
||||
if (!wallet) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
if (window.opener) {
|
||||
return <PopupPage opener={window.opener} />;
|
||||
}
|
||||
return <WalletPage />;
|
||||
if (page === 'wallet') {
|
||||
return <WalletPage />;
|
||||
} else if (page === 'connections') {
|
||||
return <ConnectionsPage />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Paper>
|
||||
<AppBar position="static" color="default" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" style={{ flexGrow: 1 }} component="h2">
|
||||
{selectedAccount && selectedAccount.name} Balances{' '}
|
||||
<Typography
|
||||
variant="h6"
|
||||
style={{ flexGrow: 1, fontSize: isExtensionWidth && '1rem' }}
|
||||
component="h2"
|
||||
>
|
||||
{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' && (
|
||||
<Tooltip title="Edit Account Name" arrow>
|
||||
<IconButton onClick={() => setShowEditAccountNameDialog(true)}>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => setShowEditAccountNameDialog(true)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Merge Accounts" arrow>
|
||||
<IconButton onClick={() => setShowMergeAccounts(true)}>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => setShowMergeAccounts(true)}
|
||||
>
|
||||
<MergeType />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Add Token" arrow>
|
||||
<IconButton onClick={() => setShowAddTokenDialog(true)}>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => setShowAddTokenDialog(true)}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Sort Accounts" arrow>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => {
|
||||
switch (sortAccounts) {
|
||||
case SortAccounts.None:
|
||||
|
@ -234,6 +252,7 @@ export default function BalancesList() {
|
|||
</Tooltip>
|
||||
<Tooltip title="Refresh" arrow>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => {
|
||||
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 : (
|
||||
<div style={{ display: 'flex', height: '20px', overflow: 'hidden' }}>
|
||||
{isAssociatedToken && (
|
||||
<div
|
||||
|
@ -430,8 +457,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
|
|||
primary={
|
||||
<>
|
||||
{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 <LoadingIndicator delay={0} />;
|
||||
|
@ -514,6 +541,75 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
|||
: undefined
|
||||
: undefined;
|
||||
|
||||
const additionalInfo = isExtensionWidth ? undefined : (
|
||||
<>
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
Deposit Address: {publicKey.toBase58()}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Token Name: {tokenName ?? 'Unknown'}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Token Symbol: {tokenSymbol ?? 'Unknown'}
|
||||
</Typography>
|
||||
{mint ? (
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
Token Address: {mint.toBase58()}
|
||||
</Typography>
|
||||
) : null}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Solana
|
||||
</Link>
|
||||
</Typography>
|
||||
{market && (
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={`https://dex.projectserum.com/#/market/${market}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Serum
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
{swapInfo && swapInfo.coin.erc20Contract && (
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://etherscan.io/token/${swapInfo.coin.erc20Contract}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Ethereum
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{exportNeedsDisplay && wallet.allowsExport && (
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link href={'#'} onClick={(e) => setExportAccDialogOpen(true)}>
|
||||
Export
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{wallet.allowsExport && (
|
||||
|
@ -562,70 +658,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
|||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
Deposit Address: {publicKey.toBase58()}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Token Name: {tokenName ?? 'Unknown'}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Token Symbol: {tokenSymbol ?? 'Unknown'}
|
||||
</Typography>
|
||||
{mint ? (
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
Token Address: {mint.toBase58()}
|
||||
</Typography>
|
||||
) : null}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Solana
|
||||
</Link>
|
||||
</Typography>
|
||||
{market && (
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={`https://dex.projectserum.com/#/market/${market}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Serum
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
{swapInfo && swapInfo.coin.erc20Contract && (
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://etherscan.io/token/${swapInfo.coin.erc20Contract}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Ethereum
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{exportNeedsDisplay && wallet.allowsExport && (
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link href={'#'} onClick={(e) => setExportAccDialogOpen(true)}>
|
||||
Export
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{additionalInfo}
|
||||
</div>
|
||||
<SendDialog
|
||||
open={sendDialogOpen}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default function ConnectionIcon() {
|
||||
return (
|
||||
<SvgIcon style={{ height: 28, width: 28, margin: -2 }} viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path d="M7.76 16.24C6.67 15.16 6 13.66 6 12s.67-3.16 1.76-4.24l1.42 1.42C8.45 9.9 8 10.9 8 12c0 1.1.45 2.1 1.17 2.83l-1.41 1.41zm8.48 0C17.33 15.16 18 13.66 18 12s-.67-3.16-1.76-4.24l-1.42 1.42C15.55 9.9 16 10.9 16 12c0 1.1-.45 2.1-1.17 2.83l1.41 1.41zM12 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8 2c0 2.21-.9 4.21-2.35 5.65l1.42 1.42C20.88 17.26 22 14.76 22 12s-1.12-5.26-2.93-7.07l-1.42 1.42A7.94 7.94 0 0120 12zM6.35 6.35L4.93 4.93C3.12 6.74 2 9.24 2 12s1.12 5.26 2.93 7.07l1.42-1.42C4.9 16.21 4 14.21 4 12s.9-4.21 2.35-5.65z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<Paper>
|
||||
<AppBar position="static" color="default" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography
|
||||
variant="h6"
|
||||
style={{ flexGrow: 1, fontSize: isExtensionWidth && '1rem' }}
|
||||
component="h2"
|
||||
>
|
||||
Connected Dapps
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<List disablePadding>
|
||||
{Object.entries(connectedWallets).map(([origin, connectedWallet]) => (
|
||||
<ConnectionsListItem
|
||||
origin={origin}
|
||||
connectedWallet={connectedWallet}
|
||||
key={origin}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<ListItem button onClick={() => setOpen((open) => !open)}>
|
||||
<ListItemIcon>
|
||||
<div className={classes.listItemIcon}>
|
||||
<img
|
||||
src={iconUrl}
|
||||
onError={() => setIconUrl(faviconUrl)}
|
||||
className={classes.listItemImage}
|
||||
alt={origin}
|
||||
/>
|
||||
</div>
|
||||
</ListItemIcon>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ListItemText primary={origin} secondary={account.name} />
|
||||
</div>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<div class={classes.itemDetails}>
|
||||
<div class={classes.buttonContainer}>
|
||||
<Button
|
||||
variant={connectedWallet.autoApprove ? 'contained' : 'outlined'}
|
||||
color="primary"
|
||||
size="small"
|
||||
startIcon={<DoneAll />}
|
||||
onClick={() => setAutoApprove(!connectedWallet.autoApprove)}
|
||||
>
|
||||
{connectedWallet.autoApprove ? 'Auto-Approved' : 'Auto-Approve'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={disconnectWallet}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -62,8 +62,9 @@ export default function DebugButtons() {
|
|||
|
||||
const noSol = amount === 0;
|
||||
const requestAirdropDisabled = endpoint === MAINNET_URL;
|
||||
const spacing = 24;
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ display: 'flex', marginLeft: spacing }}>
|
||||
<Tooltip
|
||||
title={
|
||||
requestAirdropDisabled
|
||||
|
@ -95,7 +96,7 @@ export default function DebugButtons() {
|
|||
color="primary"
|
||||
onClick={mintTestToken}
|
||||
disabled={sending || noSol}
|
||||
style={{ marginLeft: 24 }}
|
||||
style={{ marginLeft: spacing }}
|
||||
>
|
||||
Mint Test Token
|
||||
</Button>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" className={classes.title} component="h1">
|
||||
Solana SPL Token Wallet
|
||||
{isExtensionWidth ? 'Sollet' : 'Solana SPL Token Wallet'}
|
||||
</Typography>
|
||||
<WalletSelector />
|
||||
<NetworkSelector />
|
||||
<NavigationButtons />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<main className={classes.content}>{children}</main>
|
||||
<Footer />
|
||||
{!isExtensionWidth && <Footer />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationButtons() {
|
||||
const isExtensionWidth = useIsExtensionWidth();
|
||||
const [page] = usePage();
|
||||
|
||||
if (isExtensionPopup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let elements = [];
|
||||
if (page === 'wallet') {
|
||||
elements = [
|
||||
isExtension && <ConnectionsButton />,
|
||||
<WalletSelector />,
|
||||
<NetworkSelector />,
|
||||
];
|
||||
} else if (page === 'connections') {
|
||||
elements = [<WalletButton />];
|
||||
}
|
||||
|
||||
if (isExtension && isExtensionWidth) {
|
||||
elements.push(<ExpandButton />);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
function ExpandButton() {
|
||||
const onClick = () => {
|
||||
window.open(chrome.extension.getURL('index.html'), '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title="Expand View">
|
||||
<IconButton color="inherit" onClick={onClick}>
|
||||
<OpenInNew />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function WalletButton() {
|
||||
const classes = useStyles();
|
||||
const setPage = usePage()[1];
|
||||
const onClick = () => setPage('wallet');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hidden smUp>
|
||||
<Tooltip title="Wallet Balances">
|
||||
<IconButton color="inherit" onClick={onClick}>
|
||||
<MonetizationOn />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Hidden>
|
||||
<Hidden xsDown>
|
||||
<Button color="inherit" onClick={onClick} className={classes.button}>
|
||||
Wallet
|
||||
</Button>
|
||||
</Hidden>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionsButton() {
|
||||
const classes = useStyles();
|
||||
const setPage = usePage()[1];
|
||||
const onClick = () => setPage('connections');
|
||||
const connectedWallets = useConnectedWallets();
|
||||
|
||||
const connectionAmount = Object.keys(connectedWallets).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hidden smUp>
|
||||
<Tooltip title="Manage Connections">
|
||||
<IconButton color="inherit" onClick={onClick}>
|
||||
<Badge
|
||||
badgeContent={connectionAmount}
|
||||
classes={{ badge: classes.badge }}
|
||||
>
|
||||
<ConnectionIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Hidden>
|
||||
<Hidden xsDown>
|
||||
<Badge
|
||||
badgeContent={connectionAmount}
|
||||
classes={{ badge: classes.badge }}
|
||||
>
|
||||
<Button color="inherit" onClick={onClick} className={classes.button}>
|
||||
Connections
|
||||
</Button>
|
||||
</Badge>
|
||||
</Hidden>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,13 @@ import React from 'react';
|
|||
import Link from '@material-ui/core/Link';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
export default function LabelValue({ label, value, link = false, onClick, gutterBottom }) {
|
||||
export default function LabelValue({
|
||||
label,
|
||||
value,
|
||||
link = false,
|
||||
onClick,
|
||||
gutterBottom,
|
||||
}) {
|
||||
return (
|
||||
<Typography gutterBottom={gutterBottom}>
|
||||
{label}:{' '}
|
||||
|
|
|
@ -3,7 +3,7 @@ import Typography from '@material-ui/core/Typography';
|
|||
import LabelValue from './LabelValue';
|
||||
import { useWallet } from '../../utils/wallet';
|
||||
|
||||
export default function Neworder({ instruction, onOpenAddress, v3=false }) {
|
||||
export default function Neworder({ instruction, onOpenAddress, v3 = false }) {
|
||||
const wallet = useWallet();
|
||||
const { data, market, marketInfo } = instruction;
|
||||
const marketLabel =
|
||||
|
|
|
@ -3,7 +3,7 @@ import LabelValue from './LabelValue';
|
|||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
export default function UnknownInstruction({ instruction, onOpenAddress }) {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
|
@ -13,14 +13,15 @@ export default function UnknownInstruction({ instruction, onOpenAddress }) {
|
|||
Unknown instruction:
|
||||
</Typography>
|
||||
<LabelValue
|
||||
key='Program'
|
||||
label='Program'
|
||||
key="Program"
|
||||
label="Program"
|
||||
value={instruction.programId?.toBase58()}
|
||||
link={true}
|
||||
gutterBottom={true}
|
||||
onClick={() => onOpenAddress(instruction.programId.toBase58())}
|
||||
/>
|
||||
{instruction.accountMetas && instruction.accountMetas.map((accountMeta, index) => {
|
||||
{instruction.accountMetas &&
|
||||
instruction.accountMetas.map((accountMeta, index) => {
|
||||
return (
|
||||
<>
|
||||
<LabelValue
|
||||
|
@ -30,7 +31,9 @@ export default function UnknownInstruction({ instruction, onOpenAddress }) {
|
|||
link={true}
|
||||
onClick={() => onOpenAddress(accountMeta.publicKey.toBase58())}
|
||||
/>
|
||||
<Typography gutterBottom>Writable: {accountMeta.isWritable.toString()}</Typography>
|
||||
<Typography gutterBottom>
|
||||
Writable: {accountMeta.isWritable.toString()}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -7,12 +7,40 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
|
||||
/* TODO consolidate popup dimensions */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-width: 375px;
|
||||
min-height: 600px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* TODO consolidate popup dimensions */
|
||||
@media screen and (max-height: 700px) {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
max-height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO consolidate popup dimensions */
|
||||
@media screen and (max-width: 450px) {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
max-width: 375px;
|
||||
}
|
||||
html::-webkit-scrollbar,
|
||||
body::-webkit-scrollbar,
|
||||
#root::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import Container from '@material-ui/core/Container';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { useIsExtensionWidth } from '../utils/utils';
|
||||
import ConnectionsList from '../components/ConnectionsList';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
[theme.breakpoints.down(theme.ext)]: {
|
||||
padding: 0,
|
||||
},
|
||||
[theme.breakpoints.up(theme.ext)]: {
|
||||
maxWidth: 'md',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ConnectionsPage() {
|
||||
const classes = useStyles();
|
||||
const isExtensionWidth = useIsExtensionWidth();
|
||||
return (
|
||||
<Container fixed maxWidth="md" className={classes.container}>
|
||||
<Grid container spacing={isExtensionWidth ? 0 : 3}>
|
||||
<Grid item xs={12}>
|
||||
<ConnectionsList />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,12 @@ import React, {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useWallet, useWalletPublicKeys } from '../utils/wallet';
|
||||
import {
|
||||
useWallet,
|
||||
useWalletPublicKeys,
|
||||
useWalletSelector,
|
||||
} from '../utils/wallet';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { decodeMessage } from '../utils/transactions';
|
||||
import { useConnection, useSolanaExplorerUrlSuffix } from '../utils/connection';
|
||||
import {
|
||||
|
@ -31,31 +36,74 @@ import WarningIcon from '@material-ui/icons/Warning';
|
|||
import SystemInstruction from '../components/instructions/SystemInstruction';
|
||||
import DexInstruction from '../components/instructions/DexInstruction';
|
||||
import TokenInstruction from '../components/instructions/TokenInstruction';
|
||||
import { useLocalStorageState } from '../utils/utils';
|
||||
import { useLocalStorageState, isExtension } from '../utils/utils';
|
||||
|
||||
function getInitialRequests() {
|
||||
if (!isExtension) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO CHECK OPENER (?)
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
return [JSON.parse(urlParams.get('request'))];
|
||||
}
|
||||
|
||||
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 selectedWallet = useWallet();
|
||||
const { accounts, setWalletSelector } = useWalletSelector();
|
||||
const [wallet, setWallet] = useState(isExtension ? null : selectedWallet);
|
||||
|
||||
const [connectedAccount, setConnectedAccount] = useState(null);
|
||||
const hasConnectedAccount = !!connectedAccount;
|
||||
|
||||
const [requests, setRequests] = useState(getInitialRequests);
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
const postMessage = useCallback(
|
||||
(message) => {
|
||||
opener.postMessage({ jsonrpc: '2.0', ...message }, origin);
|
||||
if (isExtension) {
|
||||
chrome.runtime.sendMessage({
|
||||
channel: 'sollet_extension_background_channel',
|
||||
data: message,
|
||||
});
|
||||
} else {
|
||||
opener.postMessage({ jsonrpc: '2.0', ...message }, origin);
|
||||
}
|
||||
},
|
||||
[opener, origin],
|
||||
);
|
||||
|
||||
const [connectedAccount, setConnectedAccount] = useState(null);
|
||||
const hasConnectedAccount = !!connectedAccount;
|
||||
const [requests, setRequests] = useState([]);
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
// (Extension only) Fetch connected wallet for site from local storage.
|
||||
useEffect(() => {
|
||||
if (isExtension) {
|
||||
chrome.storage.local.get('connectedWallets', (result) => {
|
||||
const connectedWallet = (result.connectedWallets || {})[origin];
|
||||
if (connectedWallet) {
|
||||
setWalletSelector(connectedWallet.selector);
|
||||
setConnectedAccount(new PublicKey(connectedWallet.publicKey));
|
||||
setAutoApprove(connectedWallet.autoApprove);
|
||||
} else {
|
||||
setConnectedAccount(selectedWallet.publicKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [origin, setWalletSelector, selectedWallet]);
|
||||
|
||||
// (Extension only) Set wallet once connectedWallet is retrieved.
|
||||
useEffect(() => {
|
||||
if (isExtension && connectedAccount) {
|
||||
setWallet(selectedWallet);
|
||||
}
|
||||
}, [connectedAccount, selectedWallet]);
|
||||
|
||||
// Send a disconnect event if this window is closed, this component is
|
||||
// unmounted, or setConnectedAccount(null) is called.
|
||||
useEffect(() => {
|
||||
if (hasConnectedAccount) {
|
||||
if (hasConnectedAccount && !isExtension) {
|
||||
function unloadHandler() {
|
||||
postMessage({ method: 'disconnected' });
|
||||
}
|
||||
|
@ -65,11 +113,15 @@ export default function PopupPage({ opener }) {
|
|||
window.removeEventListener('beforeunload', unloadHandler);
|
||||
};
|
||||
}
|
||||
}, [hasConnectedAccount, postMessage]);
|
||||
}, [hasConnectedAccount, postMessage, origin]);
|
||||
|
||||
// Disconnect if the user switches to a different wallet.
|
||||
useEffect(() => {
|
||||
if (connectedAccount && !connectedAccount.equals(wallet.publicKey)) {
|
||||
if (
|
||||
wallet &&
|
||||
connectedAccount &&
|
||||
!connectedAccount.equals(wallet.publicKey)
|
||||
) {
|
||||
setConnectedAccount(null);
|
||||
}
|
||||
}, [connectedAccount, wallet]);
|
||||
|
@ -92,92 +144,132 @@ export default function PopupPage({ opener }) {
|
|||
return () => window.removeEventListener('message', messageHandler);
|
||||
}, [origin, postMessage]);
|
||||
|
||||
if (!connectedAccount || !connectedAccount.equals(wallet.publicKey)) {
|
||||
const request = requests[0];
|
||||
const popRequest = () => setRequests((requests) => requests.slice(1));
|
||||
|
||||
if (requests.length === 0) {
|
||||
if (isExtension) {
|
||||
window.close();
|
||||
} else {
|
||||
focusParent();
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography>
|
||||
{isExtension
|
||||
? 'Submitting...'
|
||||
: 'Please keep this window open in the background.'}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
if (!wallet) {
|
||||
return <Typography>Loading wallet...</Typography>;
|
||||
}
|
||||
|
||||
const mustConnect =
|
||||
!connectedAccount || !connectedAccount.equals(wallet.publicKey);
|
||||
// We must detect when to show the connection form on the website as it is not sent as a request.
|
||||
if (
|
||||
(isExtension && request.method === 'connect') ||
|
||||
(!isExtension && mustConnect)
|
||||
) {
|
||||
// Approve the parent page to connect to this wallet.
|
||||
function connect(autoApprove) {
|
||||
setConnectedAccount(wallet.publicKey);
|
||||
if (isExtension) {
|
||||
chrome.storage.local.get('connectedWallets', (result) => {
|
||||
// TODO better way to do this
|
||||
const account = accounts.find((account) =>
|
||||
account.address.equals(wallet.publicKey),
|
||||
);
|
||||
const connectedWallets = {
|
||||
...(result.connectedWallets || {}),
|
||||
[origin]: {
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
selector: account.selector,
|
||||
autoApprove,
|
||||
},
|
||||
};
|
||||
chrome.storage.local.set({ connectedWallets });
|
||||
});
|
||||
}
|
||||
postMessage({
|
||||
method: 'connected',
|
||||
params: { publicKey: wallet.publicKey.toBase58(), autoApprove },
|
||||
id: isExtension ? request.id : undefined,
|
||||
});
|
||||
setAutoApprove(autoApprove);
|
||||
focusParent();
|
||||
if (!isExtension) {
|
||||
focusParent();
|
||||
} else {
|
||||
popRequest();
|
||||
}
|
||||
}
|
||||
|
||||
return <ApproveConnectionForm origin={origin} onApprove={connect} />;
|
||||
}
|
||||
|
||||
if (requests.length > 0) {
|
||||
const request = requests[0];
|
||||
assert(
|
||||
request.method === 'signTransaction' ||
|
||||
request.method === 'signAllTransactions',
|
||||
assert(
|
||||
(request.method === 'signTransaction' ||
|
||||
request.method === 'signAllTransactions') &&
|
||||
wallet,
|
||||
);
|
||||
|
||||
let messages =
|
||||
request.method === 'signTransaction'
|
||||
? [bs58.decode(request.params.message)]
|
||||
: request.params.messages.map((m) => bs58.decode(m));
|
||||
|
||||
async function onApprove() {
|
||||
popRequest();
|
||||
if (request.method === 'signTransaction') {
|
||||
sendSignature(messages[0]);
|
||||
} else {
|
||||
sendAllSignatures(messages);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSignature(message) {
|
||||
postMessage({
|
||||
result: {
|
||||
signature: await wallet.createSignature(message),
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendAllSignatures(messages) {
|
||||
const signatures = await Promise.all(
|
||||
messages.map((m) => wallet.createSignature(m)),
|
||||
);
|
||||
postMessage({
|
||||
result: {
|
||||
signatures,
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
});
|
||||
}
|
||||
|
||||
let messages =
|
||||
request.method === 'signTransaction'
|
||||
? [bs58.decode(request.params.message)]
|
||||
: request.params.messages.map((m) => bs58.decode(m));
|
||||
|
||||
async function onApprove() {
|
||||
setRequests((requests) => requests.slice(1));
|
||||
if (request.method === 'signTransaction') {
|
||||
sendSignature(messages[0]);
|
||||
} else {
|
||||
sendAllSignatures(messages);
|
||||
}
|
||||
if (requests.length === 1) {
|
||||
focusParent();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSignature(message) {
|
||||
postMessage({
|
||||
result: {
|
||||
signature: await wallet.createSignature(message),
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendAllSignatures(messages) {
|
||||
const signatures = await Promise.all(
|
||||
messages.map((m) => wallet.createSignature(m)),
|
||||
);
|
||||
postMessage({
|
||||
result: {
|
||||
signatures,
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
});
|
||||
}
|
||||
|
||||
function sendReject() {
|
||||
setRequests((requests) => requests.slice(1));
|
||||
postMessage({
|
||||
error: 'Transaction cancelled',
|
||||
id: request.id,
|
||||
});
|
||||
if (requests.length === 1) {
|
||||
focusParent();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ApproveSignatureForm
|
||||
key={request.id}
|
||||
autoApprove={autoApprove}
|
||||
origin={origin}
|
||||
messages={messages}
|
||||
onApprove={onApprove}
|
||||
onReject={sendReject}
|
||||
/>
|
||||
);
|
||||
function sendReject() {
|
||||
popRequest();
|
||||
postMessage({
|
||||
error: 'Transaction cancelled',
|
||||
id: request.id,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography>Please keep this window open in the background.</Typography>
|
||||
<ApproveSignatureForm
|
||||
key={request.id}
|
||||
autoApprove={autoApprove}
|
||||
origin={origin}
|
||||
messages={messages}
|
||||
onApprove={onApprove}
|
||||
onReject={sendReject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -234,6 +326,11 @@ const useStyles = makeStyles((theme) => ({
|
|||
|
||||
function ApproveConnectionForm({ origin, onApprove }) {
|
||||
const wallet = useWallet();
|
||||
const { accounts } = useWalletSelector();
|
||||
// TODO better way to do this
|
||||
const account = accounts.find((account) =>
|
||||
account.address.equals(wallet.publicKey),
|
||||
);
|
||||
const classes = useStyles();
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
let [dismissed, setDismissed] = useLocalStorageState(
|
||||
|
@ -249,7 +346,10 @@ function ApproveConnectionForm({ origin, onApprove }) {
|
|||
<div className={classes.connection}>
|
||||
<Typography>{origin}</Typography>
|
||||
<ImportExportIcon fontSize="large" />
|
||||
<Typography>{wallet.publicKey.toBase58()}</Typography>
|
||||
<Typography>{account.name}</Typography>
|
||||
<Typography variant="caption">
|
||||
({wallet.publicKey.toBase58()})
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography>Only connect with sites you trust.</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
|
@ -330,7 +430,11 @@ function isSafeInstruction(publicKeys, owner, txInstructions) {
|
|||
} else {
|
||||
if (instruction.type === 'raydium') {
|
||||
// Whitelist raydium for now.
|
||||
} else if (['cancelOrder', 'matchOrders', 'cancelOrderV3'].includes(instruction.type)) {
|
||||
} else if (
|
||||
['cancelOrder', 'matchOrders', 'cancelOrderV3'].includes(
|
||||
instruction.type,
|
||||
)
|
||||
) {
|
||||
// It is always considered safe to cancel orders, match orders
|
||||
} else if (instruction.type === 'systemCreate') {
|
||||
let { newAccountPubkey } = instruction.data;
|
||||
|
@ -500,10 +604,19 @@ function ApproveSignatureForm({
|
|||
);
|
||||
case 'newOrderV3':
|
||||
return (
|
||||
<NewOrder instruction={instruction} onOpenAddress={onOpenAddress} v3={true} />
|
||||
<NewOrder
|
||||
instruction={instruction}
|
||||
onOpenAddress={onOpenAddress}
|
||||
v3={true}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <UnknownInstruction instruction={instruction} onOpenAddress={onOpenAddress} />;
|
||||
return (
|
||||
<UnknownInstruction
|
||||
instruction={instruction}
|
||||
onOpenAddress={onOpenAddress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,13 +4,33 @@ import BalancesList from '../components/BalancesList';
|
|||
import Grid from '@material-ui/core/Grid';
|
||||
import { useIsProdNetwork } from '../utils/connection';
|
||||
import DebugButtons from '../components/DebugButtons';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { useIsExtensionWidth } from '../utils/utils';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
[theme.breakpoints.down(theme.ext)]: {
|
||||
padding: 0,
|
||||
},
|
||||
[theme.breakpoints.up(theme.ext)]: {
|
||||
maxWidth: 'md',
|
||||
},
|
||||
},
|
||||
balancesContainer: {
|
||||
[theme.breakpoints.down(theme.ext)]: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default function WalletPage() {
|
||||
const classes = useStyles();
|
||||
const isProdNetwork = useIsProdNetwork();
|
||||
const isExtensionWidth = useIsExtensionWidth();
|
||||
return (
|
||||
<Container fixed maxWidth="md">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Container fixed maxWidth="md" className={classes.container}>
|
||||
<Grid container spacing={isExtensionWidth ? 0 : 3}>
|
||||
<Grid item xs={12} className={classes.balancesContainer}>
|
||||
<BalancesList />
|
||||
</Grid>
|
||||
{isProdNetwork ? null : (
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
const ConnectedWalletsContext = createContext({});
|
||||
|
||||
export const ConnectedWalletsProvider = ({ children }) => {
|
||||
const [connectedWallets, setConnectedWallets] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const updateConnectionAmount = () => {
|
||||
chrome.storage.local.get('connectedWallets', (result) => {
|
||||
setConnectedWallets(result.connectedWallets || {});
|
||||
});
|
||||
};
|
||||
const listener = (changes) => {
|
||||
if ('connectedWallets' in changes) {
|
||||
updateConnectionAmount();
|
||||
}
|
||||
};
|
||||
updateConnectionAmount();
|
||||
chrome.storage.local.onChanged.addListener(listener);
|
||||
return () => chrome.storage.local.onChanged.removeListener(listener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConnectedWalletsContext.Provider value={connectedWallets}>
|
||||
{children}
|
||||
</ConnectedWalletsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useConnectedWallets = () => useContext(ConnectedWalletsContext);
|
|
@ -0,0 +1,15 @@
|
|||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const PageContext = createContext('wallet');
|
||||
|
||||
export const PageProvider = ({ children }) => {
|
||||
const [page, setPage] = useState('wallet');
|
||||
|
||||
return (
|
||||
<PageContext.Provider value={[page, setPage]}>
|
||||
{children}
|
||||
</PageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePage = () => useContext(PageContext);
|
|
@ -4,6 +4,7 @@ import ERC20_ABI from './erc20-abi.json';
|
|||
import SWAP_ABI from './swap-abi.json';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { useCallAsync } from '../notifications';
|
||||
import { isExtension } from '../utils';
|
||||
|
||||
const web3 = new Web3(window.ethereum);
|
||||
// Change to use estimated gas limit
|
||||
|
@ -234,11 +235,11 @@ export function ConnectToMetamaskButton() {
|
|||
color="primary"
|
||||
variant="outlined"
|
||||
component="a"
|
||||
href="https://metamask.io/"
|
||||
href={isExtension ? 'https://sollet.io' : 'https://metamask.io/'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Connect to MetaMask
|
||||
{isExtension ? 'Open sollet.io' : 'Connect to MetaMask'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -169,7 +169,6 @@ function publicKeyLayout(property) {
|
|||
return new PublicKeyLayout(property);
|
||||
}
|
||||
|
||||
|
||||
export const OWNER_VALIDATION_PROGRAM_ID = new PublicKey(
|
||||
'4MNPdKu9wFMvEeZBMt3Eipfs5ovVWTJb31pEXDJAAxX5',
|
||||
);
|
||||
|
|
|
@ -132,12 +132,12 @@ const toInstruction = async (
|
|||
} else {
|
||||
return {
|
||||
type: 'Unknown',
|
||||
accountMetas: instruction.accounts.map(index => ({
|
||||
publicKey: accountKeys[index],
|
||||
isWritable: transactionMessage.isAccountWritable(index)
|
||||
accountMetas: instruction.accounts.map((index) => ({
|
||||
publicKey: accountKeys[index],
|
||||
isWritable: transactionMessage.isAccountWritable(index),
|
||||
})),
|
||||
programId
|
||||
}
|
||||
programId,
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
|
@ -411,7 +411,7 @@ const getNewOrderV3Data = (accounts, accountKeys) => {
|
|||
NEW_ORDER_V3_OWNER_INDEX,
|
||||
);
|
||||
return { openOrdersPubkey, ownerPubkey };
|
||||
}
|
||||
};
|
||||
|
||||
const getSettleFundsData = (accounts, accountKeys) => {
|
||||
const basePubkey = getAccountByIndex(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Account, Connection, PublicKey } from '@solana/web3.js';
|
||||
import { useMediaQuery } from '@material-ui/core';
|
||||
import * as bs58 from 'bs58';
|
||||
|
||||
export async function sleep(ms: number) {
|
||||
|
@ -87,6 +88,14 @@ export async function confirmTransaction(
|
|||
return result.value;
|
||||
}
|
||||
|
||||
// TODO consolidate popup dimensions
|
||||
export function useIsExtensionWidth() {
|
||||
return useMediaQuery('(max-width:450px)');
|
||||
}
|
||||
|
||||
export const isExtension = window.location.protocol === 'chrome-extension:';
|
||||
|
||||
export const isExtensionPopup = isExtension && window.opener;
|
||||
/**
|
||||
* Returns an account object when given the private key
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@ import { randomBytes, secretbox } from 'tweetnacl';
|
|||
import * as bip32 from 'bip32';
|
||||
import bs58 from 'bs58';
|
||||
import { EventEmitter } from 'events';
|
||||
import { isExtension } from './utils';
|
||||
|
||||
export async function generateMnemonicAndSeed() {
|
||||
const bip39 = await import('bip39');
|
||||
|
@ -170,5 +171,10 @@ export function forgetWallet() {
|
|||
importsEncryptionKey: null,
|
||||
};
|
||||
walletSeedChanged.emit('change', unlockedMnemonicAndSeed);
|
||||
window.location.reload();
|
||||
if (isExtension) {
|
||||
// Must use wrapper function for window.location.reload
|
||||
chrome.storage.local.clear(() => window.location.reload());
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue