diff --git a/package.json b/package.json index d5a7acf8..fb5494b0 100644 --- a/package.json +++ b/package.json @@ -245,8 +245,10 @@ "amplitude-js": "^8.12.0", "bootstrap": "^5.1.3", "command-exists": "^1.2.9", + "electron-cfg": "^1.2.7", "electron-debug": "^3.2.0", "electron-log": "^4.4.6", + "electron-promise-ipc": "^2.2.4", "electron-updater": "^5.0.1", "hexdump-nodejs": "^0.1.0", "history": "^5.2.0", diff --git a/src/main/accounts.ts b/src/main/accounts.ts deleted file mode 100644 index e6412f28..00000000 --- a/src/main/accounts.ts +++ /dev/null @@ -1,198 +0,0 @@ -// import * as sol from '@solana/web3.js'; -// import fs from 'fs'; - -// import { netToURL } from '../common/strings'; -// import { -// AccountsRequest, -// AccountsResponse, -// GetAccountRequest, -// GetAccountResponse, -// ImportAccountRequest, -// ImportAccountResponse, -// Net, -// UpdateAccountRequest, -// WBAccount, -// } from '../types/types'; -// import db from './db'; -// import { logger } from './logger'; -// import { KEY_PATH } from './const'; - -// const hexdump = require('hexdump-nodejs'); - -// const HEXDUMP_BYTES = 512; -// const AIRDROP_AMOUNT = 100; - -// const addKeypair = async (kpPath: string) => { -// const kp = sol.Keypair.generate(); - -// // goofy looking but otherwise stringify encodes Uint8Array like: -// // {"0": 1, "1": 2, "2": 3 ...} -// const secretKeyUint = Array.from(Uint8Array.from(kp.secretKey)); -// const fileContents = JSON.stringify(secretKeyUint); -// await fs.promises.writeFile(kpPath, fileContents); -// }; - -// const localKeypair = async (f: string): Promise => { -// const fileContents = await fs.promises.readFile(f); -// const data = Uint8Array.from(JSON.parse(fileContents.toString())); -// return sol.Keypair.fromSecretKey(data); -// }; - -// function deleteAccount(msg: ImportAccountRequest) { -// const { pubKey, net } = msg; -// db.accounts.delete(pubKey, net); -// } - -// async function getAccount(msg: GetAccountRequest): Promise { -// const { net, pubKey } = msg; -// const solConn = new sol.Connection(netToURL(net)); -// const resp: GetAccountResponse = {}; -// try { -// const key = new sol.PublicKey(pubKey); -// const solAccount = await solConn.getAccountInfo(key); -// let solAmount = 0; -// if (solAccount?.lamports) -// solAmount = solAccount.lamports / sol.LAMPORTS_PER_SOL; -// const hexDump = hexdump(solAccount?.data.subarray(0, HEXDUMP_BYTES)); -// if (solAccount !== null) { -// resp.account = { -// net, -// pubKey, -// solAmount, -// hexDump, -// executable: solAccount.executable, -// exists: true, -// }; -// } else { -// resp.account = { net, pubKey, exists: false, executable: false }; -// } -// } catch (e) { -// resp.err = e as Error; -// } -// return resp; -// } - -// async function accounts(msg: AccountsRequest): Promise { -// const { net } = msg; -// try { -// await fs.promises.access(KEY_PATH); -// } catch { -// logger.info('Creating root key', { KEY_PATH }); -// await addKeypair(KEY_PATH); -// } -// const kp = await localKeypair(KEY_PATH); -// const solConn = new sol.Connection(netToURL(net)); -// const existingAccounts = await db.accounts.all(net); -// logger.info('existingAccounts', { existingAccounts }); -// if (existingAccounts?.length > 0) { -// const pubKeys = existingAccounts.map((a) => { -// return new sol.PublicKey(a.pubKey); -// }); -// const solAccountInfo = await solConn.getMultipleAccountsInfo(pubKeys); -// const mergedAccountInfo: WBAccount[] = solAccountInfo.map( -// (solAccount: sol.AccountInfo | null, i: number) => { -// const key = new sol.PublicKey(existingAccounts[i].pubKey); -// const { humanName } = existingAccounts[i]; -// const exists = false; -// const newAcc: WBAccount = { -// net, -// humanName, -// exists, -// pubKey: key.toString(), -// }; -// if (solAccount) { -// newAcc.solAmount = solAccount.lamports / sol.LAMPORTS_PER_SOL; -// newAcc.hexDump = hexdump(solAccount?.data.subarray(0, HEXDUMP_BYTES)); -// newAcc.exists = true; -// } -// return newAcc; -// } -// ); -// return { -// rootKey: kp.publicKey.toString(), -// accounts: mergedAccountInfo, -// }; -// } -// const createdAccounts: sol.Keypair[] = []; -// if (net === Net.Localhost) { -// await solConn.confirmTransaction( -// await solConn.requestAirdrop( -// kp.publicKey, -// AIRDROP_AMOUNT * sol.LAMPORTS_PER_SOL -// ), -// 'finalized' -// ); -// const N_ACCOUNTS = 5; -// const txn = new sol.Transaction(); -// for (let i = 0; i < N_ACCOUNTS; i += 1) { -// const acc = new sol.Keypair(); -// txn.add( -// sol.SystemProgram.createAccount({ -// fromPubkey: kp.publicKey, -// newAccountPubkey: acc.publicKey, -// space: 0, -// lamports: 10 * sol.LAMPORTS_PER_SOL, -// programId: sol.SystemProgram.programId, -// }) -// ); -// logger.info('adding account', { -// acc_pubkey: acc.publicKey, -// }); - -// createdAccounts.push(acc); -// } - -// const txnID = await sol.sendAndConfirmTransaction( -// solConn, -// txn, -// [kp, createdAccounts].flat(), -// { commitment: 'finalized' } -// ); - -// logger.info('created accounts', { txnID }); - -// createdAccounts.forEach((acc, i) => { -// db.accounts.insert( -// acc.publicKey.toString(), -// Net.Localhost, -// `Wallet ${i}` -// ); -// }); -// } - -// return { -// rootKey: kp.publicKey.toString(), -// // todo: this should be on created accounts from DB -// accounts: createdAccounts.map((acc, i) => { -// return { -// net, -// exists: true, -// art: '', -// pubKey: acc.publicKey.toString(), -// humanName: `Wallet ${i}`, -// }; -// }), -// }; -// } - -// async function updateAccountName(msg: UpdateAccountRequest) { -// const { net, pubKey, humanName } = msg; -// const res = await db.accounts.updateHumanName(pubKey, net, humanName); -// return res; -// } - -// async function importAccount( -// msg: ImportAccountRequest -// ): Promise { -// const { net, pubKey } = msg; -// await db.accounts.insert(pubKey, net, ''); -// return { net }; -// } - -// export { -// importAccount, -// accounts, -// getAccount, -// deleteAccount, -// updateAccountName, -// }; diff --git a/src/main/config.ts b/src/main/config.ts index 306990d2..83b6259e 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -1,21 +1,32 @@ -import { - ConfigAction, - ConfigMap, - WBConfigRequest, - WBConfigResponse, -} from '../types/types'; -import db from './db'; +import cfg from 'electron-cfg'; +import promiseIpc from 'electron-promise-ipc'; +import type { IpcMainEvent, IpcRendererEvent } from 'electron'; + import { logger } from './logger'; -async function wbConfig(msg: WBConfigRequest): Promise { - const { action, key } = msg; - if (action === ConfigAction.Set) { - const { val } = msg; - await db.config.set(key, val); - } - const values: ConfigMap = await db.config.all(); - logger.info('config values', { values: JSON.stringify(values) }); - return { values }; +declare type IpcEvent = IpcRendererEvent & IpcMainEvent; + +// NOTE: using the electron-cfg window size code can reault in the window shrinking every time the app restarts +// Sven has seen it on windows with one 4k screen at 100%, the other at 200% + +// Need to import the file and call a function (from the main process) to get the IPC promise to exist. +export function initConfigPromises() { + // 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); + const config = cfg.get('config'); + if (!config) { + return {}; + } + return config; + }); + promiseIpc.on( + 'CONFIG-Set', + (key: unknown, val: unknown, event?: IpcEvent | undefined) => { + logger.silly(`main: called CONFIG-Set, ${key}, ${val}, ${event}`); + return cfg.set(`config.${key}`, val); + } + ); } -export default wbConfig; +export default {}; diff --git a/src/main/db.ts b/src/main/db.ts deleted file mode 100644 index 75b52f24..00000000 --- a/src/main/db.ts +++ /dev/null @@ -1,142 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import * as sol from '@solana/web3.js'; -import { Net, ConfigMap } from 'types/types'; -import { netToURL } from '../common/strings'; -import { CONFIG_FILE_PATH, ACCOUNTS_DIR_PATH } from './const'; -import { logger } from './logger'; - -// PersistedAccount[File] allow us to store -// accounts in a file format that solana-test-validator -// understands with the --account flag. -// i.e., what is given by the following command: -// $ solana account --output-file --output json -type PersistedAccount = { - lamports: number; - data: string[]; - owner: string; - executable: boolean; - rentEpoch: number | undefined; -}; - -type PersistedAccountFile = { - pubkey: string; - humanName: string; - importedAt: string; - net: Net; - account: PersistedAccount; -}; - -const acctFile = (pubKey: string, net: Net): string => { - return path.join(ACCOUNTS_DIR_PATH, `${net}_${pubKey}.json`); -}; - -const db = { - config: { - all: async (): Promise => { - const config = await JSON.parse( - (await fs.promises.readFile(CONFIG_FILE_PATH)).toString() - ); - return config; - }, - get: async (key: string): Promise => { - const cfg = await db.config.all(); - return cfg[key]; - }, - set: async (key: string, val: string | undefined) => { - const cfg = await db.config.all(); - cfg[key] = val; - try { - await fs.promises.writeFile(CONFIG_FILE_PATH, JSON.stringify(cfg)); - } catch (err) { - logger.error('Error writing config', { err }); - } - }, - }, - accounts: { - /* - all: async (net: Net): Promise => { - const accountFileNames = await fs.promises.readdir(ACCOUNTS_DIR_PATH); - const accountFiles = await Promise.all( - accountFileNames.map(async (f: string): Promise => { - const accountFileData = await fs.promises.readFile( - path.join(ACCOUNTS_DIR_PATH, f) - ); - const persistedAccount: PersistedAccountFile = JSON.parse( - accountFileData.toString() - ); - return { - importedAt: persistedAccount.importedAt, - net: persistedAccount.net, - pubKey: persistedAccount.pubkey, - humanName: persistedAccount.humanName, - }; - }) - ); - return accountFiles - .sort((acctA, acctB) => { - if (acctA.importedAt && acctB.importedAt) { - return acctA.importedAt.localeCompare(acctB.importedAt); - } - return 0; - }) - .sort((acctA, acctB) => { - if (acctA.importedAt && acctB.importedAt) { - return Date.parse(acctB.importedAt) - Date.parse(acctA.importedAt); - } - return 0; - }) - .filter((acct) => { - return acct.net === net; - }); - }, - */ - insert: async (pubKey: string, net: Net, humanName: string) => { - const solConn = new sol.Connection(netToURL(net)); - const key = new sol.PublicKey(pubKey); - const solAccount = await solConn.getAccountInfo(key); - const importedAt = new Date().toString(); - try { - if (solAccount) { - const persistedAccount: PersistedAccountFile = { - pubkey: pubKey, - humanName, - net, - importedAt, - account: { - lamports: solAccount.lamports, - data: [solAccount.data.toString('base64'), 'base64'], - owner: solAccount.owner.toString(), - executable: solAccount.executable, - rentEpoch: solAccount.rentEpoch, - }, - }; - await fs.promises.writeFile( - acctFile(pubKey, net), - JSON.stringify(persistedAccount) - ); - } else { - throw new Error('Account not found'); - } - } catch (err) { - logger.error('Error writing account JSON', { err }); - } - }, - updateHumanName: async (pubKey: string, net: Net, humanName: string) => { - const acct: PersistedAccountFile = JSON.parse( - (await fs.promises.readFile(acctFile(pubKey, net))).toString() - ); - acct.humanName = humanName; - await fs.promises.writeFile(acctFile(pubKey, net), JSON.stringify(acct)); - }, - delete: async (pubKey: string, net: Net) => { - try { - await fs.promises.rm(acctFile(pubKey, net)); - } catch (err) { - logger.error('Error deleting account JSON', { err }); - } - }, - }, -}; - -export default db; diff --git a/src/main/main.ts b/src/main/main.ts index c78a0242..318dfe08 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -8,23 +8,14 @@ import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import { logger, initLogging } from './logger'; import { runValidator, validatorLogs } from './validator'; -// import { -// accounts, -// getAccount, -// importAccount, -// deleteAccount, -// updateAccountName, -// } from './accounts'; -import wbConfig from './config'; +import { initConfigPromises } from './config'; import fetchAnchorIdl from './anchor'; -// import fetchValidatorNetworkInfo from './validatorNetworkInfo'; import { subscribeTransactionLogs, unsubscribeTransactionLogs, } from './transactionLogs'; import { RESOURCES_PATH } from './const'; -// import wbConfig from './config'; export default class AppUpdater { constructor() { @@ -37,6 +28,8 @@ export default class AppUpdater { let mainWindow: BrowserWindow | null = null; const MAX_STRING_LOG_LENGTH = 32; +initConfigPromises(); + ipcMain.on( 'main', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -48,9 +41,6 @@ ipcMain.on( case 'run-validator': await runValidator(); break; - // case 'accounts': - // res = await accounts(msg); - // break; case 'validator-logs': res = await validatorLogs(msg); break; @@ -58,24 +48,6 @@ ipcMain.on( res = await fetchAnchorIdl(msg); logger.debug(`fetchIDL(${msg}: (${res})`); break; - // case 'update-account-name': - // await updateAccountName(msg); - // break; - // case 'import-account': - // await importAccount(msg); - // break; - // case 'get-account': - // res = await getAccount(msg); - // break; - // case 'delete-account': - // await deleteAccount(msg); - // break; - case 'config': - res = await wbConfig(msg); - break; - // case 'get-validator-network-info': - // res = await fetchValidatorNetworkInfo(msg); - // break; case 'subscribe-transaction-logs': await subscribeTransactionLogs(event, msg); break; diff --git a/src/main/preload.js b/src/main/preload.js index f89ffabc..9bc1afe3 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -1,6 +1,6 @@ const { contextBridge, ipcRenderer } = require('electron'); const log = require('electron-log'); - +const promiseIpc = require('electron-promise-ipc'); // TODO: make this a setting... log.transports.console.level = 'silly'; log.transports.ipc.level = 'silly'; @@ -18,48 +18,12 @@ contextBridge.exposeInMainWorld('electron', { validatorState(msg) { send('validator-state', msg); }, - // accounts(msg) { - // send('accounts', msg); - // }, - // addKeypair() { - // send('add-keypair', {}); - // }, - // airdropTokens(msg) { - // send('airdrop', msg); - // }, validatorLogs(msg) { send('validator-logs', msg); }, fetchAnchorIDL(msg) { send('fetch-anchor-idl', msg); }, - // fetchValidatorNetworkInfo(msg) { - // send('get-validator-network-info', msg); - // }, - // updateAccountName(msg) { - // send('update-account-name', msg); - // }, - // importAccount(msg) { - // send('import-account', msg); - // }, - // getAccount(msg) { - // send('get-account', msg); - // }, - // deleteAccount(msg) { - // send('delete-account', msg); - // }, - // onProgramLog(msg) { - // send('get-account', msg); - // }, - config(msg) { - send('config', msg); - }, - // subscribeTransactionLogs(msg) { - // send('subscribe-transaction-logs', msg); - // }, - // unsubscribeTransactionLogs(msg) { - // send('unsubscribe-transaction-logs', msg); - // }, on(method, func) { ipcRenderer.on(method, (event, ...args) => func(...args)); }, @@ -74,3 +38,10 @@ contextBridge.exposeInMainWorld('electron', { }, }, }); + +contextBridge.exposeInMainWorld('promiseIpc', { + send: (event, ...args) => promiseIpc.send(event, ...args), + on: (event, listener) => promiseIpc.on(event, listener), + off: (event, listener) => promiseIpc.off(event, listener), + removeAllListeners: (event) => promiseIpc.removeAllListeners(event), +}); diff --git a/src/main/validatorNetworkInfo.ts b/src/main/validatorNetworkInfo.ts deleted file mode 100644 index f254dd99..00000000 --- a/src/main/validatorNetworkInfo.ts +++ /dev/null @@ -1,51 +0,0 @@ -// import * as sol from '@solana/web3.js'; -// import { netToURL } from '../common/strings'; -// import { -// VCount, -// ValidatorNetworkInfoResponse, -// ValidatorNetworkInfoRequest, -// } from '../types/types'; - -// interface VersionCount { -// [key: string]: number; -// } - -// const fetchValidatorNetworkInfo = async (msg: ValidatorNetworkInfoRequest) => { -// const url = netToURL(msg.net); -// const solConn = new sol.Connection(url); -// const contactInfo = await solConn.getClusterNodes(); -// const nodeVersion = await solConn.getVersion(); - -// const frequencyCount: VersionCount = {}; - -// contactInfo.map((info: sol.ContactInfo) => { -// let version = 'none'; -// if (info.version) { -// version = info.version; -// } - -// if (frequencyCount[version]) { -// frequencyCount[version] += 1; -// } else { -// frequencyCount[version] = 1; -// } -// return undefined; -// }); -// const versions: VCount[] = []; -// Object.entries(frequencyCount).forEach(([version, count]) => { -// versions.push({ -// version, -// count, -// }); -// }); - -// const response: ValidatorNetworkInfoResponse = { -// nodes: contactInfo, -// version: nodeVersion['solana-core'], -// versionCount: versions, -// }; - -// return response; -// }; - -// export default fetchValidatorNetworkInfo; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index acfd8434..33ee93c0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -25,19 +25,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; -import { ConfigAction } from 'types/types'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import Account from './nav/Account'; import Anchor from './nav/Anchor'; import Validator from './nav/Validator'; import ValidatorNetworkInfo from './nav/ValidatorNetworkInfo'; -import { useAppDispatch, useAppSelector } from './hooks'; +import { useAppDispatch } from './hooks'; import { + useConfigState, setConfigValue, - selectConfigState, ConfigKey, - setConfig, } from './data/Config/configState'; import ValidatorNetwork from './data/ValidatorNetwork/ValidatorNetwork'; @@ -48,6 +46,8 @@ declare global { interface Window { // eslint-disable-next-line @typescript-eslint/no-explicit-any electron?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + promiseIpc?: any; } } @@ -230,13 +230,11 @@ function AnalyticsBanner() { type="button" onClick={() => { dispatch( - setConfigValue({ key: ConfigKey.AnalyticsEnabled, value: false }) + setConfigValue({ + key: ConfigKey.AnalyticsEnabled, + value: analyticsEnabled, + }) ); - window.electron.ipcRenderer.config({ - action: ConfigAction.Set, - key: ConfigKey.AnalyticsEnabled, - val: analyticsEnabled ? 'true' : 'false', - }); }} > OK @@ -260,40 +258,14 @@ function GlobalContainer() { } function App() { - const config = useAppSelector(selectConfigState); - const dispatch = useAppDispatch(); + const config = useConfigState(); Object.assign(console, logger.functions); - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const listener = (resp: any) => { - const { method, res } = resp; - switch (method) { - case 'config': - dispatch( - setConfig({ - values: res.values, - loading: false, - }) - ); - break; - default: - } - }; - window.electron.ipcRenderer.on('main', listener); - window.electron.ipcRenderer.config({ - action: ConfigAction.Get, - }); - return () => { - window.electron.ipcRenderer.removeListener('main', listener); - }; - }, [dispatch]); - if (config.loading) { - return <>; + return <>Config Loading ...; } - if (config.values && !(`${ConfigKey.AnalyticsEnabled}` in config.values)) { + if (!config.values || !(`${ConfigKey.AnalyticsEnabled}` in config.values)) { return ; } return ( diff --git a/src/renderer/data/Config/configState.ts b/src/renderer/data/Config/configState.ts index d1b1ba93..4c1b003a 100644 --- a/src/renderer/data/Config/configState.ts +++ b/src/renderer/data/Config/configState.ts @@ -1,9 +1,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ConfigMap } from 'types/types'; +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../hooks'; // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types // eslint-disable-next-line import/no-cycle import { RootState } from '../../store'; +const logger = window.electron.log; + export enum ConfigKey { AnalyticsEnabled = 'analytics_enabled', } @@ -39,6 +44,9 @@ export const configSlice = createSlice({ ) => { if (state.values) { state.values[action.payload.key] = action.payload.value; + window.promiseIpc + .send('CONFIG-Set', action.payload.key, action.payload.value) + .catch(logger.error); } }, }, @@ -50,3 +58,27 @@ export const { setConfig, setConfigValue } = configSlice.actions; export const selectConfigState = (state: RootState) => state.config; export default configSlice.reducer; + +export function useConfigState() { + const config = useAppSelector(selectConfigState); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (config.loading) { + window.promiseIpc + .send('CONFIG-GetAll') + .then((ret: ConfigMap) => { + dispatch( + setConfig({ + values: ret, + loading: false, + }) + ); + return `return ${ret}`; + }) + .catch((e: Error) => logger.error(e)); + } + }, [dispatch, config.loading, config.values]); + + return config; +} diff --git a/src/types/types.tsx b/src/types/types.tsx index 1081c8a1..7a31c387 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -75,16 +75,6 @@ export type FetchAnchorIDLRequest = { programID: string; }; -export type WBConfigRequest = { - key: string; - val?: string; - action: string; -}; - -export type WBConfigResponse = { - values: ConfigMap; -}; - export interface ChangeSubscriptionMap { [net: string]: { [programID: string]: {