diff --git a/.erb/configs/webpack.config.base.ts b/.erb/configs/webpack.config.base.ts index 2356a9e2..dc2c8ea0 100644 --- a/.erb/configs/webpack.config.base.ts +++ b/.erb/configs/webpack.config.base.ts @@ -41,6 +41,11 @@ const configuration: webpack.Configuration = { resolve: { extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], modules: [webpackPaths.srcPath, 'node_modules'], + fallback: + { + 'stream': require.resolve('stream-browserify'), + 'crypto': require.resolve('crypto-browserify') + } }, plugins: [ diff --git a/package.json b/package.json index 2801e52b..9cf543e2 100644 --- a/package.json +++ b/package.json @@ -242,9 +242,15 @@ "@fortawesome/free-solid-svg-icons": "^6.1.0", "@fortawesome/react-fontawesome": "^0.1.18", "@reduxjs/toolkit": "^1.7.2", + "@solana/wallet-adapter-base": "^0.9.5", + "@solana/wallet-adapter-react": "^0.15.4", + "@solana/wallet-adapter-react-ui": "^0.9.6", + "@solana/wallet-adapter-wallets": "^0.15.5", "amplitude-js": "^8.12.0", + "bip39": "^3.0.4", "bootstrap": "^5.1.3", "command-exists": "^1.2.9", + "crypto-browserify": "^3.12.0", "electron-cfg": "^1.2.7", "electron-debug": "^3.2.0", "electron-log": "^4.4.6", @@ -265,6 +271,7 @@ "react-toastify": "^9.0.1", "regenerator-runtime": "^0.13.9", "shelljs": "^0.8.5", + "stream-browserify": "^3.0.0", "styled-components": "^5.3.3", "typescript-lru-cache": "^1.2.3", "underscore": "^1.13.1", diff --git a/src/main/ipc/accounts.ts b/src/main/ipc/accounts.ts index ca8ce561..29c1bdde 100644 --- a/src/main/ipc/accounts.ts +++ b/src/main/ipc/accounts.ts @@ -1,9 +1,34 @@ import cfg from 'electron-cfg'; import promiseIpc from 'electron-promise-ipc'; -import type { IpcMainEvent, IpcRendererEvent } from 'electron'; +import { IpcMainEvent, IpcRendererEvent } from 'electron'; + +import * as web3 from '@solana/web3.js'; +import * as bip39 from 'bip39'; + +import { NewKeyPairInfo } from '../../types/types'; import { logger } from '../logger'; +async function createNewKeypair(): Promise { + const mnemonic = bip39.generateMnemonic(); + const seed = await bip39.mnemonicToSeed(mnemonic); + const newKeypair = web3.Keypair.fromSeed(seed.slice(0, 32)); + + logger.silly( + `main generated new account${newKeypair.publicKey.toString()} ${JSON.stringify( + newKeypair + )}` + ); + + const val = { + privatekey: newKeypair.secretKey, + mnemonic, + }; + cfg.set(`accounts.${newKeypair.publicKey.toString()}`, val); + + return val; +} + declare type IpcEvent = IpcRendererEvent & IpcMainEvent; // Need to import the file and call a function (from the main process) to get the IPC promise to exist. @@ -26,6 +51,13 @@ export function initAccountPromises() { return cfg.set(`accounts.${key}`, val); } ); + promiseIpc.on( + 'ACCOUNT-CreateNew', + (event: IpcEvent | undefined): Promise => { + logger.silly(`main: called ACCOUNT-CreateNew, ${event}`); + return createNewKeypair(); + } + ); } export default {}; diff --git a/src/main/ipc/config.ts b/src/main/ipc/config.ts index 94a06d25..e5c059fa 100644 --- a/src/main/ipc/config.ts +++ b/src/main/ipc/config.ts @@ -11,6 +11,7 @@ declare type IpcEvent = IpcRendererEvent & IpcMainEvent; // Need to import the file and call a function (from the main process) to get the IPC promise to exist. export function initConfigPromises() { + logger.info(`Config file at ${cfg.file()}`); // gets written to .\AppData\Roaming\SolanaWorkbench\electron-cfg.json on windows promiseIpc.on('CONFIG-GetAll', (event: IpcEvent | undefined) => { logger.silly('main: called CONFIG-GetAll', event); diff --git a/src/renderer/App.scss b/src/renderer/App.scss index 2dde03f2..833c3ade 100644 --- a/src/renderer/App.scss +++ b/src/renderer/App.scss @@ -193,6 +193,13 @@ th, overflow-y: auto; } +// Wallet adapter colours so its easier to see our dragonfly logo +// this isn't really right, but i don't like the purple, as it implies Phantom wallet? +// .wallet-adapter-button-trigger { +// background-color: powderblue !important; +// color: steelblue !important; +// } + // EdiText - https://github.com/alioguzhan/react-editext#styling-with-styled-components div[editext='view-container'], div[editext='view'], diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 33ee93c0..387bc01e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import isElectron from 'is-electron'; import './App.scss'; +import * as sol from '@solana/web3.js'; import { Routes, Route, NavLink, Outlet } from 'react-router-dom'; @@ -25,19 +26,40 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; -import { useState } from 'react'; +import { FC, useMemo, useState } from 'react'; + +import { + ConnectionProvider, + WalletProvider, +} from '@solana/wallet-adapter-react'; +// import { LedgerWalletAdapter } from '@solana/wallet-adapter-wallets'; +import { + WalletModalProvider, + WalletMultiButton, +} from '@solana/wallet-adapter-react-ui'; +import { ElectronAppStorageWalletAdapter } from './wallet-adapter/electronAppStorage'; + import Account from './nav/Account'; import Anchor from './nav/Anchor'; import Validator from './nav/Validator'; import ValidatorNetworkInfo from './nav/ValidatorNetworkInfo'; -import { useAppDispatch } from './hooks'; +import { useAppDispatch, useAppSelector } from './hooks'; import { useConfigState, setConfigValue, ConfigKey, } from './data/Config/configState'; +import { useAccountsState } from './data/accounts/accountState'; import ValidatorNetwork from './data/ValidatorNetwork/ValidatorNetwork'; +import { + netToURL, + selectValidatorNetworkState, +} from './data/ValidatorNetwork/validatorNetworkState'; +import { getElectronStorageWallet } from './data/accounts/account'; + +// Default styles that can be overridden by your app +require('@solana/wallet-adapter-react-ui/styles.css'); const logger = window.electron.log; @@ -178,6 +200,7 @@ function Topbar() { > +
@@ -244,26 +267,66 @@ function AnalyticsBanner() { ); } -function GlobalContainer() { - // Note: NavLink is not compatible with react-router-dom's NavLink, so just add the styling +export const GlobalContainer: FC = () => { + const dispatch = useAppDispatch(); + const config = useConfigState(); + const accounts = useAccountsState(); + const { net } = useAppSelector(selectValidatorNetworkState); + + const wallets = useMemo(() => { + const electronStorageWallet = new ElectronAppStorageWalletAdapter({ + accountFn: (): Promise => { + if (!config) { + throw Error( + "Config not loaded, can't get ElectronWallet keypair yet" + ); + } + + return getElectronStorageWallet(dispatch, config, accounts); + }, + }); + return [ + // Sadly, electron apps don't run browser plugins, so these won't work without lots of pain + // new PhantomWalletAdapter(), + // new SlopeWalletAdapter(), + // new SolflareWalletAdapter({ network }), + // new TorusWalletAdapter(), + // new LedgerWalletAdapter(), + // new SolletWalletAdapter({ network }), + // new SolletExtensionWalletAdapter({ network }), + electronStorageWallet, + // new LocalStorageWalletAdapter({ endpoint }), + ]; + }, [accounts, config, dispatch]); + + if (config.loading || accounts.loading) { + return <>Config Loading ...${accounts.loading}; + } return (
- - - - - + + + + + + + + + + +
); -} +}; function App() { const config = useConfigState(); + const accounts = useAccountsState(); Object.assign(console, logger.functions); if (config.loading) { - return <>Config Loading ...; + return <>Config Loading ...${accounts.loading}; } if (!config.values || !(`${ConfigKey.AnalyticsEnabled}` in config.values)) { return ; diff --git a/src/renderer/components/LogView.tsx b/src/renderer/components/LogView.tsx index 198eb71c..03026a54 100644 --- a/src/renderer/components/LogView.tsx +++ b/src/renderer/components/LogView.tsx @@ -60,13 +60,15 @@ function LogView() { return () => { const sub = logSubscriptions[net]; - sub.solConn - .removeOnLogsListener(sub.subscriptionID) - // eslint-disable-next-line promise/always-return - .then(() => { - delete logSubscriptions[net]; - }) - .catch(logger.info); + if (sub?.solConn) { + sub.solConn + .removeOnLogsListener(sub.subscriptionID) + // eslint-disable-next-line promise/always-return + .then(() => { + delete logSubscriptions[net]; + }) + .catch(logger.info); + } }; }, [net, status]); diff --git a/src/renderer/components/ProgramChangeView.tsx b/src/renderer/components/ProgramChangeView.tsx index 65cd6f49..0cb41cdb 100644 --- a/src/renderer/components/ProgramChangeView.tsx +++ b/src/renderer/components/ProgramChangeView.tsx @@ -13,13 +13,11 @@ import ButtonGroup from 'react-bootstrap/ButtonGroup'; import ButtonToolbar from 'react-bootstrap/ButtonToolbar'; import Table from 'react-bootstrap/Table'; import { toast } from 'react-toastify'; -import Popover from 'react-bootstrap/Popover'; import EdiText from 'react-editext'; import OutsideClickHandler from 'react-outside-click-handler'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import { Keypair } from '@solana/web3.js'; +import { useWallet } from '@solana/wallet-adapter-react'; import { setSelected, accountsActions, @@ -43,7 +41,9 @@ import { } from '../data/accounts/programChanges'; import createNewAccount from '../data/accounts/account'; import WatchAccountButton from './WatchAccountButton'; -import { setAccountValues } from '../data/accounts/accountState'; +import InlinePK from './InlinePK'; + +const logger = window.electron.log; export const MAX_PROGRAM_CHANGES_DISPLAYED = 20; export enum KnownProgramID { @@ -83,6 +83,7 @@ function ProgramChangeView() { const [sortColumn, setSortColumn] = useState(SortColumn.MaxDelta); const [validatorSlot, setValidatorSlot] = useState(0); const [pinnedAccount, setPinnedAccount] = useState({}); + const WalletAdapterState = useWallet(); function sortFunction(a: AccountInfo, b: AccountInfo) { switch (sortColumn) { @@ -106,9 +107,17 @@ function ProgramChangeView() { const pinMap: PinnedAccountMap = {}; const showKeys: string[] = []; // list of Keys - pinnedAccounts.forEach((key: string) => { + // Add the solana wallet's account to the monitored list (if its not already watched.) + if (WalletAdapterState.publicKey) { + const key = WalletAdapterState.publicKey.toString(); showKeys.push(key); pinMap[key] = true; + } + pinnedAccounts.forEach((key: string) => { + if (!(key in pinMap)) { + showKeys.push(key); + pinMap[key] = true; + } }); const changes = GetTopAccounts( @@ -118,9 +127,9 @@ function ProgramChangeView() { ); // logger.info('GetTopAccounts', changes); - changes.forEach((c: string) => { - if (!(c in pinnedAccount)) { - showKeys.push(c); + changes.forEach((key: string) => { + if (!(key in pinMap)) { + showKeys.push(key); } }); setPinnedAccount(pinMap); @@ -134,7 +143,6 @@ function ProgramChangeView() { const [programID, setProgramID] = useState( KnownProgramID.SystemProgram ); - const [anchorEl, setAnchorEl] = useState(undefined); useEffect(() => { if (status !== NetStatus.Running) { @@ -238,68 +246,28 @@ function ProgramChangeView() { - - - New Account - - - -
New Account Keypair created.
-
- Public Key:{' '} -
-                        {anchorEl?.publicKey.toString()}
-                      
-
-
- Private Key: (keep this in a .json file - somewhere safe) -
-