├── .npmrc
├── .prettierignore
├── assets
├── icon.icns
├── icon.ico
├── icon.png
├── icons
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ ├── 64x64.png
│ ├── 96x96.png
│ └── 1024x1024.png
├── entitlements.mac.plist
├── Anchor.toml
└── assets.d.ts
├── .prettierrc.json
├── .github
├── config.yml
├── ISSUE_TEMPLATE
│ ├── 3-Feature_request.md
│ ├── 2-Question.md
│ └── 1-Bug_report.md
├── stale.yml
└── workflows
│ ├── test.yml
│ └── publish.yml
├── src
├── renderer
│ ├── tsconfig.json
│ ├── vitest.config.ts
│ ├── index.html
│ ├── public
│ │ └── themes
│ │ │ └── vantablack.css
│ ├── common
│ │ ├── prettifyPubkey.ts
│ │ ├── analytics.ts
│ │ └── globals.ts
│ ├── data
│ │ ├── localstorage.ts
│ │ ├── accounts
│ │ │ ├── accountInfo.ts
│ │ │ ├── programChanges.ts
│ │ │ ├── account.ts
│ │ │ └── accountState.ts
│ │ ├── ValidatorNetwork
│ │ │ ├── validatorNetworkState.ts
│ │ │ └── ValidatorNetwork.tsx
│ │ ├── Config
│ │ │ └── configState.ts
│ │ └── SelectedAccountsList
│ │ │ └── selectedAccountsState.ts
│ ├── components
│ │ ├── base
│ │ │ ├── Chip.tsx
│ │ │ ├── IconButton.tsx
│ │ │ └── EditableText.tsx
│ │ ├── tokens
│ │ │ ├── ActiveAccordionHeader.tsx
│ │ │ ├── MintTokenToButton.tsx
│ │ │ ├── CreateNewMintButton.tsx
│ │ │ ├── MetaplexMintMetaDataView.tsx
│ │ │ ├── TokensListView.tsx
│ │ │ ├── MintInfoView.tsx
│ │ │ ├── TransferTokenButton.tsx
│ │ │ └── MetaplexTokenData.tsx
│ │ ├── PinAccountIcon.tsx
│ │ ├── CopyIcon.tsx
│ │ ├── LogView.tsx
│ │ ├── InlinePK.tsx
│ │ ├── ProgramChange.tsx
│ │ ├── WatchAccountButton.tsx
│ │ ├── AirDropSolButton.tsx
│ │ ├── TransferSolButton.tsx
│ │ └── AccountView.tsx
│ ├── hooks.ts
│ ├── index.tsx
│ ├── auto-imports.d.ts
│ ├── nav
│ │ ├── Account.tsx
│ │ ├── ValidatorNetworkInfo.tsx
│ │ ├── Anchor.tsx
│ │ └── TokenPage.tsx
│ ├── windi.config.ts
│ ├── index.css
│ ├── App.scss
│ ├── store.ts
│ └── vite.config.ts
├── types
│ ├── hexdump-nodejs.d.ts
│ └── types.ts
├── main
│ ├── tsconfig.json
│ ├── util.ts
│ ├── validator.ts
│ ├── anchor.ts
│ ├── transactionLogs.ts
│ ├── ipc
│ │ ├── config.ts
│ │ └── accounts.ts
│ ├── const.ts
│ ├── preload.js
│ ├── logger.ts
│ ├── main.ts
│ └── menu.ts
├── __tests__
│ └── App.test.tsx
└── common
│ ├── hooks.ts
│ └── strings.ts
├── .editorconfig
├── .gitattributes
├── electron.Dockerfile
├── .dockerignore
├── .vscode
├── settings.json
├── tasks.json
└── launch.json
├── .eslintignore
├── .gitignore
├── docker
└── solana
│ └── Dockerfile
├── tsconfig.json
├── bin
├── notarize.js
└── setup.sh
├── .eslintrc.json
├── LICENSE
├── Dockerfile
├── README.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules# Ignore artifacts:
2 | build
3 | coverage
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/128x128.png
--------------------------------------------------------------------------------
/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/16x16.png
--------------------------------------------------------------------------------
/assets/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/24x24.png
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/32x32.png
--------------------------------------------------------------------------------
/assets/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/48x48.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/512x512.png
--------------------------------------------------------------------------------
/assets/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/64x64.png
--------------------------------------------------------------------------------
/assets/icons/96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/96x96.png
--------------------------------------------------------------------------------
/assets/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workbenchapp/solana-workbench/HEAD/assets/icons/1024x1024.png
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------
1 | requiredHeaders:
2 | - Prerequisites
3 | - Expected Behavior
4 | - Current Behavior
5 | - Possible Solution
6 | - Your Environment
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3-Feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: You want something added to the Workbench. 🎉
4 | labels: 'enhancement'
5 | ---
6 |
--------------------------------------------------------------------------------
/src/renderer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./*"]
7 | }
8 | },
9 | "include": ["../types"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/hexdump-nodejs.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'hexdump-nodejs' {
2 | function hexdump(
3 | data: Buffer | Uint8Array,
4 | offset?: number,
5 | length?: number
6 | ): string;
7 |
8 | export = hexdump;
9 | }
10 |
--------------------------------------------------------------------------------
/src/renderer/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import viteConfig from './vite.config';
3 |
4 | export default defineConfig({
5 | ...viteConfig,
6 | test: {
7 | environment: 'jsdom',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.exe binary
3 | *.png binary
4 | *.jpg binary
5 | *.jpeg binary
6 | *.ico binary
7 | *.icns binary
8 | *.eot binary
9 | *.otf binary
10 | *.ttf binary
11 | *.woff binary
12 | *.woff2 binary
13 |
--------------------------------------------------------------------------------
/src/main/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["../*"]
7 | },
8 | "outDir": "../../release/dist"
9 | },
10 | "files": ["main.ts"],
11 | "include": ["../types/**/*", "preload.js"]
12 | }
13 |
--------------------------------------------------------------------------------
/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
stuff got mixed up
38 | // with the whole Tailwind-Bootstrap situation. For now, we
39 | // always use when we need an inline monospace,
40 | // and for a block display.
41 | .pre {
42 | font-family: "Space Mono", monospace;
43 | }
44 | pre {
45 | background-color: rgb(24, 29, 37);
46 | font-family: "Space Mono", monospace;
47 | }
48 | code {
49 | border-width: 0px !important;
50 | font-family: "Space Mono", monospace;
51 | }
52 |
53 | .accordion-flush .accordion-item .accordion-button {
54 | padding: 2px 15px;
55 | }
56 |
57 | // Import all of bootstrap, this isn't an exercise in minimisation
58 | // @import "bootstrap/scss/bootstrap";
59 |
--------------------------------------------------------------------------------
/src/renderer/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import throttle from 'lodash/throttle';
3 |
4 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
5 | // eslint-disable-next-line import/no-cycle
6 | import ValidatorReducer from './data/ValidatorNetwork/validatorNetworkState';
7 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
8 | // eslint-disable-next-line import/no-cycle
9 | import SelectedAccountsListReducer from './data/SelectedAccountsList/selectedAccountsState';
10 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
11 | // eslint-disable-next-line import/no-cycle
12 | import ConfigReducer from './data/Config/configState';
13 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
14 | // eslint-disable-next-line import/no-cycle
15 | import AccountReducer from './data/accounts/accountState';
16 |
17 | import { saveState } from './data/localstorage';
18 |
19 | const store = configureStore({
20 | reducer: {
21 | validatornetwork: ValidatorReducer,
22 | selectedaccounts: SelectedAccountsListReducer,
23 | config: ConfigReducer,
24 | account: AccountReducer,
25 | },
26 | });
27 |
28 | // TODO: this is a really bad way to save a subset of redux - as its triggered any time anything changes
29 | // I think middleware is supposed to do it better
30 | store.subscribe(
31 | throttle(() => {
32 | saveState('selectedaccounts', store.getState().selectedaccounts);
33 | saveState('config', store.getState().config);
34 | saveState('account', store.getState().account);
35 | }, 1000)
36 | );
37 |
38 | // Infer the `RootState` and `AppDispatch` types from the store itself
39 | export type RootState = ReturnType;
40 | export type AppDispatch = typeof store.dispatch;
41 |
42 | export default store;
43 |
--------------------------------------------------------------------------------
/src/renderer/components/base/EditableText.tsx:
--------------------------------------------------------------------------------
1 | import { KeyboardEvent, useEffect, useRef, useState } from 'react';
2 | import IconButton from './IconButton';
3 |
4 | const EditableText: React.FC<
5 | {
6 | value: string;
7 | onSave: (value: string) => void;
8 | } & React.InputHTMLAttributes
9 | > = ({ value, onSave, ...rest }) => {
10 | const [editingValue, setEditingValue] = useState(
11 | undefined
12 | );
13 | const [editing, setEditing] = useState(false);
14 | const input = useRef(null);
15 |
16 | useEffect(() => {
17 | input.current?.focus();
18 | }, [editing]);
19 |
20 | const save = () => {
21 | onSave(editingValue || value);
22 | setEditing(false);
23 | };
24 |
25 | const onKeyDown = (ev: KeyboardEvent) => {
26 | switch (ev.key) {
27 | case 'Enter':
28 | save();
29 | break;
30 | case 'Escape':
31 | setEditing(false);
32 | break;
33 | default:
34 | break;
35 | }
36 | };
37 |
38 | if (editing) {
39 | return (
40 |
41 | setEditingValue(ev.currentTarget.value)}
45 | value={editingValue || value}
46 | ref={input}
47 | onKeyDown={onKeyDown}
48 | />
49 |
50 |
51 |
52 |
53 | setEditing(false)} dense>
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | return (
61 |
62 | {value || 'Unset'}
63 | setEditing(true)}>
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default EditableText;
71 |
--------------------------------------------------------------------------------
/src/main/logger.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import winston from 'winston';
3 | import fs from 'fs';
4 | import logfmt from 'logfmt';
5 | import { RESOURCES_PATH, WORKBENCH_DIR_PATH, WORKBENCH_VERSION } from './const';
6 |
7 | const MAX_LOG_FILE_BYTES = 5 * 1028 * 1028;
8 |
9 | // eslint-disable-next-line import/no-mutable-exports
10 | let logger = winston.createLogger({
11 | transports: [new winston.transports.Console()],
12 | });
13 |
14 | const LOG_DIR_PATH = path.join(WORKBENCH_DIR_PATH, 'logs');
15 | const LOG_FILE_PATH = path.join(LOG_DIR_PATH, 'latest.log');
16 | const LOG_KV_PAD = 50;
17 |
18 | if (!fs.existsSync(LOG_DIR_PATH)) {
19 | fs.mkdirSync(LOG_DIR_PATH);
20 | }
21 |
22 | const initLogging = async () => {
23 | // todo: could do better log rotation,
24 | // but this will do for now to avoid infinite growth
25 | try {
26 | const stat = await fs.promises.stat(LOG_FILE_PATH);
27 | if (stat.size > MAX_LOG_FILE_BYTES) {
28 | await fs.promises.rm(LOG_FILE_PATH);
29 | }
30 | // might get exception if file does not exist,
31 | // but it's expected.
32 | //
33 | // eslint-disable-next-line no-empty
34 | } catch (error) {}
35 |
36 | const logfmtFormat = winston.format.printf((info) => {
37 | const { timestamp } = info.metadata;
38 | delete info.metadata.timestamp;
39 | return `${timestamp} ${info.level.toUpperCase()} ${info.message.padEnd(
40 | LOG_KV_PAD,
41 | ' '
42 | )}${typeof info.metadata === 'object' && logfmt.stringify(info.metadata)}`;
43 | });
44 | const loggerConfig: winston.LoggerOptions = {
45 | format: winston.format.combine(
46 | winston.format.timestamp(),
47 | winston.format.metadata(),
48 | logfmtFormat
49 | ),
50 | transports: [
51 | new winston.transports.File({
52 | filename: LOG_FILE_PATH,
53 | handleExceptions: true,
54 | }),
55 | ],
56 | };
57 | if (process.env.NODE_ENV === 'development') {
58 | loggerConfig.transports = [new winston.transports.Console()];
59 | }
60 | logger = winston.createLogger(loggerConfig);
61 | logger.info('Workbench session begin', {
62 | WORKBENCH_VERSION,
63 | RESOURCES_PATH,
64 | });
65 | };
66 |
67 | export { logger, initLogging };
68 |
--------------------------------------------------------------------------------
/src/main/ipc/accounts.ts:
--------------------------------------------------------------------------------
1 | import cfg from 'electron-cfg';
2 | import promiseIpc from 'electron-promise-ipc';
3 | import { IpcMainEvent, IpcRendererEvent } from 'electron';
4 |
5 | import * as web3 from '@solana/web3.js';
6 | import * as bip39 from 'bip39';
7 |
8 | import { NewKeyPairInfo } from '../../types/types';
9 |
10 | import { logger } from '../logger';
11 |
12 | async function createNewKeypair(): Promise {
13 | const mnemonic = bip39.generateMnemonic();
14 | const seed = await bip39.mnemonicToSeed(mnemonic);
15 | const newKeypair = web3.Keypair.fromSeed(seed.slice(0, 32));
16 |
17 | logger.silly(
18 | `main generated new account${newKeypair.publicKey.toString()} ${JSON.stringify(
19 | newKeypair
20 | )}`
21 | );
22 |
23 | const val = {
24 | privatekey: newKeypair.secretKey,
25 | mnemonic,
26 | };
27 | cfg.set(`accounts.${newKeypair.publicKey.toString()}`, val);
28 |
29 | return val;
30 | }
31 |
32 | declare type IpcEvent = IpcRendererEvent & IpcMainEvent;
33 |
34 | // Need to import the file and call a function (from the main process) to get the IPC promise to exist.
35 | export function initAccountPromises() {
36 | // gets written to .\AppData\Roaming\SolanaWorkbench\electron-cfg.json on windows
37 | promiseIpc.on('ACCOUNT-GetAll', (event: IpcEvent | undefined) => {
38 | logger.silly('main: called ACCOUNT-GetAll', event);
39 | const config = cfg.get('accounts');
40 | if (!config) {
41 | return {};
42 | }
43 | return config;
44 | });
45 | // TODO: so the idea is that this == a list of private keys with annotations (like human name...)
46 | // so it could be key: public key, value is a map[string]interface{} with a convention that 'privatekey' contains that in X form...
47 | promiseIpc.on(
48 | 'ACCOUNT-Set',
49 | (key: unknown, val: unknown, event?: IpcEvent | undefined) => {
50 | logger.silly(`main: called ACCOUNT-Set, ${key}, ${val}, ${event}`);
51 | return cfg.set(`accounts.${key}`, val);
52 | }
53 | );
54 | promiseIpc.on(
55 | 'ACCOUNT-CreateNew',
56 | (event: IpcEvent | undefined): Promise => {
57 | logger.silly(`main: called ACCOUNT-CreateNew, ${event}`);
58 | return createNewKeypair();
59 | }
60 | );
61 | }
62 |
63 | export default {};
64 |
--------------------------------------------------------------------------------
/src/renderer/components/LogView.tsx:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 | import { useEffect, useState } from 'react';
3 | import {
4 | logger,
5 | commitmentLevel,
6 | GetValidatorConnection,
7 | } from '../common/globals';
8 | import {
9 | NetStatus,
10 | selectValidatorNetworkState,
11 | } from '../data/ValidatorNetwork/validatorNetworkState';
12 | import { useAppSelector } from '../hooks';
13 |
14 | export interface LogSubscriptionMap {
15 | [net: string]: {
16 | subscriptionID: number;
17 | solConn: sol.Connection;
18 | };
19 | }
20 |
21 | const logSubscriptions: LogSubscriptionMap = {};
22 |
23 | function LogView() {
24 | const [logs, setLogs] = useState([]);
25 | const { net, status } = useAppSelector(selectValidatorNetworkState);
26 |
27 | useEffect(() => {
28 | setLogs([]);
29 |
30 | if (status !== NetStatus.Running) {
31 | return () => {};
32 | }
33 |
34 | const solConn = GetValidatorConnection(net);
35 | const subscriptionID = solConn.onLogs(
36 | 'all',
37 | (logsInfo) => {
38 | setLogs((prevLogs: string[]) => {
39 | const newLogs = [
40 | logsInfo.signature,
41 | logsInfo.err?.toString() || 'Ok',
42 | ...logsInfo.logs.reverse(),
43 | ...prevLogs,
44 | ];
45 |
46 | // utter pseudo-science -- determine max log lines from window size
47 | const MAX_DISPLAYED_LOG_LINES = (3 * window.innerHeight) / 22;
48 | if (newLogs.length > MAX_DISPLAYED_LOG_LINES) {
49 | return newLogs.slice(0, MAX_DISPLAYED_LOG_LINES);
50 | }
51 | return newLogs;
52 | });
53 | },
54 | commitmentLevel
55 | );
56 | logSubscriptions[net] = { subscriptionID, solConn };
57 |
58 | return () => {
59 | const sub = logSubscriptions[net];
60 | if (sub?.solConn) {
61 | sub.solConn
62 | .removeOnLogsListener(sub.subscriptionID)
63 | // eslint-disable-next-line promise/always-return
64 | .then(() => {
65 | delete logSubscriptions[net];
66 | })
67 | .catch(logger.info);
68 | }
69 | };
70 | }, [net, status]);
71 |
72 | return (
73 |
74 | {logs.length > 0 ? logs.join('\n') : ''}
75 |
76 | );
77 | }
78 |
79 | export default LogView;
80 |
--------------------------------------------------------------------------------
/src/renderer/data/ValidatorNetwork/validatorNetworkState.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 |
3 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
4 | // eslint-disable-next-line import/no-cycle
5 | import type { RootState } from '../../store';
6 |
7 | // from https://react-redux.js.org/tutorials/typescript-quick-start
8 |
9 | export enum Net {
10 | Localhost = 'localhost',
11 | Dev = 'devnet',
12 | Test = 'testnet',
13 | MainnetBeta = 'mainnet-beta',
14 | }
15 | export enum NetStatus {
16 | Unknown = 'unknown',
17 | Running = 'running',
18 | Unavailable = 'unavailable',
19 | Starting = 'starting',
20 | }
21 |
22 | export const netToURL = (net: Net): string => {
23 | switch (net) {
24 | case Net.Localhost:
25 | return 'http://127.0.0.1:8899';
26 | case Net.Dev:
27 | return 'https://api.devnet.solana.com';
28 | case Net.Test:
29 | return 'https://api.testnet.solana.com';
30 | case Net.MainnetBeta:
31 | return 'https://api.mainnet-beta.solana.com';
32 | default:
33 | }
34 | return '';
35 | };
36 |
37 | export interface ValidatorState {
38 | net: Net;
39 | status: NetStatus;
40 | }
41 |
42 | // TODO: Using a global to let the electron solana-wallet-backend what network we're on (its not a react component)
43 | // Sven wasted too much time on this, and its only temporary until that txn sign code moves to the react backend.
44 | export const globalNetworkSet = { net: Net.Localhost };
45 |
46 | // Define the initial state using that type
47 | const initialState: ValidatorState = {
48 | net: Net.Localhost,
49 | status: NetStatus.Unknown,
50 | };
51 |
52 | export const validatorNetworkSlice = createSlice({
53 | name: 'validatornetwork',
54 | // `createSlice` will infer the state type from the `initialState` argument
55 | initialState,
56 | reducers: {
57 | setNet: (state, action: PayloadAction) => {
58 | state.net = action.payload;
59 | globalNetworkSet.net = state.net;
60 | },
61 | setState: (state, action: PayloadAction) => {
62 | state.status = action.payload;
63 | },
64 | },
65 | });
66 |
67 | export const accountsActions = validatorNetworkSlice.actions;
68 | export const { setNet, setState } = validatorNetworkSlice.actions;
69 |
70 | // Other code such as selectors can use the imported `RootState` type
71 | export const selectValidatorNetworkState = (state: RootState) =>
72 | state.validatornetwork;
73 |
74 | export default validatorNetworkSlice.reducer;
75 |
--------------------------------------------------------------------------------
/src/renderer/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import { join } from 'path';
3 | import AutoImport from 'unplugin-auto-import/vite';
4 | import IconsResolver from 'unplugin-icons/resolver';
5 | import Icons from 'unplugin-icons/vite';
6 | import { defineConfig } from 'vite';
7 | import ViteFonts from 'vite-plugin-fonts';
8 | import InlineCSSModules from 'vite-plugin-inline-css-modules';
9 | import WindiCSS from 'vite-plugin-windicss';
10 | import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
11 | import checker from 'vite-plugin-checker';
12 | import EnvironmentPlugin from 'vite-plugin-environment';
13 |
14 | const PACKAGE_ROOT = __dirname;
15 | /**
16 | * @type {import('vite').UserConfig}
17 | * @see https://vitejs.dev/config/
18 | */
19 | export default defineConfig({
20 | mode: process.env.MODE,
21 | root: PACKAGE_ROOT,
22 | resolve: {
23 | alias: {
24 | '@/': `${join(PACKAGE_ROOT, './')}/`,
25 | },
26 | },
27 | optimizeDeps: {
28 | esbuildOptions: {
29 | // Node.js global to browser globalThis
30 | define: {
31 | global: 'globalThis',
32 | },
33 | // Enable esbuild polyfill plugins
34 | plugins: [
35 | NodeGlobalsPolyfillPlugin({
36 | buffer: true,
37 | }),
38 | ],
39 | target: 'es2021',
40 | },
41 | },
42 | plugins: [
43 | EnvironmentPlugin({
44 | BROWSER: 'true', // Anchor <=0.24.2
45 | ANCHOR_BROWSER: 'true', // Anchor >0.24.2
46 | }),
47 | ViteFonts({
48 | google: {
49 | families: ['Roboto:wght@400;500;700', 'Space Mono:wght@400'],
50 | },
51 | }),
52 | InlineCSSModules(),
53 | Icons({
54 | compiler: 'jsx',
55 | jsx: 'react',
56 | }),
57 | AutoImport({
58 | resolvers: [
59 | IconsResolver({
60 | prefix: 'Icon',
61 | extension: 'jsx',
62 | }),
63 | ],
64 | }),
65 | WindiCSS({
66 | scan: {
67 | dirs: ['.'], // all files in the cwd
68 | fileExtensions: ['tsx', 'js', 'ts'], // also enabled scanning for js/ts
69 | },
70 | }),
71 | react(),
72 | WindiCSS(),
73 | checker({
74 | typescript: {
75 | root: PACKAGE_ROOT,
76 | tsconfigPath: `./tsconfig.json`,
77 | },
78 | }),
79 | ],
80 | base: '',
81 | server: {
82 | fs: {
83 | strict: true,
84 | },
85 | host: true,
86 | port: process.env.PORT ? +process.env.PORT : 1212,
87 | strictPort: true,
88 | },
89 | build: {
90 | sourcemap: true,
91 | outDir: '../../release/dist/renderer',
92 | assetsDir: '.',
93 | emptyOutDir: true,
94 | brotliSize: false,
95 | target: 'es2021',
96 | },
97 | });
98 |
--------------------------------------------------------------------------------
/src/renderer/data/Config/configState.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { useEffect } from 'react';
3 | import { ConfigMap } from '../../../types/types';
4 | import { logger } from '../../common/globals';
5 | import { useAppDispatch, useAppSelector } from '../../hooks';
6 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
7 | // eslint-disable-next-line import/no-cycle
8 | import { RootState } from '../../store';
9 |
10 | export enum ConfigKey {
11 | AnalyticsEnabled = 'analytics_enabled',
12 | }
13 |
14 | export interface ConfigValues {
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | [key: string]: any;
17 | }
18 | export interface ConfigState {
19 | loading: boolean;
20 | values: ConfigValues | undefined;
21 | }
22 |
23 | const initialState: ConfigState = {
24 | values: undefined,
25 | loading: true,
26 | };
27 |
28 | export const configSlice = createSlice({
29 | name: 'config',
30 | // `createSlice` will infer the state type from the `initialState` argument
31 | initialState,
32 | reducers: {
33 | setConfig: (state, action: PayloadAction) => {
34 | state.values = action.payload.values;
35 | state.loading = action.payload.loading;
36 | },
37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
38 | setConfigValue: (
39 | state,
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | action: PayloadAction<{ key: string; value: any }>
42 | ) => {
43 | if (state.values) {
44 | state.values[action.payload.key] = action.payload.value;
45 | window.promiseIpc
46 | .send('CONFIG-Set', action.payload.key, action.payload.value)
47 | .catch(logger.error);
48 | }
49 | },
50 | },
51 | });
52 |
53 | export const configActions = configSlice.actions;
54 | export const { setConfig, setConfigValue } = configSlice.actions;
55 |
56 | export const selectConfigState = (state: RootState) => state.config;
57 |
58 | export default configSlice.reducer;
59 |
60 | export function useConfigState() {
61 | const config = useAppSelector(selectConfigState);
62 | const dispatch = useAppDispatch();
63 |
64 | useEffect(() => {
65 | if (config.loading) {
66 | window.promiseIpc
67 | .send('CONFIG-GetAll')
68 | .then((ret: ConfigMap) => {
69 | dispatch(
70 | setConfig({
71 | values: ret,
72 | loading: false,
73 | })
74 | );
75 | return `return ${ret}`;
76 | })
77 | .catch((e: Error) => logger.error(e));
78 | }
79 | }, [dispatch, config.loading, config.values]);
80 |
81 | return config;
82 | }
83 |
--------------------------------------------------------------------------------
/src/renderer/components/InlinePK.tsx:
--------------------------------------------------------------------------------
1 | import { faExplosion } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { OverlayTrigger, Tooltip } from 'react-bootstrap';
4 | import React from 'react';
5 |
6 | import analytics from '../common/analytics';
7 | import prettifyPubkey from '../common/prettifyPubkey';
8 | import { useAppSelector } from '../hooks';
9 | import {
10 | Net,
11 | netToURL,
12 | selectValidatorNetworkState,
13 | } from '../data/ValidatorNetwork/validatorNetworkState';
14 |
15 | import CopyIcon from './CopyIcon';
16 |
17 | const explorerURL = (net: Net, address: string) => {
18 | switch (net) {
19 | case Net.Test:
20 | case Net.Dev:
21 | return `https://explorer.solana.com/address/${address}?cluster=${net}`;
22 | case Net.Localhost:
23 | return `https://explorer.solana.com/address/${address}/ \
24 | ?cluster=custom&customUrl=${encodeURIComponent(netToURL(net))}`;
25 | default:
26 | return `https://explorer.solana.com/address/${address}`;
27 | }
28 | };
29 |
30 | const renderCopyTooltip = (id: string, text: string) =>
31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any,func-names
32 | function (ttProps: any) {
33 | return (
34 | // eslint-disable-next-line react/jsx-props-no-spreading
35 |
36 | {text}
37 |
38 | );
39 | };
40 |
41 | const InlinePK: React.FC<{
42 | pk: string | undefined;
43 | className?: string;
44 | formatLength?: number;
45 | }> = ({ pk, className, formatLength }) => {
46 | const { net } = useAppSelector(selectValidatorNetworkState);
47 |
48 | if (!pk) {
49 | return (
50 |
51 | No onchain account
52 |
53 | );
54 | }
55 |
56 | return (
57 |
58 | {prettifyPubkey(pk, formatLength)}
59 |
60 |
61 | {pk !== '' ? (
62 |
67 | analytics('clickExplorerLink', { net })}
69 | href={explorerURL(net, pk)}
70 | target="_blank"
71 | className="sol-link"
72 | rel="noreferrer"
73 | >
74 |
78 |
79 |
80 | ) : (
81 | 'No onchain account'
82 | )}
83 |
84 |
85 | );
86 | };
87 |
88 | InlinePK.defaultProps = {
89 | className: '',
90 | formatLength: 32,
91 | };
92 |
93 | export default InlinePK;
94 |
--------------------------------------------------------------------------------
/src/renderer/data/SelectedAccountsList/selectedAccountsState.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 |
3 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
4 | // eslint-disable-next-line import/no-cycle
5 | import { RootState } from '../../store';
6 | import { loadState } from '../localstorage';
7 |
8 | export interface SelectedAccountsList {
9 | pinnedAccounts: string[]; // list of pubKeys (TODO: should really add net...)
10 | selectedAccount: string;
11 | hoveredAccount: string;
12 | editedAccount: string;
13 | rootKey: string;
14 | }
15 |
16 | // Define the initial state using that type
17 | let initialState: SelectedAccountsList = {
18 | pinnedAccounts: [],
19 | selectedAccount: '',
20 | hoveredAccount: '',
21 | editedAccount: '',
22 | rootKey: '',
23 | };
24 | const loaded = loadState('selectedaccounts');
25 | if (loaded) {
26 | // work out the schema change (30mar2022)
27 | if (loaded.listedAccounts) {
28 | loaded.pinnedAccounts = loaded.listedAccounts;
29 | delete loaded.listedAccounts;
30 | }
31 | if (loaded.pinnedAccounts) {
32 | for (let i = 0; i < initialState.pinnedAccounts.length; i += 1) {
33 | let val = loaded.pinnedAccounts[i];
34 | if (val instanceof Object) {
35 | val = val.pubKey;
36 | if (val) {
37 | loaded.pinnedAccounts[i] = val;
38 | }
39 | }
40 | }
41 | // ensure any listedAccount is only in the list once
42 | loaded.pinnedAccounts = Array.from(new Set(loaded.pinnedAccounts));
43 | }
44 | initialState = loaded;
45 | }
46 |
47 | export const selectedAccountsListSlice = createSlice({
48 | name: 'selectedaccounts',
49 | // `createSlice` will infer the state type from the `initialState` argument
50 | initialState,
51 | reducers: {
52 | setAccounts: (state, action: PayloadAction) => {
53 | state.pinnedAccounts = action.payload;
54 | },
55 | setRootKey: (state, action: PayloadAction) => {
56 | state.rootKey = action.payload;
57 | },
58 | shift: (state) => {
59 | state.pinnedAccounts.shift();
60 | },
61 | unshift: (state, action: PayloadAction) => {
62 | state.pinnedAccounts.unshift(action.payload);
63 | },
64 | rm: (state, action: PayloadAction) => {
65 | state.pinnedAccounts = state.pinnedAccounts.filter(
66 | (a) => a !== action.payload
67 | );
68 | },
69 | setEdited: (state, action: PayloadAction) => {
70 | state.editedAccount = action.payload;
71 | },
72 | setHovered: (state, action: PayloadAction) => {
73 | state.hoveredAccount = action.payload;
74 | },
75 | setSelected: (state, action: PayloadAction) => {
76 | state.selectedAccount = action.payload;
77 | },
78 | },
79 | });
80 |
81 | export const accountsActions = selectedAccountsListSlice.actions;
82 | export const {
83 | setAccounts,
84 | setRootKey,
85 | shift,
86 | unshift,
87 | rm,
88 | setEdited,
89 | setHovered,
90 | setSelected,
91 | } = selectedAccountsListSlice.actions;
92 |
93 | // Other code such as selectors can use the imported `RootState` type
94 | export const selectAccountsListState = (state: RootState) =>
95 | state.selectedaccounts;
96 |
97 | export default selectedAccountsListSlice.reducer;
98 |
--------------------------------------------------------------------------------
/src/renderer/components/ProgramChange.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import { setSelected } from '../data/SelectedAccountsList/selectedAccountsState';
3 | import { AccountInfo } from '../data/accounts/accountInfo';
4 | import { useAccountMeta } from '../data/accounts/accountState';
5 | import {
6 | getAccount,
7 | truncateLamportAmount,
8 | truncateSolAmount,
9 | } from '../data/accounts/getAccount';
10 | import {
11 | Net,
12 | NetStatus,
13 | selectValidatorNetworkState,
14 | } from '../data/ValidatorNetwork/validatorNetworkState';
15 | import { useAppDispatch, useAppSelector, useInterval } from '../hooks';
16 | import InlinePK from './InlinePK';
17 | import PinAccountIcon from './PinAccountIcon';
18 |
19 | export function ProgramChange(props: {
20 | net: Net;
21 | pubKey: string;
22 | pinned: boolean;
23 | pinAccount: (pk: string, b: boolean) => void;
24 | selected: boolean;
25 | }) {
26 | const dispatch = useAppDispatch();
27 | const { pubKey, selected, net, pinned, pinAccount } = props;
28 | const [change, setChangeInfo] = useState(undefined);
29 | const { status } = useAppSelector(selectValidatorNetworkState);
30 | const accountMeta = useAccountMeta(pubKey);
31 |
32 | const updateAccount = useCallback(() => {
33 | if (status !== NetStatus.Running) {
34 | return;
35 | }
36 | if (!pubKey) {
37 | // setChangeInfo(undefined);
38 | return;
39 | }
40 | const update = getAccount(net, pubKey);
41 | if (!update) {
42 | return;
43 | }
44 | setChangeInfo(update);
45 | }, [net, status, pubKey]);
46 | useEffect(updateAccount, [updateAccount]);
47 | useInterval(updateAccount, 666);
48 |
49 | if (!change) {
50 | return null;
51 | }
52 |
53 | const showCount = change?.count || 0;
54 | const showSOL = change
55 | ? truncateLamportAmount(change)
56 | : `no account on ${net}`;
57 | const showChange = change ? truncateSolAmount(change.maxDelta) : 0;
58 |
59 | return (
60 | dispatch(setSelected(pubKey))}
62 | className={`transition cursor-pointer duration-50 bg-opacity-20 hover:bg-opacity-30 hover:bg-primary-light ${
63 | selected ? 'bg-primary-light' : ''
64 | }`}
65 | >
66 |
67 |
68 |
73 |
74 |
75 |
76 |
81 | {accountMeta?.privatekey ? : ''}
82 |
83 |
84 |
85 | {showChange}
86 |
87 |
88 |
89 |
90 | {showSOL}
91 |
92 |
93 |
94 |
95 | {showCount}
96 |
97 |
98 |
99 | );
100 | }
101 |
102 | export default ProgramChange;
103 |
--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 |
3 | export enum Net {
4 | Localhost = 'localhost',
5 | Dev = 'devnet',
6 | Test = 'testnet',
7 | MainnetBeta = 'mainnet-beta',
8 | }
9 | export enum NetStatus {
10 | Unknown = 'unknown',
11 | Running = 'running',
12 | Unavailable = 'unavailable',
13 | Starting = 'starting',
14 | }
15 |
16 | export const BASE58_PUBKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
17 | export const MAX_PROGRAM_CHANGES_DISPLAYED = 20;
18 |
19 | export enum ProgramID {
20 | SystemProgram = '11111111111111111111111111111111',
21 | SerumDEXV3 = '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin',
22 | TokenProgram = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
23 | }
24 |
25 | export enum ConfigAction {
26 | Get = 'get',
27 | Set = 'set',
28 | }
29 |
30 | export type ValidatorLogsRequest = {
31 | filter: string;
32 | net: Net;
33 | };
34 |
35 | export type GetAccountRequest = {
36 | net: Net;
37 | pubKey: string;
38 | };
39 |
40 | export type AccountsRequest = {
41 | net: Net;
42 | };
43 |
44 | export type UpdateAccountRequest = {
45 | net: Net;
46 | pubKey: string;
47 | humanName: string;
48 | };
49 |
50 | export type ImportAccountRequest = {
51 | net: Net;
52 | pubKey: string;
53 | };
54 |
55 | export type ValidatorNetworkInfoRequest = {
56 | net: Net;
57 | };
58 |
59 | export type ImportAccountResponse = {
60 | net: Net;
61 | };
62 |
63 | export type SubscribeProgramChangesRequest = {
64 | net: Net;
65 | programID: string;
66 | };
67 |
68 | export type UnsubscribeProgramChangesRequest = {
69 | net: Net;
70 | subscriptionID: number;
71 | programID: string;
72 | };
73 |
74 | export type FetchAnchorIDLRequest = {
75 | programID: string;
76 | };
77 |
78 | export interface ChangeSubscriptionMap {
79 | [net: string]: {
80 | [programID: string]: {
81 | subscriptionID: number;
82 | solConn: sol.Connection;
83 | };
84 | };
85 | }
86 |
87 | export interface LogSubscriptionMap {
88 | [net: string]: {
89 | subscriptionID: number;
90 | solConn: sol.Connection;
91 | };
92 | }
93 |
94 | export interface AccountMap {
95 | [pubKey: string]: boolean;
96 | }
97 |
98 | export interface ConfigMap {
99 | [key: string]: string | undefined;
100 | }
101 |
102 | export type VCount = {
103 | version: string;
104 | count: number;
105 | };
106 |
107 | export type ValidatorNetworkInfoResponse = {
108 | version: string;
109 | nodes: sol.ContactInfo[];
110 | versionCount: VCount[];
111 | };
112 |
113 | // https://docs.solana.com/developing/clients/jsonrpc-api#getclusternodes
114 | export type NodeInfo = {
115 | pubkey: string; // - Node public key, as base-58 encoded string
116 | gossip: string | null; // - Gossip network address for the node
117 | tpu?: string | null; // - TPU network address for the node
118 | rpc?: string | null; // - JSON RPC network address for the node, or null if the JSON RPC service is not enabled
119 | version?: string | null; // - The software version of the node, or null if the version information is not available
120 | featureSet?: number | null; // - The unique identifier of the node's feature set
121 | shredVersion?: number | null; // - The shred version the node has been configured to use
122 | };
123 |
124 | export interface NewKeyPairInfo {
125 | privatekey: Uint8Array;
126 | mnemonic: string;
127 | }
128 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/MintTokenToButton.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from 'react-toastify';
2 | import * as sol from '@solana/web3.js';
3 |
4 | import * as walletAdapter from '@solana/wallet-adapter-react';
5 | import { Button } from 'react-bootstrap';
6 | import { useQueryClient } from 'react-query';
7 | import * as walletWeb3 from '../../wallet-adapter/web3';
8 |
9 | import { logger } from '../../common/globals';
10 | import { useAppSelector } from '../../hooks';
11 | import {
12 | NetStatus,
13 | selectValidatorNetworkState,
14 | } from '../../data/ValidatorNetwork/validatorNetworkState';
15 | import { ensureAtaFor } from './CreateNewMintButton';
16 |
17 | async function mintToken(
18 | connection: sol.Connection,
19 | payer: walletAdapter.WalletContextState,
20 | mintKey: sol.PublicKey,
21 | mintTo: sol.PublicKey
22 | ) {
23 | if (!mintTo) {
24 | logger.info('no mintTo', mintTo);
25 | return;
26 | }
27 | if (!mintKey) {
28 | logger.info('no mintKey', mintKey);
29 | return;
30 | }
31 | if (!payer.publicKey) {
32 | logger.info('no payer.publicKey', payer.publicKey);
33 | return;
34 | }
35 | const tokenAta = await ensureAtaFor(connection, payer, mintKey, mintTo);
36 | if (!tokenAta) {
37 | logger.info('no tokenAta', tokenAta);
38 | return;
39 | }
40 |
41 | // Minting 1 new token to the "fromTokenAccount" account we just returned/created.
42 | const signature = await walletWeb3.mintTo(
43 | connection,
44 | payer, // Payer of the transaction fees
45 | mintKey, // Mint for the account
46 | tokenAta, // Address of the account to mint to
47 | payer.publicKey, // Minting authority
48 | 1 // Amount to mint
49 | );
50 | logger.info('SIGNATURE', signature);
51 | }
52 |
53 | function MintTokenToButton(props: {
54 | connection: sol.Connection;
55 | fromKey: walletAdapter.WalletContextState;
56 | mintKey: sol.PublicKey | undefined;
57 | mintTo: sol.PublicKey | undefined;
58 | disabled: boolean;
59 | andThen: () => void;
60 | }) {
61 | const { connection, fromKey, mintKey, mintTo, andThen, disabled } = props;
62 | const { status } = useAppSelector(selectValidatorNetworkState);
63 | const queryClient = useQueryClient();
64 |
65 | return (
66 |
99 | );
100 | }
101 |
102 | export default MintTokenToButton;
103 |
--------------------------------------------------------------------------------
/src/renderer/data/accounts/programChanges.ts:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 | import { GetValidatorConnection, logger } from '../../common/globals';
3 | import { Net } from '../ValidatorNetwork/validatorNetworkState';
4 | import { AccountInfo } from './accountInfo';
5 | import { peekAccount, updateCache } from './getAccount';
6 |
7 | export interface ProgramChangesState {
8 | changes: AccountInfo[];
9 | paused: boolean;
10 | }
11 |
12 | export interface ChangeLookupMap {
13 | [pubKey: string]: AccountInfo;
14 | }
15 | export interface ChangeSubscriptionMap {
16 | [net: string]: {
17 | [programID: string]: {
18 | subscriptionID: number;
19 | solConn: sol.Connection;
20 | };
21 | };
22 | }
23 |
24 | const changeSubscriptions: ChangeSubscriptionMap = {};
25 | export const subscribeProgramChanges = async (
26 | net: Net,
27 | programID: string,
28 | setValidatorSlot: (slot: number) => void
29 | ) => {
30 | let programIDPubkey: sol.PublicKey;
31 | if (programID === sol.SystemProgram.programId.toString()) {
32 | programIDPubkey = sol.SystemProgram.programId;
33 | } else {
34 | programIDPubkey = new sol.PublicKey(programID);
35 | }
36 |
37 | if (
38 | !(net in changeSubscriptions) ||
39 | !(programID in changeSubscriptions[net])
40 | ) {
41 | logger.silly('subscribeProgramChanges', programID);
42 |
43 | const solConn = GetValidatorConnection(net);
44 | const subscriptionID = solConn.onProgramAccountChange(
45 | programIDPubkey,
46 | (info: sol.KeyedAccountInfo, ctx: sol.Context) => {
47 | if (setValidatorSlot) {
48 | setValidatorSlot(ctx.slot);
49 | }
50 | const pubKey = info.accountId.toString();
51 | // logger.silly('programChange', pubKey);
52 | const solAmount = info.accountInfo.lamports / sol.LAMPORTS_PER_SOL;
53 | let [count, maxDelta, solDelta, prevSolAmount] = [1, 0, 0, 0];
54 |
55 | const account = peekAccount(net, pubKey);
56 | if (account) {
57 | ({ count, maxDelta } = account);
58 | if (account.accountInfo) {
59 | prevSolAmount = account.accountInfo.lamports / sol.LAMPORTS_PER_SOL;
60 | solDelta = solAmount - prevSolAmount;
61 | if (Math.abs(solDelta) > Math.abs(maxDelta)) {
62 | maxDelta = solDelta;
63 | }
64 | }
65 |
66 | count += 1;
67 | } else {
68 | // logger.silly('new pubKey in programChange', pubKey);
69 | }
70 |
71 | const programAccountChange: AccountInfo = {
72 | net,
73 | pubKey,
74 | accountInfo: info.accountInfo,
75 | accountId: info.accountId,
76 |
77 | count,
78 | solDelta,
79 | maxDelta,
80 | programID,
81 | };
82 |
83 | updateCache(programAccountChange);
84 | }
85 | );
86 | changeSubscriptions[net] = {
87 | [programID]: {
88 | subscriptionID,
89 | solConn,
90 | },
91 | };
92 | }
93 | };
94 |
95 | export const unsubscribeProgramChanges = async (
96 | net: Net,
97 | programID: string
98 | ) => {
99 | const sub = changeSubscriptions[net][programID];
100 | if (!sub) return;
101 | logger.silly('unsubscribeProgramChanges', programID);
102 |
103 | await sub.solConn.removeProgramAccountChangeListener(sub.subscriptionID);
104 | delete changeSubscriptions[net][programID];
105 | };
106 |
--------------------------------------------------------------------------------
/src/renderer/nav/ValidatorNetworkInfo.tsx:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 | import { useEffect, useState } from 'react';
3 | import { Col, Row } from 'react-bootstrap';
4 | import Container from 'react-bootstrap/Container';
5 | import { VictoryPie } from 'victory';
6 | import { GetValidatorConnection, logger } from '../common/globals';
7 | import {
8 | Net,
9 | selectValidatorNetworkState,
10 | } from '../data/ValidatorNetwork/validatorNetworkState';
11 | import { useAppSelector } from '../hooks';
12 |
13 | interface VersionCount {
14 | [key: string]: number;
15 | }
16 | export type VCount = {
17 | version: string;
18 | count: number;
19 | };
20 | export type ValidatorNetworkInfoResponse = {
21 | version: string;
22 | nodes: sol.ContactInfo[];
23 | versionCount: VCount[];
24 | };
25 | // https://docs.solana.com/developing/clients/jsonrpc-api#getclusternodes
26 | const fetchValidatorNetworkInfo = async (net: Net) => {
27 | const solConn = GetValidatorConnection(net);
28 | const contactInfo = await solConn.getClusterNodes();
29 | // TODO: on success / failure update the ValidatorNetworkState..
30 | const nodeVersion = await solConn.getVersion();
31 |
32 | const frequencyCount: VersionCount = {};
33 |
34 | contactInfo.map((info: sol.ContactInfo) => {
35 | let version = 'none';
36 | if (info.version) {
37 | version = info.version;
38 | }
39 |
40 | if (frequencyCount[version]) {
41 | frequencyCount[version] += 1;
42 | } else {
43 | frequencyCount[version] = 1;
44 | }
45 | return undefined;
46 | });
47 | const versions: VCount[] = [];
48 | Object.entries(frequencyCount).forEach(([version, count]) => {
49 | versions.push({
50 | version,
51 | count,
52 | });
53 | });
54 |
55 | const response: ValidatorNetworkInfoResponse = {
56 | nodes: contactInfo,
57 | version: nodeVersion['solana-core'],
58 | versionCount: versions,
59 | };
60 |
61 | return response;
62 | };
63 |
64 | function ValidatorNetworkInfo() {
65 | const validator = useAppSelector(selectValidatorNetworkState);
66 | const { net } = validator;
67 |
68 | const [data, setData] = useState({
69 | version: 'unknown',
70 | nodes: [],
71 | versionCount: [],
72 | });
73 | useEffect(() => {
74 | // TODO: set a spinner while waiting for response
75 | fetchValidatorNetworkInfo(net)
76 | .then((d) => setData(d))
77 | .catch(logger.info);
78 | }, [validator, net]);
79 |
80 | // TODO: maybe show te version spread as a histogram and feature info ala
81 | // solana --url mainnet-beta feature status
82 | return (
83 |
84 |
85 |
86 | Current Network:
87 | {net}
88 |
89 |
90 | Current Version:
91 | {data.version}
92 |
93 |
94 |
95 | datum.version}
103 | x={(d) => (d as VCount).version}
104 | y={(d) => (d as VCount).count}
105 | />
106 |
107 |
108 | );
109 | }
110 |
111 | export default ValidatorNetworkInfo;
112 |
--------------------------------------------------------------------------------
/src/renderer/nav/Anchor.tsx:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-explicit-any: off */
2 |
3 | import { useEffect, useRef, useState } from 'react';
4 | import { FormControl, InputGroup } from 'react-bootstrap';
5 |
6 | function Anchor() {
7 | const programIDRef = useRef({} as HTMLInputElement);
8 | const [idl, setIDL] = useState({});
9 |
10 | useEffect(() => {
11 | const listener = (resp: any) => {
12 | const { method, res } = resp;
13 | switch (method) {
14 | case 'fetch-anchor-idl':
15 | setIDL(res);
16 | break;
17 | default:
18 | }
19 | };
20 | window.electron.ipcRenderer.on('main', listener);
21 | return () => {
22 | window.electron.ipcRenderer.removeListener('main', listener);
23 | };
24 | }, []);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | Program ID
32 | {
37 | window.electron.ipcRenderer.fetchAnchorIDL({
38 | programID: programIDRef.current.value,
39 | });
40 | }}
41 | />
42 |
43 |
44 |
45 | {idl?.error ? (
46 |
47 | {idl.error?.message}
48 |
49 | ) : (
50 | ''
51 | )}
52 |
53 | {idl.instructions ? (
54 | idl.instructions.map((instruction: any) => {
55 | return (
56 |
57 |
58 | {instruction.name}
59 |
60 |
61 |
62 |
63 | - Args
64 | {instruction.args.map((arg: any) => {
65 | return (
66 | -
67 | {arg.name}
68 |
69 | {arg.type.toString()}
70 |
71 |
72 | );
73 | })}
74 |
75 |
76 |
77 |
78 | - Accounts
79 | {instruction.accounts.map((account: any) => {
80 | return (
81 | - {account.name}
82 | );
83 | })}
84 |
85 |
86 |
87 |
88 | );
89 | })
90 | ) : (
91 |
92 | e.g.: GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv
93 |
94 | )}
95 |
96 |
97 | );
98 | }
99 |
100 | export default Anchor;
101 |
--------------------------------------------------------------------------------
/src/renderer/data/ValidatorNetwork/ValidatorNetwork.tsx:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 | import { useEffect } from 'react';
3 | import Dropdown from 'react-bootstrap/Dropdown';
4 | import DropdownButton from 'react-bootstrap/DropdownButton';
5 | import { NavLink } from 'react-router-dom';
6 | import { GetValidatorConnection, logger } from '../../common/globals';
7 | import { useAppDispatch, useAppSelector, useInterval } from '../../hooks';
8 | import {
9 | Net,
10 | NetStatus,
11 | selectValidatorNetworkState,
12 | setNet,
13 | setState,
14 | } from './validatorNetworkState';
15 |
16 | const validatorState = async (net: Net): Promise => {
17 | let solConn: sol.Connection;
18 |
19 | // Connect to cluster
20 | try {
21 | solConn = GetValidatorConnection(net);
22 | await solConn.getEpochInfo();
23 | } catch (error) {
24 | return NetStatus.Unavailable;
25 | }
26 | return NetStatus.Running;
27 | };
28 |
29 | function ValidatorNetwork() {
30 | const validator = useAppSelector(selectValidatorNetworkState);
31 | const { net } = validator;
32 | const dispatch = useAppDispatch();
33 |
34 | useEffect(() => {
35 | validatorState(net)
36 | .then((state) => {
37 | return dispatch(setState(state));
38 | })
39 | .catch(logger.info);
40 | }, [dispatch, net, validator]);
41 |
42 | const effect = () => {};
43 | useEffect(effect, []);
44 |
45 | useInterval(() => {
46 | validatorState(net)
47 | .then((state) => {
48 | return dispatch(setState(state));
49 | })
50 | .catch((err) => {
51 | logger.debug(err);
52 | });
53 | }, 11111);
54 |
55 | const netDropdownSelect = (eventKey: string | null) => {
56 | // TODO: analytics('selectNet', { prevNet: net, newNet: eventKey });
57 | if (eventKey) {
58 | dispatch(setState(NetStatus.Unknown));
59 | dispatch(setNet(eventKey as Net));
60 | }
61 | };
62 |
63 | let statusText = validator.status as string;
64 | let statusClass = 'text-red-500';
65 | if (validator.status === NetStatus.Running) {
66 | statusText = 'Available';
67 | statusClass = 'text-green-500';
68 | }
69 | const statusDisplay = (
70 | <>
71 |
72 | {statusText}
73 | >
74 | );
75 |
76 | const netDropdownTitle = (
77 | <>
78 | {net}
79 | {statusDisplay}
80 | >
81 | );
82 |
83 | function GetLocalnetManageText() {
84 | if (
85 | validator.net === Net.Localhost &&
86 | validator.status !== NetStatus.Running
87 | ) {
88 | // TODO: consider using icons and having STOP, START, MANAGE...
89 | return Manage {Net.Localhost} ;
90 | }
91 | return <>{Net.Localhost}>;
92 | }
93 |
94 | return (
95 |
101 |
102 |
103 |
104 |
105 | {Net.Dev}
106 |
107 |
108 | {Net.Test}
109 |
110 |
111 | {Net.MainnetBeta}
112 |
113 |
114 | );
115 | }
116 |
117 | export default ValidatorNetwork;
118 |
--------------------------------------------------------------------------------
/src/renderer/components/WatchAccountButton.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { PublicKey } from '@solana/web3.js';
3 | import { Col, Row } from 'react-bootstrap';
4 | import Button from 'react-bootstrap/Button';
5 | import Form from 'react-bootstrap/Form';
6 | import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
7 | import Popover from 'react-bootstrap/Popover';
8 | import { logger } from '../common/globals';
9 | import { setSelected } from '../data/SelectedAccountsList/selectedAccountsState';
10 | import { useAppDispatch } from '../hooks';
11 |
12 | function WatchAcountPopover(props: {
13 | onWatch: (pk: string, b: boolean) => void;
14 | }) {
15 | const { onWatch } = props;
16 |
17 | const pubKeyVal = '';
18 |
19 | const [toKey, setToKey] = useState(pubKeyVal);
20 | const [validationError, setValidationErr] = useState();
21 |
22 | useEffect(() => {
23 | if (pubKeyVal) {
24 | setToKey(pubKeyVal);
25 | }
26 | }, [pubKeyVal]);
27 |
28 | useEffect(() => {
29 | if (!toKey) {
30 | setValidationErr('');
31 | return;
32 | }
33 | // validate public key
34 | try {
35 | PublicKey.isOnCurve(toKey);
36 | setValidationErr(undefined);
37 | } catch (err) {
38 | setValidationErr('Invalid key');
39 | logger.errror(err);
40 | }
41 | }, [toKey]);
42 |
43 | return (
44 |
45 | Watch Account
46 |
47 |
49 |
50 | Public Key
51 |
52 |
53 | setToKey(e.target.value)}
58 | />
59 | {validationError ? (
60 |
64 | {validationError}
65 |
66 | ) : (
67 | <>>
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | function WatchAccountButton(props: {
93 | pinAccount: (pk: string, b: boolean) => void;
94 | }) {
95 | const { pinAccount } = props;
96 | const [show, setShow] = useState(false);
97 | const dispatch = useAppDispatch();
98 |
99 | const handleWatch = (toKey, isPinned) => {
100 | pinAccount(toKey, isPinned);
101 | dispatch(setSelected(toKey));
102 | setShow(false);
103 | };
104 |
105 | return (
106 |
112 |
121 |
122 | );
123 | }
124 |
125 | export default WatchAccountButton;
126 |
--------------------------------------------------------------------------------
/src/renderer/components/AirDropSolButton.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Col, Row } from 'react-bootstrap';
3 | import Button from 'react-bootstrap/Button';
4 | import Form from 'react-bootstrap/Form';
5 | import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
6 | import { useQueryClient } from 'react-query';
7 |
8 | import Popover from 'react-bootstrap/Popover';
9 | import { toast } from 'react-toastify';
10 | import { logger } from '../common/globals';
11 |
12 | import {
13 | NetStatus,
14 | selectValidatorNetworkState,
15 | } from '../data/ValidatorNetwork/validatorNetworkState';
16 |
17 | import { airdropSol } from '../data/accounts/account';
18 | import { useAppSelector } from '../hooks';
19 |
20 | function AirDropPopover(props: { pubKey: string | undefined }) {
21 | const { pubKey } = props;
22 | const { net } = useAppSelector(selectValidatorNetworkState);
23 | const queryClient = useQueryClient();
24 |
25 | let pubKeyVal = pubKey;
26 | if (!pubKeyVal) {
27 | pubKeyVal = 'paste';
28 | }
29 |
30 | const [sol, setSol] = useState('0.01');
31 | const [toKey, setToKey] = useState(pubKeyVal);
32 |
33 | useEffect(() => {
34 | if (pubKeyVal) {
35 | setToKey(pubKeyVal);
36 | }
37 | }, [pubKeyVal]);
38 |
39 | return (
40 |
41 | Airdrop SOL
42 |
43 |
45 |
46 | SOL
47 |
48 |
49 | setSol(e.target.value)}
54 | />
55 |
56 |
57 |
58 |
59 |
60 |
61 | To
62 |
63 |
64 | setToKey(e.target.value)}
69 | />
70 |
71 |
72 |
73 |
74 |
75 |
76 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
103 | function AirDropSolButton(props: { pubKey: string | undefined }) {
104 | const { pubKey } = props;
105 | const { status } = useAppSelector(selectValidatorNetworkState);
106 |
107 | return (
108 |
114 |
121 |
122 | );
123 | }
124 |
125 | export default AirDropSolButton;
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Solana Workbench
2 | 
3 | Solana Workbench is your one stop shop for local Solana development.
4 |
5 | Deploy local validators, airdrop tokens, and more with its GUI on OSX and Windows.
6 | Solana development may be like chewing glass today, but we’re on a mission to change
7 | that forever.
8 |
9 | ## Build dependencies
10 |
11 | If you already have Node on your system (we recommend version 17), you can
12 | install the Node deps like so:
13 |
14 | ```
15 | $ npm install
16 | ```
17 |
18 | In order to use Anchor functionality, the `anchor` CLI must be
19 | installed. To connect to a local test validator, you can either
20 | run one yourself on the CLI, or use the Docker functionality via
21 | the app.
22 |
23 | Detailed instructions:
24 |
25 | >> NOTE: use `bin/setup.sh` for both Linux and OSX (but don't forget to add XCode cmdline tools for OSX) - it basically does the following
26 |
27 | - [Nvm](https://github.com/nvm-sh/nvm): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash`
28 | - Node (latest version): `nvm install 16.15.0` and `nvm use 16.15.0`
29 | - Docker: `curl -o- https://get.docker.com | bash`
30 | - Yarn: `corepack enable`
31 | - Anchor CLI must be available in PATH to use Anchor stuff
32 | - from https://book.anchor-lang.com/chapter_2/installation.html
33 | - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
34 | - `source $HOME/.cargo/env`
35 | - `sh -c "$(curl -sSfL https://release.solana.com/v1.9.9/install)"`
36 | - `cargo install --git https://github.com/project-serum/anchor avm --locked --force`
37 | - `avm use latest` -- needed on Linux (needs `libudev-dev`)
38 | - Be sure to add `$HOME/.avm/bin` to your PATH to be able to run the installed binaries
39 |
40 | ### Linux
41 |
42 | to build the rust based tools (`solana` and `anchor` cli's), you will also need to install some native build tools and libraries. (See the Dockerfile for more)
43 |
44 | - Docker Desktop, or a working configured Docker setup
45 |
46 |
47 | ```
48 | sudo apt-get install -yq curl libudev-dev git build-essential libssl-dev pkg-config
49 | ```
50 |
51 | ### OSX
52 |
53 | - XCode Command Line Tools (if on OSX)
54 | - on OSX some path stuffing around, so solana and anchor binaries are in the path (for development)
55 | - Docker Desktop, or a working configured Docker setup
56 | - Add anchor and solana to your path (edit `~/.zshenv` if you're using `zsh`):
57 |
58 | ```
59 | . "$HOME/.cargo/env"
60 |
61 | path+=("$HOME/.avm/bin")
62 | path+=("$HOME/.local/share/solana/install/active_release/bin")
63 |
64 | . "$HOME/.nvm/nvm.sh"
65 |
66 | export PATH
67 | ```
68 |
69 | ### Windows (native)
70 |
71 | without anchor tooling for now
72 |
73 | - [NVM for Windows](https://github.com/coreybutler/nvm-windows)
74 | - Node (latest version): `nvm install 16.15.0`
75 | - as Administrator `nvm use 16.15.0`
76 | - Yarn: `corepack enable`
77 | - `npm install`
78 | - Docker Desktop, or a working configured Docker setup
79 |
80 |
81 | ## Run
82 |
83 | to run:
84 |
85 | ```
86 | $ npm run dev
87 | ```
88 |
89 | Now you're working with Workbench!
90 |
91 | ## Development
92 |
93 | The project is currently in a migratory phase from Bootstrap to Tailwind. Do not write new code using Bootstrap layouting. Instead, opt for using Tailwind's
94 | atomic CSS system instead. The goal is to eventually be able to fully remove bootstrap from the codebase.
95 |
96 | ## Building A Release
97 |
98 | On each platform (OSX, Windows, Linux), run:
99 |
100 | ```
101 | git clone https://github.com/workbenchapp/solana-workbench new-release-dir
102 | cd new-release-dir
103 | npm install
104 | npm run package
105 | ```
106 |
107 | To sign and notarize the OSX artifacts:
108 |
109 | 1. You must have the correct certificates from developer.apple.com installed on the build computer.
110 | 2. Signing will occur automatically during `npm run package`.
111 | 3. Notarization requires three environment variables to be set:
112 | 1. `APPLE_NOTARIZATION=1` -- Indicate that the builds should be notarized
113 | 2. `APPLE_ID` -- The email address associated with the developer Apple account
114 | 3. `APPLE_ID_PASS` -- The [app specific password](https://support.apple.com/en-us/HT204397) for the app. This is different from the Apple ID's main password and set in the developer portal.
115 |
116 | >> TODO: Add the signing steps for Windows.
117 |
118 | Then upload binaries and `latest*.yml` files to the Github release.
119 |
--------------------------------------------------------------------------------
/src/renderer/data/accounts/account.ts:
--------------------------------------------------------------------------------
1 | import { AnyAction, Dispatch, ThunkDispatch } from '@reduxjs/toolkit';
2 | import { WalletContextState } from '@solana/wallet-adapter-react';
3 | import * as sol from '@solana/web3.js';
4 | import {
5 | logger,
6 | commitmentLevel,
7 | GetValidatorConnection,
8 | } from '../../common/globals';
9 | import { NewKeyPairInfo } from '../../../types/types';
10 | import { ConfigState, setConfigValue } from '../Config/configState';
11 | import { SelectedAccountsList } from '../SelectedAccountsList/selectedAccountsState';
12 | import { Net, ValidatorState } from '../ValidatorNetwork/validatorNetworkState';
13 | import { AccountsState, reloadFromMain } from './accountState';
14 |
15 | export async function airdropSol(
16 | net: Net,
17 | toKey: string,
18 | solAmount: number | string
19 | ) {
20 | const to = new sol.PublicKey(toKey);
21 | const sols =
22 | typeof solAmount === 'number' ? solAmount : parseFloat(solAmount);
23 |
24 | const connection = GetValidatorConnection(net);
25 |
26 | const airdropSignature = await connection.requestAirdrop(
27 | to,
28 | sols * sol.LAMPORTS_PER_SOL
29 | );
30 |
31 | await connection.confirmTransaction(airdropSignature);
32 | }
33 |
34 | export async function sendSolFromSelectedWallet(
35 | connection: sol.Connection,
36 | fromKey: WalletContextState,
37 | toKey: string,
38 | solAmount: string
39 | ) {
40 | const { publicKey, sendTransaction } = fromKey;
41 | if (!publicKey) {
42 | throw Error('no wallet selected');
43 | }
44 | const toPublicKey = new sol.PublicKey(toKey);
45 |
46 | const lamports = sol.LAMPORTS_PER_SOL * parseFloat(solAmount);
47 |
48 | let signature: sol.TransactionSignature = '';
49 |
50 | const transaction = new sol.Transaction().add(
51 | sol.SystemProgram.transfer({
52 | fromPubkey: publicKey,
53 | toPubkey: toPublicKey,
54 | lamports,
55 | })
56 | );
57 |
58 | signature = await sendTransaction(transaction, connection);
59 |
60 | await connection.confirmTransaction(signature, commitmentLevel);
61 | }
62 |
63 | async function createNewAccount(
64 | dispatch?: ThunkDispatch<
65 | {
66 | validatornetwork: ValidatorState;
67 | selectedaccounts: SelectedAccountsList;
68 | config: ConfigState;
69 | account: AccountsState;
70 | },
71 | undefined,
72 | AnyAction
73 | > &
74 | Dispatch
75 | ): Promise {
76 | return window.promiseIpc
77 | .send('ACCOUNT-CreateNew')
78 | .then((account: NewKeyPairInfo) => {
79 | if (dispatch) {
80 | dispatch(reloadFromMain());
81 | }
82 | logger.info(`renderer received a new account${JSON.stringify(account)}`);
83 | const newKeypair = sol.Keypair.fromSeed(account.privatekey.slice(0, 32));
84 | return newKeypair;
85 | })
86 | .catch((e: Error) => {
87 | logger.error(e);
88 | throw e;
89 | });
90 | }
91 |
92 | export async function getElectronStorageWallet(
93 | dispatch: ThunkDispatch<
94 | {
95 | validatornetwork: ValidatorState;
96 | selectedaccounts: SelectedAccountsList;
97 | config: ConfigState;
98 | account: AccountsState;
99 | },
100 | undefined,
101 | AnyAction
102 | > &
103 | Dispatch,
104 | config: ConfigState,
105 | accounts: AccountsState
106 | ): Promise {
107 | // TODO: This will eventually move into an electron wallet module, with its promiseIPC bits abstracted, but not this month.
108 | if (config?.values?.ElectronAppStorageKeypair) {
109 | const account =
110 | accounts.accounts[config?.values?.ElectronAppStorageKeypair];
111 |
112 | if (account) {
113 | const pk = new Uint8Array({ length: 64 });
114 | // TODO: so i wanted a for loop, but somehow, all the magic TS stuff said nope.
115 | let i = 0;
116 | while (i < 64) {
117 | // const index = i.toString();
118 | const value = account.privatekey[i];
119 | pk[i] = value;
120 | i += 1;
121 | }
122 | // const pk = account.accounts[key].privatekey as Uint8Array;
123 | try {
124 | return await new Promise((resolve) => {
125 | resolve(sol.Keypair.fromSecretKey(pk));
126 | });
127 | } catch (e) {
128 | logger.error('useKeypair: ', e);
129 | }
130 | }
131 | }
132 | // if the config doesn't have a keypair set, make one..
133 | return createNewAccount(dispatch)
134 | .then((keypair) => {
135 | dispatch(
136 | setConfigValue({
137 | key: 'ElectronAppStorageKeypair',
138 | value: keypair.publicKey.toString(),
139 | })
140 | );
141 | return keypair;
142 | })
143 | .catch((e) => {
144 | logger.error(e);
145 | throw e;
146 | });
147 | }
148 |
149 | export default createNewAccount;
150 |
--------------------------------------------------------------------------------
/src/renderer/data/accounts/accountState.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import * as sol from '@solana/web3.js';
3 | import { useEffect } from 'react';
4 | import { logger } from '../../common/globals';
5 | import { useAppDispatch, useAppSelector } from '../../hooks';
6 | // https://redux.js.org/usage/usage-with-typescript#define-slice-state-and-action-types
7 | // eslint-disable-next-line import/no-cycle
8 | import { RootState } from '../../store';
9 |
10 | export interface AccountMetaValues {
11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
12 | [key: string]: any;
13 | }
14 | export interface AccountMeta {
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | [publickey: string]: AccountMetaValues;
17 | }
18 | export interface AccountsState {
19 | loading: boolean;
20 | accounts: AccountMeta;
21 | }
22 |
23 | const initialState: AccountsState = {
24 | accounts: {},
25 | loading: true,
26 | };
27 |
28 | export const accountSlice = createSlice({
29 | name: 'account',
30 | // `createSlice` will infer the state type from the `initialState` argument
31 | initialState,
32 | reducers: {
33 | // TODO - this needs a better name - its a bulk, over-write from server/main
34 | setAccount: (state, action: PayloadAction) => {
35 | state.accounts = action.payload.accounts;
36 | state.loading = action.payload.loading;
37 | },
38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
39 | setAccountValues: (
40 | state,
41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
42 | action: PayloadAction<{ key: string; value: any }>
43 | ) => {
44 | if (!action.payload.key || action.payload.key === '') {
45 | return;
46 | }
47 | if (state.accounts) {
48 | logger.info(
49 | `renderer ACCOUNT-Set: overwriting meta for ${
50 | action.payload.key
51 | } with ${JSON.stringify(action.payload.value)}`
52 | );
53 | // TODO: need to merge existing key:value with incoming (and define how to delete a key..)
54 | state.accounts[action.payload.key] = action.payload.value;
55 | window.promiseIpc
56 | .send('ACCOUNT-Set', action.payload.key, action.payload.value)
57 | .catch(logger.error);
58 | }
59 | },
60 | reloadFromMain: (state) => {
61 | logger.info('triggerReload accounts from main (setting loading = true)');
62 |
63 | state.loading = true;
64 | },
65 | },
66 | });
67 |
68 | export const accountActions = accountSlice.actions;
69 | export const { setAccountValues, reloadFromMain } = accountSlice.actions;
70 |
71 | export const selectAccountsState = (state: RootState) => state.account;
72 |
73 | export default accountSlice.reducer;
74 |
75 | const { setAccount } = accountSlice.actions;
76 | // get all accounts...
77 | export function useAccountsState() {
78 | const account = useAppSelector(selectAccountsState);
79 | const dispatch = useAppDispatch();
80 |
81 | useEffect(() => {
82 | if (account.loading) {
83 | window.promiseIpc
84 | .send('ACCOUNT-GetAll')
85 | .then((ret: AccountMeta) => {
86 | logger.verbose('LOADING accounts from main');
87 | dispatch(
88 | setAccount({
89 | accounts: ret,
90 | loading: false,
91 | })
92 | );
93 | return `return ${ret}`;
94 | })
95 | .catch((e: Error) => logger.error(e));
96 | }
97 | }, [dispatch, account.loading, account.accounts]);
98 |
99 | return account;
100 | }
101 |
102 | // get a specific account
103 | export function useAccountMeta(key: string | undefined) {
104 | const account = useAccountsState();
105 |
106 | if (!key || !account || account.loading || !account.accounts) {
107 | return undefined;
108 | }
109 | // exists to cater for the possibility that we need to do a round trip
110 | // for now, I'm just going to use the existing state
111 | return account.accounts[key];
112 | }
113 |
114 | // get a specific account
115 | export function useKeypair(key: string | undefined) {
116 | const account = useAccountMeta(key);
117 |
118 | if (
119 | !key ||
120 | !account ||
121 | account.loading ||
122 | !account.accounts ||
123 | !account.accounts[key]
124 | ) {
125 | return undefined;
126 | }
127 | const pk = new Uint8Array({ length: 64 });
128 | // TODO: so i wanted a for loop, but somehow, all the magic TS stuff said nope.
129 | let i = 0;
130 | while (i < 64) {
131 | const index = i.toString();
132 | const value = account.accounts[key].privatekey[index];
133 | pk[i] = value;
134 | i += 1;
135 | }
136 | // const pk = account.accounts[key].privatekey as Uint8Array;
137 | try {
138 | return sol.Keypair.fromSecretKey(pk);
139 | } catch (e) {
140 | logger.error('useKeypair: ', e);
141 | }
142 | return undefined;
143 | }
144 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/CreateNewMintButton.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from 'react-toastify';
2 | import * as sol from '@solana/web3.js';
3 |
4 | import * as walletAdapter from '@solana/wallet-adapter-react';
5 | import { Button } from 'react-bootstrap';
6 | import { useQueryClient } from 'react-query';
7 |
8 | import * as walletWeb3 from '../../wallet-adapter/web3';
9 |
10 | import { logger } from '../../common/globals';
11 | import { useAppSelector } from '../../hooks';
12 | import {
13 | NetStatus,
14 | selectValidatorNetworkState,
15 | } from '../../data/ValidatorNetwork/validatorNetworkState';
16 |
17 | async function createNewMint(
18 | connection: sol.Connection,
19 | payer: walletAdapter.WalletContextState,
20 | mintOwner: sol.PublicKey
21 | ): Promise {
22 | // TODO: extract to createMintButton
23 |
24 | logger.info('createMint', mintOwner.toString());
25 | // https://github.com/solana-labs/solana-program-library/blob/f487f520bf10ca29bf8d491192b6ff2b4bf89710/token/js/src/actions/createMint.ts
26 | // const mint = await createMint(
27 | // connection,
28 | // myWallet, // Payer of the transaction
29 | // myWallet.publicKey, // Account that will control the minting
30 | // null, // Account that will control the freezing of the token
31 | // 0 // Location of the decimal place
32 | // );
33 | const confirmOptions: sol.ConfirmOptions = {
34 | // using the global commitmentLevel = 'processed' causes this to error out
35 | commitment: 'finalized',
36 | };
37 | // eslint-disable-next-line promise/no-nesting
38 | return walletWeb3
39 | .createMint(
40 | connection,
41 | payer, // Payer of the transaction
42 | mintOwner, // Account that will control the minting
43 | null, // Account that will control the freezing of the token
44 | 0, // Location of the decimal place
45 | undefined, // mint keypair - will be generated if not specified
46 | confirmOptions
47 | )
48 | .then((newMint) => {
49 | logger.info('Minted ', newMint.toString());
50 |
51 | return newMint;
52 | })
53 | .catch((e) => {
54 | logger.error(e);
55 | throw e;
56 | });
57 | }
58 |
59 | export async function ensureAtaFor(
60 | connection: sol.Connection,
61 | payer: walletAdapter.WalletContextState,
62 | newMint: sol.PublicKey,
63 | ATAFor: sol.PublicKey
64 | ): Promise {
65 | // Get the token account of the fromWallet Solana address. If it does not exist, create it.
66 | logger.info('getOrCreateAssociatedTokenAccount', newMint.toString());
67 |
68 | try {
69 | const fromTokenAccount = await walletWeb3.getOrCreateAssociatedTokenAccount(
70 | connection,
71 | payer,
72 | newMint,
73 | ATAFor
74 | );
75 | // updateFunderATA(fromTokenAccount.address);
76 | return fromTokenAccount.address;
77 | } catch (e) {
78 | logger.error(
79 | e,
80 | 'getOrCreateAssociatedTokenAccount ensuremyAta',
81 | newMint.toString()
82 | );
83 | }
84 | return undefined;
85 | }
86 |
87 | function CreateNewMintButton(props: {
88 | connection: sol.Connection;
89 | fromKey: walletAdapter.WalletContextState;
90 | myWallet: sol.PublicKey | undefined;
91 | disabled: boolean;
92 | andThen: (newMint: sol.PublicKey) => sol.PublicKey;
93 | }) {
94 | const { connection, fromKey, myWallet, andThen, disabled } = props;
95 | const { status } = useAppSelector(selectValidatorNetworkState);
96 | const queryClient = useQueryClient();
97 |
98 | return (
99 |
138 | );
139 | }
140 |
141 | export default CreateNewMintButton;
142 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/MetaplexMintMetaDataView.tsx:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 | import * as metaplex from '@metaplex/js';
3 |
4 | import Accordion from 'react-bootstrap/esm/Accordion';
5 | import { useWallet } from '@solana/wallet-adapter-react';
6 | import { useQuery } from 'react-query';
7 |
8 | import { queryTokenMetadata } from '../../data/accounts/getAccount';
9 | import { selectValidatorNetworkState } from '../../data/ValidatorNetwork/validatorNetworkState';
10 | import MetaplexTokenDataButton from './MetaplexTokenData';
11 |
12 | import InlinePK from '../InlinePK';
13 | import { ActiveAccordionHeader } from './ActiveAccordionHeader';
14 | import { useAppSelector } from '../../hooks';
15 |
16 | export function MetaplexMintMetaDataView(props: { mintKey: string }) {
17 | const { mintKey } = props;
18 | const fromKey = useWallet();
19 | const { net } = useAppSelector(selectValidatorNetworkState);
20 |
21 | // TODO: this can't be here before the query
22 | // TODO: there's a better way in query v4 - https://tkdodo.eu/blog/offline-react-query
23 | // if (status !== NetStatus.Running) {
24 | // return (
25 | //
26 | //
27 | // Validator Offline{' '}
28 | //
32 | //
33 | //
34 | // Validator Offline
35 | //
36 | //
37 | // );
38 | // }
39 |
40 | const {
41 | status: loadStatus,
42 | // error,
43 | data: metaInfo,
44 | } = useQuery(
45 | ['token-mint-meta', { net, pubKey: mintKey }],
46 | // TODO: need to be able to say "we errored, don't keep looking" - there doesn't need to be metadata...
47 | queryTokenMetadata,
48 | {}
49 | );
50 |
51 | const mintEventKey = `${mintKey}_metaplex_info`;
52 |
53 | if (!mintKey) {
54 | return (
55 |
56 | {}}>
57 | No Mint selected
58 |
59 |
60 | No DATA
61 |
62 |
63 | );
64 | }
65 | const mintPubKey = new sol.PublicKey(mintKey);
66 |
67 | // ("idle" or "error" or "loading" or "success").
68 | if (loadStatus === 'loading') {
69 | return (
70 |
71 | {}}>
72 | Loading Metaplex token info{' '}
73 |
74 |
75 |
76 | No DATA
77 |
78 |
79 | );
80 | }
81 |
82 | // logger.info('token metaInfo:', JSON.stringify(metaInfo));
83 |
84 | if (!metaInfo || !metaInfo.data) {
85 | return (
86 |
87 | {}}>
88 |
89 | Metaplex Info
90 | None
91 |
92 |
93 |
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | const canEditMetadata =
105 | metaInfo.data.updateAuthority === fromKey.publicKey?.toString() &&
106 | metaInfo.data.isMutable;
107 |
108 | return (
109 |
110 | {}}>
111 |
112 | Metadata
113 |
114 |
115 |
116 |
117 | {metaInfo?.data.data.symbol}
118 |
119 | :{' '} ({metaInfo?.data.data.name} )
120 |
121 |
122 |
126 |
127 |
128 |
129 |
130 |
131 | {JSON.stringify(
132 | metaInfo,
133 | (k, v) => {
134 | if (k === 'data') {
135 | if (v.type || v.mint || v.name) {
136 | return v;
137 | }
138 | return `${JSON.stringify(v).substring(0, 32)} ...`;
139 | }
140 | return v;
141 | },
142 | 2
143 | )}
144 |
145 |
146 |
147 |
148 | );
149 | }
150 |
151 | export default MetaplexMintMetaDataView;
152 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/TokensListView.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Accordion, Container } from 'react-bootstrap';
2 | import * as sol from '@solana/web3.js';
3 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
4 | import { useQuery } from 'react-query';
5 | import { queryTokenAccounts } from '../../data/accounts/getAccount';
6 | import { selectValidatorNetworkState } from '../../data/ValidatorNetwork/validatorNetworkState';
7 |
8 | import { useAppSelector } from '../../hooks';
9 | import InlinePK from '../InlinePK';
10 | import { MintInfoView } from './MintInfoView';
11 | import { MetaplexMintMetaDataView } from './MetaplexMintMetaDataView';
12 | import MintTokenToButton from './MintTokenToButton';
13 | import TransferTokenButton from './TransferTokenButton';
14 | import { ActiveAccordionHeader } from './ActiveAccordionHeader';
15 |
16 | export function TokensListView(props: { pubKey: string | undefined }) {
17 | const { pubKey } = props;
18 | const { net } = useAppSelector(selectValidatorNetworkState);
19 | // TODO: cleanup - do we really need these here?
20 | const accountPubKey = pubKey ? new sol.PublicKey(pubKey) : undefined;
21 | const fromKey = useWallet(); // pay from wallet adapter
22 | const { connection } = useConnection();
23 |
24 | // TODO: this can't be here before the query
25 | // TODO: there's a better way in query v4 - https://tkdodo.eu/blog/offline-react-query
26 | // if (status !== NetStatus.Running) {
27 | // return (
28 | //
29 | // Validator Offline
30 | //
31 | // Validator Offline
32 | //
33 | //
34 | // );
35 | // }
36 |
37 | const {
38 | status: loadStatus,
39 | // error,
40 | data: tokenAccountsData,
41 | } = useQuery, Error>(
42 | ['parsed-token-account', { net, pubKey }],
43 | queryTokenAccounts
44 | );
45 |
46 | if (!pubKey) {
47 | return <>>;
48 | }
49 |
50 | const ataEventKey = `${pubKey}_info`;
51 | // ("idle" or "error" or "loading" or "success").
52 | if (loadStatus !== 'success') {
53 | return (
54 |
55 | {}}>
56 | Loading tokens list
57 |
58 |
59 | Loading info
60 |
61 |
62 | );
63 | }
64 |
65 | const tokenAccounts = tokenAccountsData.value;
66 |
67 | return (
68 |
69 | {tokenAccounts?.map(
70 | (tAccount: {
71 | pubkey: sol.PublicKey;
72 | account: sol.AccountInfo;
73 | }) => {
74 | const { amount } = tAccount.account.data.parsed.info.tokenAmount;
75 |
76 | // TODO: extract to its own component
77 | return (
78 |
79 |
80 |
81 |
82 |
83 |
84 | {}}
87 | >
88 |
89 | ATA
90 | {' '}
94 |
95 |
96 | {amount} token{amount > 1 && 's'}
97 |
98 |
99 | {}}
114 | />
115 |
126 |
127 |
128 |
129 |
130 |
131 | {JSON.stringify(tAccount.account, null, 2)}
132 |
133 |
134 |
135 |
136 |
139 |
142 |
143 |
144 |
145 |
146 | );
147 | }
148 | )}
149 |
150 | );
151 | }
152 |
153 | export default TokensListView;
154 |
--------------------------------------------------------------------------------
/src/renderer/nav/TokenPage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Split from 'react-split';
3 |
4 | import Stack from 'react-bootstrap/Stack';
5 | import { Row, Col, Form, Accordion } from 'react-bootstrap';
6 |
7 | import * as sol from '@solana/web3.js';
8 | import * as spltoken from '@solana/spl-token';
9 | import { useQuery } from 'react-query';
10 | import { useWallet } from '@solana/wallet-adapter-react';
11 | import { queryTokenAccounts } from '../data/accounts/getAccount';
12 |
13 | import { MetaplexMintMetaDataView } from '../components/tokens/MetaplexMintMetaDataView';
14 | import {
15 | NetStatus,
16 | selectValidatorNetworkState,
17 | } from '../data/ValidatorNetwork/validatorNetworkState';
18 | import { useAppSelector } from '../hooks';
19 | import AccountView from '../components/AccountView';
20 | import { MintInfoView } from '../components/tokens/MintInfoView';
21 |
22 | function NotAbleToShowBanner({ children }) {
23 | return (
24 |
25 |
26 |
36 |
37 | {children}
38 |
39 |
40 | );
41 | }
42 | function MintAccordians({ mintKey }) {
43 | if (!mintKey) {
44 | return No Mint selected ;
45 | }
46 | return (
47 | <>
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | >
57 | );
58 | }
59 |
60 | function TokenPage() {
61 | const fromKey = useWallet();
62 | const { net, status } = useAppSelector(selectValidatorNetworkState);
63 |
64 | // TODO: this will come from main config...
65 | const [mintList, updateMintList] = useState([]);
66 | const [mintKey, updateMintKey] = useState();
67 | const {
68 | status: loadStatus,
69 | // error,
70 | data: tokenAccountsData,
71 | } = useQuery, Error>(
72 | ['parsed-token-account', { net, pubKey: fromKey.publicKey?.toString() }],
73 | queryTokenAccounts
74 | );
75 |
76 | useEffect(() => {
77 | updateMintKey(undefined);
78 | }, [net, status]);
79 |
80 | const setMintPubKey = (pubKey: string | sol.PublicKey) => {
81 | if (typeof pubKey === 'string') {
82 | const key = new sol.PublicKey(pubKey);
83 |
84 | updateMintKey(key);
85 | } else {
86 | updateMintKey(pubKey);
87 | }
88 | };
89 |
90 | useEffect(() => {
91 | if (!tokenAccountsData) {
92 | return;
93 | }
94 | const tokenAccounts = tokenAccountsData.value;
95 |
96 | const mints: sol.PublicKey[] = [];
97 | let foundMintKey = false;
98 |
99 | tokenAccounts?.map(
100 | (tAccount: {
101 | pubkey: sol.PublicKey;
102 | account: sol.AccountInfo;
103 | }) => {
104 | const accountState = tAccount.account.data.parsed
105 | .info as spltoken.Account;
106 |
107 | mints.push(accountState.mint);
108 | if (accountState.mint.toString() === mintKey?.toString()) {
109 | foundMintKey = true;
110 | }
111 | return mints;
112 | }
113 | );
114 | if (!foundMintKey && mintKey) {
115 | updateMintKey(undefined);
116 | }
117 |
118 | updateMintList(mints);
119 | }, [mintKey, tokenAccountsData]);
120 |
121 | useEffect(() => {
122 | if (!mintKey && mintList.length > 0) {
123 | updateMintKey(mintList[0]);
124 | }
125 | }, [mintKey, mintList]);
126 |
127 | if (loadStatus !== 'success') {
128 | return Loading token mints; // TODO: need some "loading... ()"
129 | }
130 |
131 | if (!tokenAccountsData) {
132 | return Loading token mints (still);
133 | }
134 |
135 | const { publicKey } = fromKey;
136 | if (!publicKey) {
137 | return Loading wallet;
138 | }
139 | const myWallet = publicKey;
140 |
141 | if (status !== NetStatus.Running) {
142 | return (
143 |
144 | Unable to connect to selected Validator
145 |
146 | );
147 | }
148 |
149 | return (
150 |
151 |
152 |
153 |
154 |
155 |
156 |
162 |
163 | Our Wallet
164 |
165 |
166 |
167 | {mintList.length > 0 && (
168 |
169 | Token Mint :{' '}
170 | setMintPubKey(value.target.value)}
174 | defaultValue={mintKey?.toString()}
175 | >
176 | {mintList.map((key) => {
177 | return (
178 |
181 | );
182 | })}
183 |
184 |
185 |
186 | )}
187 |
188 |
189 |
190 |
191 | );
192 | }
193 |
194 | export default TokenPage;
195 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/MintInfoView.tsx:
--------------------------------------------------------------------------------
1 | import * as sol from '@solana/web3.js';
2 |
3 | import Accordion from 'react-bootstrap/esm/Accordion';
4 | import { Button, Modal } from 'react-bootstrap';
5 | import { toast } from 'react-toastify';
6 | import {
7 | useConnection,
8 | useWallet,
9 | WalletContextState,
10 | } from '@solana/wallet-adapter-react';
11 | import { useState } from 'react';
12 | import * as walletWeb3 from '../../wallet-adapter/web3';
13 | import { useAppSelector } from '../../hooks';
14 |
15 | import { useParsedAccount } from '../../data/accounts/getAccount';
16 | import { selectValidatorNetworkState } from '../../data/ValidatorNetwork/validatorNetworkState';
17 |
18 | import { logger } from '../../common/globals';
19 | import InlinePK from '../InlinePK';
20 | import { ActiveAccordionHeader } from './ActiveAccordionHeader';
21 |
22 | function ButtonWithConfirmation({ disabled, children, onClick, title }) {
23 | const [show, setShow] = useState(false);
24 |
25 | const handleClose = () => setShow(false);
26 | const handleShow = () => setShow(true);
27 |
28 | return (
29 | <>
30 |
33 |
34 |
35 |
36 | {title}
37 |
38 | {children}
39 |
40 |
49 |
52 |
53 |
54 | >
55 | );
56 | }
57 |
58 | // TODO: need to trigger an update of a component like this automatically when the cetAccount cache notices a change...
59 | export async function closeMint(
60 | connection: sol.Connection,
61 | fromKey: WalletContextState,
62 | mintKey: sol.PublicKey,
63 | myWallet: sol.PublicKey
64 | ) {
65 | if (!myWallet) {
66 | logger.info('no myWallet', myWallet);
67 | return;
68 | }
69 | if (!mintKey) {
70 | logger.info('no mintKey', mintKey);
71 | return;
72 | }
73 |
74 | await walletWeb3.setAuthority(
75 | connection,
76 | fromKey, // Payer of the transaction fees
77 | mintKey, // Account
78 | myWallet, // Current authority
79 | 'MintTokens', // Authority type: "0" represents Mint Tokens
80 | null // Setting the new Authority to null
81 | );
82 | }
83 |
84 | export function MintInfoView(props: { mintKey: string }) {
85 | const { mintKey } = props;
86 | const fromKey = useWallet();
87 | const { connection } = useConnection();
88 | const { net } = useAppSelector(selectValidatorNetworkState);
89 |
90 | const {
91 | loadStatus,
92 | account: mintInfo,
93 | error,
94 | } = useParsedAccount(net, mintKey, {
95 | retry: 2, // TODO: this is here because sometimes, we get given an accountInfo with no parsed data.
96 | });
97 | logger.debug(
98 | `MintInfoView(${mintKey}): ${loadStatus} - ${error}: ${JSON.stringify(
99 | mintInfo
100 | )}`
101 | );
102 | const mintEventKey = `${mintKey}_mint_info`;
103 |
104 | // ("idle" or "error" or "loading" or "success").
105 | if (
106 | loadStatus !== 'success' ||
107 | !mintInfo ||
108 | !mintInfo.accountInfo ||
109 | !mintInfo.accountInfo.data?.parsed
110 | ) {
111 | logger.verbose(
112 | `something not ready for ${JSON.stringify(mintInfo)}: ${loadStatus}`
113 | );
114 |
115 | return (
116 |
117 | {}}>
118 | Loading Mint info
119 |
120 |
121 | Loading Mint info
122 |
123 |
124 | );
125 | }
126 |
127 | // logger.info('mintInfo:', JSON.stringify(mintInfo));
128 | const hasAuthority =
129 | mintInfo.accountInfo.data?.parsed.info.mintAuthority ===
130 | fromKey.publicKey?.toString();
131 | const mintAuthorityIsNull =
132 | !mintInfo?.accountInfo.data?.parsed.info.mintAuthority;
133 |
134 | if (!mintInfo || mintInfo?.data) {
135 | // logger.error(`something undefined`);
136 | return (
137 |
138 | {}}>
139 | Loading Mint data
140 |
141 |
142 | Loading Mint data
143 |
144 |
145 | );
146 | }
147 |
148 | const supply = mintInfo?.accountInfo.data?.parsed.info.supply;
149 |
150 | return (
151 |
152 | {}}>
153 |
154 | Mint
155 |
156 |
157 |
158 | {supply} token{supply > 1 && 's'}
159 |
160 |
161 | {
165 | if (!fromKey.publicKey) {
166 | return;
167 | }
168 | toast.promise(
169 | closeMint(
170 | connection,
171 | fromKey,
172 | new sol.PublicKey(mintKey),
173 | fromKey.publicKey
174 | ),
175 | {
176 | pending: `Close mint account submitted`,
177 | success: `Close mint account succeeded 👌`,
178 | error: `Close mint account failed 🤯`,
179 | }
180 | );
181 | }}
182 | >
183 |
184 | Are you sure you want to close the token mint? This will set the
185 | update authority for the mint to null, and is not reversable.
186 |
187 |
188 | Mint:
189 |
190 |
191 |
192 |
193 |
194 |
195 | Mint info: {JSON.stringify(mintInfo, null, 2)}
196 |
197 |
198 |
199 | );
200 | }
201 |
202 | export default MintInfoView;
203 |
--------------------------------------------------------------------------------
/src/main/main.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, shell, dialog } from 'electron';
2 | import log from 'electron-log';
3 | import { autoUpdater } from 'electron-updater';
4 | import path from 'path';
5 | import 'regenerator-runtime/runtime';
6 | import fetchAnchorIdl from './anchor';
7 | import { RESOURCES_PATH } from './const';
8 | import { initAccountPromises } from './ipc/accounts';
9 | import { initConfigPromises } from './ipc/config';
10 | import {
11 | initDockerPromises,
12 | inspectValidatorContainer,
13 | stopValidatorContainer,
14 | removeValidatorContainer,
15 | } from './ipc/docker';
16 | import { initLogging, logger } from './logger';
17 | import MenuBuilder from './menu';
18 | import {
19 | subscribeTransactionLogs,
20 | unsubscribeTransactionLogs,
21 | } from './transactionLogs';
22 | import { resolveHtmlPath } from './util';
23 | import { validatorLogs } from './validator';
24 |
25 | export default class AppUpdater {
26 | constructor() {
27 | log.transports.file.level = 'info';
28 | autoUpdater.logger = log;
29 | autoUpdater.checkForUpdatesAndNotify();
30 | }
31 | }
32 |
33 | let mainWindow: BrowserWindow | null = null;
34 | const MAX_STRING_LOG_LENGTH = 32;
35 |
36 | initConfigPromises();
37 | initAccountPromises();
38 | initDockerPromises();
39 |
40 | ipcMain.on(
41 | 'main',
42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
43 | async (event: Electron.IpcMainEvent, method: string, msg: any) => {
44 | // logger.info('IPC event', { method, ...msg });
45 | let res = {};
46 | try {
47 | switch (method) {
48 | case 'validator-logs':
49 | res = await validatorLogs(msg);
50 | break;
51 | case 'fetch-anchor-idl':
52 | res = await fetchAnchorIdl(msg);
53 | logger.debug(`fetchIDL(${msg}: (${res})`);
54 | break;
55 | case 'subscribe-transaction-logs':
56 | await subscribeTransactionLogs(event, msg);
57 | break;
58 | case 'unsubscribe-transaction-logs':
59 | await unsubscribeTransactionLogs(event, msg);
60 | break;
61 | default:
62 | }
63 | let loggedRes = res;
64 | if (typeof loggedRes === 'string') {
65 | loggedRes = { res: `${loggedRes.slice(0, MAX_STRING_LOG_LENGTH)}...` };
66 | }
67 | // logger.info('OK', { method, ...loggedRes });
68 | event.reply('main', { method, res });
69 | } catch (e) {
70 | const error = e as Error;
71 | const { stack } = error;
72 | logger.error('ERROR', {
73 | method,
74 | name: error.name,
75 | });
76 | logger.error('Stacktrace:');
77 | stack?.split('\n').forEach((line) => logger.error(`\t${line}`));
78 | event.reply('main', { method, error });
79 | }
80 | }
81 | );
82 |
83 | if (process.env.NODE_ENV === 'production') {
84 | const sourceMapSupport = require('source-map-support');
85 | sourceMapSupport.install();
86 | }
87 |
88 | const isDevelopment =
89 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
90 |
91 | if (isDevelopment) {
92 | require('electron-debug')();
93 | }
94 |
95 | const installExtensions = async () => {
96 | const installer = require('electron-devtools-installer');
97 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
98 | const extensions = ['REACT_DEVELOPER_TOOLS'];
99 |
100 | return installer
101 | .default(
102 | extensions.map((name) => installer[name]),
103 | forceDownload
104 | )
105 | .catch(log.info);
106 | };
107 |
108 | const createWindow = async () => {
109 | if (isDevelopment) {
110 | await installExtensions();
111 | }
112 | await initLogging();
113 |
114 | const getAssetPath = (...paths: string[]): string => {
115 | return path.join(RESOURCES_PATH, ...paths);
116 | };
117 |
118 | mainWindow = new BrowserWindow({
119 | show: false,
120 | width: 1024,
121 | height: 728,
122 | icon: getAssetPath('icon.png'),
123 | webPreferences: {
124 | preload: path.join(__dirname, 'preload.js'),
125 | },
126 | });
127 |
128 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
129 | // @ts-ignore
130 | // mainWindow.Buffer = Buffer;
131 |
132 | mainWindow.loadURL(resolveHtmlPath('index.html'));
133 | mainWindow.on('ready-to-show', () => {
134 | if (!mainWindow) {
135 | throw new Error('"mainWindow" is not defined');
136 | }
137 | if (process.env.START_MINIMIZED) {
138 | mainWindow.minimize();
139 | } else {
140 | mainWindow.show();
141 | }
142 | });
143 |
144 | // eslint-disable-next-line consistent-return
145 | mainWindow.on('close', async function (e: Event) {
146 | e.preventDefault();
147 |
148 | try {
149 | const containerInspect = await inspectValidatorContainer();
150 | if (!containerInspect?.State?.Running) return app.exit(0);
151 | } catch (err) {
152 | logger.error(err);
153 | app.exit(); // not doing show will make the window "un-closable" if an error occurs while inspecting
154 | }
155 |
156 | const choice = dialog.showMessageBoxSync(mainWindow as BrowserWindow, {
157 | type: 'question',
158 | buttons: ['Stop', 'Stop & Remove', 'Leave Running', 'Cancel'],
159 | title: 'Just before you leave',
160 | message:
161 | 'What would you like to do to the Solana Validator container before exiting?',
162 | icon: getAssetPath('icon.png'),
163 | });
164 | switch (choice) {
165 | // Stop
166 | case 0:
167 | await stopValidatorContainer();
168 | app.exit(0);
169 | break;
170 | // Stop & Delete
171 | case 1:
172 | await stopValidatorContainer();
173 | await removeValidatorContainer();
174 | app.exit(0);
175 | break;
176 | // Leave Running
177 | case 2:
178 | // TODO might close multiple window at once.
179 | app.exit(0);
180 | break;
181 | // Cancel
182 | case 3:
183 | break;
184 | default:
185 | }
186 | });
187 |
188 | mainWindow.on('closed', () => {
189 | mainWindow = null;
190 | });
191 |
192 | const menuBuilder = new MenuBuilder(mainWindow);
193 | menuBuilder.buildMenu();
194 |
195 | // Open urls in the user's browser
196 | mainWindow.webContents.setWindowOpenHandler(({ url }) => {
197 | shell.openExternal(url);
198 | return { action: 'deny' };
199 | });
200 |
201 | // Remove this if your app does not use auto updates
202 | // eslint-disable-next-line
203 | new AppUpdater();
204 | };
205 |
206 | /**
207 | * Add event listeners...
208 | */
209 |
210 | app.on('window-all-closed', () => {
211 | // Respect the OSX convention of having the application in memory even
212 | // after all windows have been closed
213 | if (process.platform !== 'darwin') {
214 | app.quit();
215 | }
216 | });
217 |
218 | app
219 | .whenReady()
220 | // eslint-disable-next-line promise/always-return
221 | .then(() => {
222 | createWindow();
223 | app.on('activate', () => {
224 | // On macOS it's common to re-create a window in the app when the
225 | // dock icon is clicked and there are no other windows open.
226 | if (mainWindow === null) createWindow();
227 | });
228 | })
229 | .catch(log.catchErrors);
230 |
--------------------------------------------------------------------------------
/src/renderer/components/TransferSolButton.tsx:
--------------------------------------------------------------------------------
1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
2 | import { useEffect, useState } from 'react';
3 | import { Col, Row } from 'react-bootstrap';
4 | import Button from 'react-bootstrap/Button';
5 | import Form from 'react-bootstrap/Form';
6 | import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
7 | import Popover from 'react-bootstrap/Popover';
8 | import { toast } from 'react-toastify';
9 | import { useQueryClient } from 'react-query';
10 | import { useAppSelector } from '../hooks';
11 |
12 | import CopyIcon from './CopyIcon';
13 | import prettifyPubkey from '../common/prettifyPubkey';
14 |
15 | import { sendSolFromSelectedWallet } from '../data/accounts/account';
16 | import {
17 | NetStatus,
18 | selectValidatorNetworkState,
19 | } from '../data/ValidatorNetwork/validatorNetworkState';
20 | import { logger } from '../common/globals';
21 |
22 | const PK_FORMAT_LENGTH = 24;
23 |
24 | function TransferSolPopover(props: {
25 | pubKey: string | undefined;
26 | targetInputDisabled: boolean | undefined;
27 | targetPlaceholder: string | undefined;
28 | }) {
29 | const { pubKey, targetInputDisabled, targetPlaceholder } = props;
30 | const selectedWallet = useWallet();
31 | const { connection } = useConnection();
32 | const queryClient = useQueryClient();
33 |
34 | let pubKeyVal = pubKey;
35 | if (!pubKeyVal) {
36 | pubKeyVal = targetPlaceholder || '';
37 | }
38 |
39 | let fromKeyVal = selectedWallet.publicKey?.toString();
40 | if (!fromKeyVal) {
41 | fromKeyVal = 'unset';
42 | }
43 |
44 | const [sol, setSol] = useState('0.01');
45 | const [fromKey, setFromKey] = useState(fromKeyVal);
46 | const [toKey, setToKey] = useState(pubKeyVal);
47 |
48 | useEffect(() => {
49 | if (pubKeyVal) {
50 | setToKey(pubKeyVal);
51 | }
52 | }, [pubKeyVal]);
53 | useEffect(() => {
54 | if (fromKeyVal) {
55 | setFromKey(fromKeyVal);
56 | }
57 | }, [fromKeyVal]);
58 |
59 | return (
60 |
61 | Transfer SOL
62 |
63 |
65 |
66 | SOL
67 |
68 |
69 | setSol(e.target.value)}
74 | />
75 | {/* TODO: check to see if the from Account has enough, including TX costs if its to come from them */}
76 | {/* TODO: add a MAX button */}
77 |
78 |
79 |
80 |
81 | {/* TODO: add a switch to&from button */}
82 |
83 | {/* TODO: these can only be accounts we know the private key for ... */}
84 | {/* TODO: should be able to edit, paste and select from list populated from accountList */}
85 |
86 | From
87 |
88 |
89 |
90 |
91 | {prettifyPubkey(fromKey, PK_FORMAT_LENGTH)}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | To
102 |
103 |
104 | {targetInputDisabled ? (
105 |
106 |
107 | {prettifyPubkey(toKey, PK_FORMAT_LENGTH)}
108 |
109 |
110 |
111 | ) : (
112 | setToKey(e.target.value)}
117 | />
118 | )}
119 | {/* TODO: add radio selector to choose where the TX cost comes from
120 |
121 | Transaction cost from To account (after transfer takes place)
122 |
123 | */}
124 |
125 |
126 |
127 |
128 |
129 |
163 |
164 |
165 |
166 |
167 |
168 | );
169 | }
170 |
171 | function TransferSolButton(props: {
172 | pubKey: string | undefined;
173 | label: string | undefined;
174 | targetInputDisabled: boolean | undefined;
175 | targetPlaceholder: string | undefined;
176 | }) {
177 | const { pubKey, label, targetInputDisabled, targetPlaceholder } = props;
178 | const { status } = useAppSelector(selectValidatorNetworkState);
179 |
180 | return (
181 |
191 |
198 |
199 | );
200 | }
201 |
202 | export default TransferSolButton;
203 |
--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
1 | import {
2 | app,
3 | Menu,
4 | shell,
5 | BrowserWindow,
6 | MenuItemConstructorOptions,
7 | } from 'electron';
8 |
9 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
10 | selector?: string;
11 | submenu?: DarwinMenuItemConstructorOptions[] | Menu;
12 | }
13 |
14 | export default class MenuBuilder {
15 | mainWindow: BrowserWindow;
16 |
17 | constructor(mainWindow: BrowserWindow) {
18 | this.mainWindow = mainWindow;
19 | }
20 |
21 | buildMenu(): Menu {
22 | if (
23 | process.env.NODE_ENV === 'development' ||
24 | process.env.DEBUG_PROD === 'true'
25 | ) {
26 | this.setupDevelopmentEnvironment();
27 | }
28 |
29 | const template =
30 | process.platform === 'darwin'
31 | ? this.buildDarwinTemplate()
32 | : this.buildDefaultTemplate();
33 |
34 | const menu = Menu.buildFromTemplate(template);
35 | Menu.setApplicationMenu(menu);
36 |
37 | return menu;
38 | }
39 |
40 | setupDevelopmentEnvironment(): void {
41 | this.mainWindow.webContents.on('context-menu', (_, props) => {
42 | const { x, y } = props;
43 |
44 | Menu.buildFromTemplate([
45 | {
46 | label: 'Inspect element',
47 | click: () => {
48 | this.mainWindow.webContents.inspectElement(x, y);
49 | },
50 | },
51 | ]).popup({ window: this.mainWindow });
52 | });
53 | }
54 |
55 | buildDarwinTemplate(): MenuItemConstructorOptions[] {
56 | const subMenuAbout: DarwinMenuItemConstructorOptions = {
57 | label: 'Solana Workbench',
58 | submenu: [
59 | {
60 | label: 'About Solana Workbench',
61 | selector: 'orderFrontStandardAboutPanel:',
62 | },
63 | { type: 'separator' },
64 | { label: 'Services', submenu: [] },
65 | { type: 'separator' },
66 | {
67 | label: 'Hide Solana Workbench',
68 | accelerator: 'Command+H',
69 | selector: 'hide:',
70 | },
71 | {
72 | label: 'Hide Others',
73 | accelerator: 'Command+Shift+H',
74 | selector: 'hideOtherApplications:',
75 | },
76 | { label: 'Show All', selector: 'unhideAllApplications:' },
77 | { type: 'separator' },
78 | {
79 | label: 'Quit',
80 | accelerator: 'Command+Q',
81 | click: () => {
82 | app.quit();
83 | },
84 | },
85 | ],
86 | };
87 | const subMenuEdit: DarwinMenuItemConstructorOptions = {
88 | label: 'Edit',
89 | submenu: [
90 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
91 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
92 | { type: 'separator' },
93 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
94 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
95 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
96 | {
97 | label: 'Select All',
98 | accelerator: 'Command+A',
99 | selector: 'selectAll:',
100 | },
101 | ],
102 | };
103 | const subMenuViewDev: MenuItemConstructorOptions = {
104 | label: 'View',
105 | submenu: [
106 | {
107 | label: 'Reload',
108 | accelerator: 'Command+R',
109 | click: () => {
110 | this.mainWindow.webContents.reload();
111 | },
112 | },
113 | {
114 | label: 'Toggle Full Screen',
115 | accelerator: 'Ctrl+Command+F',
116 | click: () => {
117 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
118 | },
119 | },
120 | {
121 | label: 'Toggle Developer Tools',
122 | accelerator: 'Alt+Command+I',
123 | click: () => {
124 | this.mainWindow.webContents.toggleDevTools();
125 | },
126 | },
127 | ],
128 | };
129 | const subMenuViewProd: MenuItemConstructorOptions = {
130 | label: 'View',
131 | submenu: [
132 | {
133 | label: 'Toggle Full Screen',
134 | accelerator: 'Ctrl+Command+F',
135 | click: () => {
136 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
137 | },
138 | },
139 | ],
140 | };
141 | const subMenuWindow: DarwinMenuItemConstructorOptions = {
142 | label: 'Window',
143 | submenu: [
144 | {
145 | label: 'Minimize',
146 | accelerator: 'Command+M',
147 | selector: 'performMiniaturize:',
148 | },
149 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
150 | { type: 'separator' },
151 | { label: 'Bring All to Front', selector: 'arrangeInFront:' },
152 | ],
153 | };
154 | const subMenuHelp: MenuItemConstructorOptions = {
155 | label: 'Help',
156 | submenu: [
157 | {
158 | label: 'Github',
159 | click() {
160 | shell.openExternal(
161 | 'https://github.com/workbenchapp/solana-workbench-releases'
162 | );
163 | },
164 | },
165 | ],
166 | };
167 |
168 | const subMenuView =
169 | process.env.NODE_ENV === 'development' ||
170 | process.env.DEBUG_PROD === 'true'
171 | ? subMenuViewDev
172 | : subMenuViewProd;
173 |
174 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
175 | }
176 |
177 | buildDefaultTemplate() {
178 | const templateDefault = [
179 | {
180 | label: '&File',
181 | submenu: [
182 | {
183 | label: '&Open',
184 | accelerator: 'Ctrl+O',
185 | },
186 | {
187 | label: '&Close',
188 | accelerator: 'Ctrl+W',
189 | click: () => {
190 | this.mainWindow.close();
191 | },
192 | },
193 | ],
194 | },
195 | {
196 | label: '&View',
197 | submenu:
198 | process.env.NODE_ENV === 'development' ||
199 | process.env.DEBUG_PROD === 'true'
200 | ? [
201 | {
202 | label: '&Reload',
203 | accelerator: 'Ctrl+R',
204 | click: () => {
205 | this.mainWindow.webContents.reload();
206 | },
207 | },
208 | {
209 | label: 'Toggle &Full Screen',
210 | accelerator: 'F11',
211 | click: () => {
212 | this.mainWindow.setFullScreen(
213 | !this.mainWindow.isFullScreen()
214 | );
215 | },
216 | },
217 | {
218 | label: 'Toggle &Developer Tools',
219 | accelerator: 'Alt+Ctrl+I',
220 | click: () => {
221 | this.mainWindow.webContents.toggleDevTools();
222 | },
223 | },
224 | ]
225 | : [
226 | {
227 | label: 'Toggle &Full Screen',
228 | accelerator: 'F11',
229 | click: () => {
230 | this.mainWindow.setFullScreen(
231 | !this.mainWindow.isFullScreen()
232 | );
233 | },
234 | },
235 | ],
236 | },
237 | {
238 | label: 'Help',
239 | submenu: [
240 | {
241 | label: 'Github',
242 | click() {
243 | shell.openExternal(
244 | 'https://github.com/workbenchapp/solana-workbench-releases'
245 | );
246 | },
247 | },
248 | ],
249 | },
250 | ];
251 |
252 | return templateDefault;
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/TransferTokenButton.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from 'react-toastify';
2 | import * as sol from '@solana/web3.js';
3 |
4 | import * as walletAdapter from '@solana/wallet-adapter-react';
5 | import {
6 | Button,
7 | Col,
8 | Form,
9 | OverlayTrigger,
10 | Popover,
11 | Row,
12 | } from 'react-bootstrap';
13 | import { useEffect, useState } from 'react';
14 | import { useQueryClient } from 'react-query';
15 | import * as walletWeb3 from '../../wallet-adapter/web3';
16 |
17 | import { logger } from '../../common/globals';
18 | import { useAppSelector } from '../../hooks';
19 | import {
20 | NetStatus,
21 | selectValidatorNetworkState,
22 | } from '../../data/ValidatorNetwork/validatorNetworkState';
23 | import { ensureAtaFor } from './CreateNewMintButton';
24 |
25 | async function transferTokenToReceiver(
26 | connection: sol.Connection,
27 | fromKey: walletAdapter.WalletContextState,
28 | mintKey: sol.PublicKey,
29 | transferFrom: sol.PublicKey,
30 | transferTo: sol.PublicKey,
31 | tokenCount: number
32 | ) {
33 | if (!transferTo) {
34 | logger.info('no transferTo', transferTo);
35 | return;
36 | }
37 | if (!mintKey) {
38 | logger.info('no mintKey', mintKey);
39 | return;
40 | }
41 | if (!fromKey.publicKey) {
42 | logger.info('no fromKey.publicKey', fromKey);
43 | return;
44 | }
45 | const fromAta = await ensureAtaFor(
46 | connection,
47 | fromKey,
48 | mintKey,
49 | transferFrom
50 | );
51 | if (!fromAta) {
52 | logger.info('no fromAta', fromAta);
53 | return;
54 | }
55 | if (!transferTo) {
56 | logger.info('no transferTo', transferTo);
57 | return;
58 | }
59 |
60 | // Get the token account of the toWallet Solana address. If it does not exist, create it.
61 | logger.info('getOrCreateAssociatedTokenAccount');
62 | const ataReceiver = await ensureAtaFor(
63 | connection,
64 | fromKey,
65 | mintKey,
66 | transferTo
67 | );
68 |
69 | if (!ataReceiver) {
70 | logger.info('no ataReceiver', ataReceiver);
71 | return;
72 | }
73 | const signature = await walletWeb3.transfer(
74 | connection,
75 | fromKey, // Payer of the transaction fees
76 | fromAta, // Source account
77 | ataReceiver, // Destination account
78 | fromKey.publicKey, // Owner of the source account
79 | tokenCount // Number of tokens to transfer
80 | );
81 | logger.info('SIGNATURE', signature);
82 | }
83 |
84 | /// ///////////////////////////////////////////////////////////////////
85 |
86 | function TransferTokenPopover(props: {
87 | connection: sol.Connection;
88 | fromKey: walletAdapter.WalletContextState;
89 | mintKey: string | undefined;
90 | transferFrom: string | undefined;
91 | }) {
92 | const { connection, fromKey, mintKey, transferFrom } = props;
93 | const queryClient = useQueryClient();
94 |
95 | let pubKeyVal = '';
96 | if (!pubKeyVal) {
97 | pubKeyVal = 'paste';
98 | }
99 |
100 | let fromKeyVal = fromKey.publicKey?.toString();
101 | if (!fromKeyVal) {
102 | fromKeyVal = 'unset';
103 | }
104 |
105 | const [tokenCount, setTokenCount] = useState('1');
106 | // const [fromKey, setFromKey] = useState(fromKeyVal);
107 | const [toKey, setToKey] = useState('');
108 |
109 | useEffect(() => {
110 | if (pubKeyVal) {
111 | setToKey(pubKeyVal);
112 | }
113 | }, [pubKeyVal]);
114 | // useEffect(() => {
115 | // if (fromKeyVal) {
116 | // setFromKey(fromKeyVal);
117 | // }
118 | // }, [fromKeyVal]);
119 |
120 | return (
121 |
122 | Transfer Tokens
123 |
124 |
126 |
127 | Number of tokens
128 |
129 |
130 | setTokenCount(e.target.value)}
135 | />
136 | {/* TODO: check to see if the from Account has enough, including TX costs if its to come from them */}
137 | {/* TODO: add a MAX button */}
138 |
139 |
140 |
141 |
142 | {/* TODO: add a switch to&from button */}
143 |
144 | {/* TODO: these can only be accounts we know the private key for ... */}
145 | {/* TODO: should be able to edit, paste and select from list populated from accountList */}
146 |
147 | From
148 |
149 |
150 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | To
163 |
164 |
165 | setToKey(e.target.value)}
170 | />
171 |
172 | {/* TODO: add radio selector to choose where the TX cost comes from */}
173 | Transaction costs, and Ata Rent from wallet
174 |
175 |
176 |
177 |
178 |
179 |
180 |
225 |
226 |
227 |
228 |
229 |
230 | );
231 | }
232 |
233 | function TransferTokenButton(props: {
234 | connection: sol.Connection;
235 | fromKey: walletAdapter.WalletContextState;
236 | mintKey: string | undefined;
237 | transferFrom: string | undefined;
238 | disabled: boolean;
239 | }) {
240 | const { connection, fromKey, mintKey, transferFrom, disabled } = props;
241 | const { status } = useAppSelector(selectValidatorNetworkState);
242 |
243 | return (
244 |
255 |
264 |
265 | );
266 | }
267 |
268 | export default TransferTokenButton;
269 |
--------------------------------------------------------------------------------
/src/renderer/components/tokens/MetaplexTokenData.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
3 | import Popover from 'react-bootstrap/Popover';
4 | import Button from 'react-bootstrap/Button';
5 | import Form from 'react-bootstrap/Form';
6 | import { Row, Col } from 'react-bootstrap';
7 | import { toast } from 'react-toastify';
8 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
9 | import * as metaplex from '@metaplex/js';
10 | import * as sol from '@solana/web3.js';
11 |
12 | import { useQuery, useQueryClient } from 'react-query';
13 | import { queryTokenMetadata } from '../../data/accounts/getAccount';
14 | import { useAppSelector } from '../../hooks';
15 |
16 | import {
17 | NetStatus,
18 | selectValidatorNetworkState,
19 | } from '../../data/ValidatorNetwork/validatorNetworkState';
20 |
21 | const logger = window.electron.log;
22 |
23 | function DataPopover(props: { mintPubKey: sol.PublicKey }) {
24 | const { mintPubKey } = props;
25 | const selectedWallet = useWallet();
26 | const { connection } = useConnection();
27 | const { net } = useAppSelector(selectValidatorNetworkState);
28 | const queryClient = useQueryClient();
29 |
30 | const pubKey = mintPubKey.toString();
31 | const {
32 | status: loadStatus,
33 | // error,
34 | data: metaData,
35 | } = useQuery(
36 | ['token-mint-meta', { net, pubKey }],
37 | queryTokenMetadata,
38 | {}
39 | );
40 |
41 | const [name, setName] = useState(
42 | metaData?.data?.data.name || 'Workbench token'
43 | );
44 | const [symbol, setSymbol] = useState(
45 | metaData?.data?.data.symbol || 'WORKBENCH'
46 | );
47 | const [uri, setUri] = useState(
48 | metaData?.data?.data.uri ||
49 | 'https://github.com/workbenchapp/solana-workbench/'
50 | );
51 | const [sellerFeeBasisPoints, setSellerFeeBasisPoints] = useState(
52 | metaData?.data?.data.sellerFeeBasisPoints || 10
53 | );
54 |
55 | if (loadStatus !== 'success' && loadStatus !== 'error') {
56 | return loading;
57 | }
58 |
59 | async function createOurMintMetadata() {
60 | // Create a new token
61 | logger.info('createOurMintMetadata', mintPubKey);
62 | if (!mintPubKey) {
63 | return;
64 | }
65 | try {
66 | const metadataToSet = new metaplex.programs.metadata.MetadataDataData({
67 | name,
68 | symbol,
69 | uri,
70 | sellerFeeBasisPoints,
71 | creators: null, // TODO:
72 | });
73 |
74 | if (
75 | metaData &&
76 | metaData.data &&
77 | metaData.data.mint === mintPubKey.toString()
78 | ) {
79 | // https://github.com/metaplex-foundation/js/blob/a4274ec97c6599dbfae8860ae2edc03f49d35d68/src/actions/updateMetadata.ts
80 | const meta = await metaplex.actions.updateMetadata({
81 | connection,
82 | wallet: selectedWallet,
83 | editionMint: mintPubKey,
84 | /** An optional new {@link MetadataDataData} object to replace the current data. This will completely overwrite the data so all fields must be set explicitly. * */
85 | newMetadataData: metadataToSet,
86 | // newUpdateAuthority?: PublicKey,
87 | // /** This parameter can only be set to true once after which it can't be reverted to false **/
88 | // primarySaleHappened?: boolean,
89 | });
90 | logger.info('update metadata', meta);
91 | } else {
92 | // https://github.com/metaplex-foundation/js/blob/a4274ec97c6599dbfae8860ae2edc03f49d35d68/src/actions/createMetadata.ts#L32
93 | const meta = await metaplex.actions.createMetadata({
94 | connection,
95 | wallet: selectedWallet,
96 | editionMint: mintPubKey,
97 | metadataData: metadataToSet,
98 | });
99 | logger.info('create metadata', meta);
100 | }
101 |
102 | // const meta = metaplex.programs.metadata.Metadata.load(conn, tokenPublicKey);
103 | } catch (e) {
104 | logger.error('metadata create', e);
105 | throw e;
106 | }
107 | }
108 |
109 | return (
110 |
111 | Metaplex token metadata
112 |
113 |
115 |
116 | Name
117 |
118 |
119 | setName(e.target.value)}
124 | />
125 |
126 |
127 |
128 |
129 |
130 | Symbol
131 |
132 |
133 | setSymbol(e.target.value)}
138 | />
139 |
140 |
141 |
142 |
143 |
144 | Uri
145 |
146 |
147 | setUri(e.target.value)}
152 | />
153 |
154 |
155 |
156 |
157 |
162 |
163 | Sellr basis points
164 |
165 |
166 | setSellerFeeBasisPoints(e.target.value)}
171 | />
172 | {/* TODO: check to see if the from Account has enough, including TX costs if its to come from them */}
173 | {/* TODO: add a MAX button */}
174 |
175 |
176 |
177 |
178 |
179 |
180 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | }
220 |
221 | function MetaplexTokenDataButton(props: {
222 | mintPubKey: sol.PublicKey | undefined;
223 | disabled: boolean;
224 | }) {
225 | const { mintPubKey, disabled } = props;
226 | const { status } = useAppSelector(selectValidatorNetworkState);
227 |
228 | if (!mintPubKey) {
229 | return <>>;
230 | }
231 |
232 | return (
233 |
239 |
248 |
249 | );
250 | }
251 |
252 | export default MetaplexTokenDataButton;
253 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 |
2 |
3 | {
4 | "name": "solana-workbench",
5 | "productName": "SolanaWorkbench",
6 | "description": "Solana workbench app for making development on Solana better",
7 | "version": "0.4.0",
8 | "main": "./release/dist/main/main.js",
9 | "scripts": {
10 | "start": "npm run dev",
11 | "dev": "concurrently --kill-others \"npm run start:main\" \"npm run start:renderer\"",
12 | "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
13 | "build:main": "tsc -p ./src/main/tsconfig.json",
14 | "build:renderer": "vite build --config ./src/renderer/vite.config.ts",
15 | "start:main": "npm run build:main && cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only ./src/main/main.ts",
16 | "start:renderer": "vite dev --config ./src/renderer/vite.config.ts",
17 | "package": "rimraf ./release && npm run build && electron-builder -- --publish always --win --mac --linux",
18 | "package-nomac": "rimraf ./release && npm run build && electron-builder -- --publish always --win --linux",
19 | "package:asarless": "npm run build && electron-builder build --config.asar=false",
20 | "lint": "cross-env NODE_ENV=development concurrently \"eslint . --ext .js,.jsx,.ts,.tsx\" \"tsc -p ./src/renderer --noemit\" \"tsc -p ./src/main --noemit\"",
21 | "lint-fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
22 | "test": "vitest run --dir ./src --config ./src/renderer/vitest.config.ts",
23 | "prepare": "husky install",
24 | "postinstall": "electron-builder install-app-deps"
25 | },
26 | "browserslist": [
27 | "last 1 electron version"
28 | ],
29 | "lint-staged": {
30 | "*.{js,jsx,ts,tsx}": [
31 | "cross-env NODE_ENV=development eslint"
32 | ],
33 | "*.json,.{eslintrc,prettierrc}": [
34 | "prettier --ignore-path .eslintignore --parser json --write"
35 | ],
36 | "*.{css,scss}": [
37 | "prettier --ignore-path .eslintignore --single-quote --write"
38 | ],
39 | "*.{html,md,yml}": [
40 | "prettier --ignore-path .eslintignore --single-quote --write"
41 | ]
42 | },
43 | "electronmon": {
44 | "patterns": [
45 | "!src/renderer/**"
46 | ]
47 | },
48 | "build": {
49 | "productName": "Solana Workbench",
50 | "appId": "org.erb.SolanaWorkbench",
51 | "asar": true,
52 | "asarUnpack": "**\\*.{node,dll}",
53 | "files": [
54 | "./release/dist/**/*",
55 | "!**/*.d.ts",
56 | "package.json"
57 | ],
58 | "mac": {
59 | "target": {
60 | "target": "default",
61 | "arch": [
62 | "arm64",
63 | "x64"
64 | ]
65 | },
66 | "type": "distribution",
67 | "hardenedRuntime": true,
68 | "entitlements": "assets/entitlements.mac.plist",
69 | "entitlementsInherit": "assets/entitlements.mac.plist",
70 | "gatekeeperAssess": false
71 | },
72 | "dmg": {
73 | "contents": [
74 | {
75 | "x": 130,
76 | "y": 220
77 | },
78 | {
79 | "x": 410,
80 | "y": 220,
81 | "type": "link",
82 | "path": "/Applications"
83 | }
84 | ]
85 | },
86 | "win": {
87 | "target": [
88 | "nsis"
89 | ]
90 | },
91 | "linux": {
92 | "target": [
93 | "AppImage"
94 | ],
95 | "category": "Development"
96 | },
97 | "directories": {
98 | "buildResources": "assets",
99 | "output": "release/build"
100 | },
101 | "extraResources": [
102 | "assets/**/*"
103 | ],
104 | "publish": [
105 | {
106 | "provider": "github",
107 | "owner": "workbenchapp",
108 | "repo": "solana-workbench"
109 | }
110 | ]
111 | },
112 | "repository": {
113 | "type": "git",
114 | "url": "git+https://github.com/workbenchapp/solana-workbench"
115 | },
116 | "author": {
117 | "name": "CryptoWorkbench inc",
118 | "email": "nathan@cryptoworkbench.io",
119 | "url": "https://cryptoworkbench.io"
120 | },
121 | "contributors": [],
122 | "license": "MIT",
123 | "bugs": {
124 | "url": "https://github.com/workbenchapp/solana-workbench/issues"
125 | },
126 | "keywords": [
127 | "electron",
128 | "boilerplate",
129 | "react",
130 | "typescript",
131 | "ts",
132 | "sass",
133 | "hot",
134 | "reload",
135 | "vite"
136 | ],
137 | "homepage": "https://github.com/workbenchapp/solana-workbench",
138 | "devDependencies": {
139 | "@esbuild-plugins/node-globals-polyfill": "^0.1.1",
140 | "@iconify-json/mdi": "^1.1.20",
141 | "@project-serum/anchor": "^0.25.0-beta.1",
142 | "@solana/wallet-adapter-wallets": "^0.15.5",
143 | "@solana/web3.js": "^1.41.3",
144 | "@svgr/core": "^6.2.1",
145 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
146 | "@testing-library/react": "^13.2.0",
147 | "@types/amplitude-js": "^8.0.2",
148 | "@types/dockerode": "^3.3.9",
149 | "@types/dompurify": "^2.3.3",
150 | "@types/enzyme": "^3.10.10",
151 | "@types/history": "^5.0.0",
152 | "@types/logfmt": "^1.2.2",
153 | "@types/node": "^17.0.31",
154 | "@types/prop-types": "^15.7.4",
155 | "@types/react": "^18.0.15",
156 | "@types/react-dom": "^18.0.3",
157 | "@types/react-outside-click-handler": "^1.3.0",
158 | "@types/react-test-renderer": "^18.0.0",
159 | "@types/shelljs": "^0.8.11",
160 | "@types/sqlite3": "^3.1.7",
161 | "@types/underscore": "^1.11.3",
162 | "@types/uuid": "^8.3.3",
163 | "@typescript-eslint/eslint-plugin": "^5.16.0",
164 | "@typescript-eslint/parser": "^5.16.0",
165 | "@typescript-eslint/typescript-estree": "^5.16.0",
166 | "@vitejs/plugin-react": "^1.3.2",
167 | "chalk": "^4.1.2",
168 | "concurrently": "^7.1.0",
169 | "core-js": "^3.20.1",
170 | "cross-env": "^7.0.3",
171 | "css-loader": "^6.5.1",
172 | "detect-port": "^1.3.0",
173 | "electron": "^18.2.0",
174 | "electron-builder": "^23.0.3",
175 | "electron-devtools-installer": "^3.2.0",
176 | "electron-notarize": "^1.1.1",
177 | "electron-rebuild": "^3.2.5",
178 | "electronmon": "^2.0.2",
179 | "enzyme": "^3.11.0",
180 | "enzyme-to-json": "^3.6.2",
181 | "eslint": "^8.15.0",
182 | "eslint-config-airbnb": "^19.0.4",
183 | "eslint-config-airbnb-base": "^15.0.0",
184 | "eslint-config-erb": "^4.0.3",
185 | "eslint-config-prettier": "^8.5.0",
186 | "eslint-import-resolver-typescript": "^2.5.0",
187 | "eslint-plugin-compat": "^4.0.2",
188 | "eslint-plugin-import": "^2.25.4",
189 | "eslint-plugin-jest": "^26.5.3",
190 | "eslint-plugin-jsx-a11y": "^6.5.1",
191 | "eslint-plugin-prettier": "^4.0.0",
192 | "eslint-plugin-promise": "^6.0.0",
193 | "eslint-plugin-react": "^7.29.4",
194 | "eslint-plugin-react-hooks": "^4.3.0",
195 | "file-loader": "^6.2.0",
196 | "husky": "^8.0.0",
197 | "identity-obj-proxy": "^3.0.0",
198 | "jest": "^28.1.1",
199 | "jsdom": "^20.0.0",
200 | "lint-staged": "^12.4.1",
201 | "mini-css-extract-plugin": "^2.4.3",
202 | "opencollective-postinstall": "^2.0.3",
203 | "prettier": "^2.6.2",
204 | "react-refresh": "^0.13.0",
205 | "react-refresh-typescript": "^2.0.2",
206 | "react-test-renderer": "^18.1.0",
207 | "rimraf": "^3.0.2",
208 | "sass": "^1.52.3",
209 | "ts-node": "^10.8.1",
210 | "typescript": "^4.6.2",
211 | "unplugin-auto-import": "^0.8.8",
212 | "unplugin-icons": "^0.14.3",
213 | "url-loader": "^4.1.1",
214 | "vite": "2.9.12",
215 | "vite-plugin-checker": "^0.4.6",
216 | "vite-plugin-environment": "^1.1.1",
217 | "vite-plugin-fonts": "^0.4.0",
218 | "vite-plugin-inline-css-modules": "^0.0.4",
219 | "vite-plugin-windicss": "^1.8.4",
220 | "vitest": "^0.14.2",
221 | "windicss": "^3.5.4"
222 | },
223 | "dependencies": {
224 | "@fortawesome/fontawesome-svg-core": "^6.1.0",
225 | "@fortawesome/free-regular-svg-icons": "^6.1.1",
226 | "@fortawesome/free-solid-svg-icons": "^6.1.0",
227 | "@fortawesome/react-fontawesome": "^0.1.18",
228 | "@metaplex/js": "^4.12.0",
229 | "@reduxjs/toolkit": "^1.7.2",
230 | "@solana/spl-token": "^0.2.0",
231 | "@solana/wallet-adapter-base": "^0.9.5",
232 | "@solana/wallet-adapter-react": "^0.15.4",
233 | "@solana/wallet-adapter-react-ui": "^0.9.6",
234 | "amplitude-js": "^8.12.0",
235 | "ansi_up": "^5.1.0",
236 | "bip39": "^3.0.4",
237 | "bootstrap": "^5.1.3",
238 | "buffer": "^6.0.3",
239 | "classnames": "^2.3.1",
240 | "dockerode": "^3.3.2",
241 | "dompurify": "^2.3.8",
242 | "electron-cfg": "^1.2.7",
243 | "electron-debug": "^3.2.0",
244 | "electron-log": "^4.4.6",
245 | "electron-promise-ipc": "^2.2.4",
246 | "electron-updater": "^5.0.1",
247 | "hexdump-nodejs": "^0.1.0",
248 | "is-electron": "^2.2.1",
249 | "logfmt": "^1.3.2",
250 | "react": "^18.1.0",
251 | "react-bootstrap": "^2.0.2",
252 | "react-dom": "^18.1.0",
253 | "react-editext": "^4.2.1",
254 | "react-outside-click-handler": "^1.3.0",
255 | "react-query": "^3.39.1",
256 | "react-redux": "^8.0.1",
257 | "react-router": "^6.2.2",
258 | "react-router-dom": "^6.2.2",
259 | "react-split": "^2.0.14",
260 | "react-toastify": "^9.0.1",
261 | "regenerator-runtime": "^0.13.9",
262 | "shelljs": "^0.8.5",
263 | "typescript-lru-cache": "^1.2.3",
264 | "underscore": "^1.13.1",
265 | "victory": "^36.3.2",
266 | "winston": "^3.3.3"
267 | },
268 | "devEngines": {
269 | "node": ">=16.15.0",
270 | "npm": ">=8.x"
271 | },
272 | "prettier": {
273 | "overrides": [
274 | {
275 | "files": [
276 | ".prettierrc",
277 | ".eslintrc"
278 | ],
279 | "options": {
280 | "parser": "json"
281 | }
282 | }
283 | ],
284 | "singleQuote": true
285 | },
286 | "husky": {
287 | "hooks": {
288 | "pre-commit": "lint-staged"
289 | }
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/src/renderer/components/AccountView.tsx:
--------------------------------------------------------------------------------
1 | import { faTerminal } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { useEffect, useState } from 'react';
4 | import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
5 | import Container from 'react-bootstrap/Container';
6 | import { Button } from 'react-bootstrap';
7 | import {
8 | useConnection,
9 | useWallet,
10 | useAnchorWallet,
11 | } from '@solana/wallet-adapter-react';
12 | import { Program, AnchorProvider, setProvider } from '@project-serum/anchor';
13 | import * as sol from '@solana/web3.js';
14 | import { useQueryClient } from 'react-query';
15 | import { GetValidatorConnection, logger } from '../common/globals';
16 | import { useAppDispatch, useAppSelector } from '../hooks';
17 |
18 | import {
19 | setAccountValues,
20 | useAccountMeta,
21 | } from '../data/accounts/accountState';
22 | import {
23 | getHumanName,
24 | forceRequestAccount,
25 | renderRawData,
26 | truncateLamportAmount,
27 | useParsedAccount,
28 | } from '../data/accounts/getAccount';
29 | import { selectValidatorNetworkState } from '../data/ValidatorNetwork/validatorNetworkState';
30 | import AirDropSolButton from './AirDropSolButton';
31 | import EditableText from './base/EditableText';
32 | import InlinePK from './InlinePK';
33 | import TransferSolButton from './TransferSolButton';
34 |
35 | import CreateNewMintButton, {
36 | ensureAtaFor,
37 | } from './tokens/CreateNewMintButton';
38 |
39 | import { TokensListView } from './tokens/TokensListView';
40 |
41 | function AccountView(props: { pubKey: string | undefined }) {
42 | const { pubKey } = props;
43 | const { net } = useAppSelector(selectValidatorNetworkState);
44 | const dispatch = useAppDispatch();
45 | const accountMeta = useAccountMeta(pubKey);
46 | const [humanName, setHumanName] = useState('');
47 | const accountPubKey = pubKey ? new sol.PublicKey(pubKey) : undefined;
48 | const fromKey = useWallet(); // pay from wallet adapter
49 | const { connection } = useConnection();
50 | const queryClient = useQueryClient();
51 |
52 | const { /* loadStatus, */ account /* , error */ } = useParsedAccount(
53 | net,
54 | pubKey,
55 | {}
56 | );
57 |
58 | // ("idle" or "error" or "loading" or "success").
59 | // TODO: this can't be here before the query
60 | // TODO: there's a better way in query v4 - https://tkdodo.eu/blog/offline-react-query
61 |
62 | // create dummy keypair wallet if none is selected by user
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | const wallet = useAnchorWallet() || {
65 | signAllTransactions: async (
66 | transactions: sol.Transaction[]
67 | ): Promise => Promise.resolve(transactions),
68 | signTransaction: async (
69 | transaction: sol.Transaction
70 | ): Promise => Promise.resolve(transaction),
71 | publicKey: new sol.Keypair().publicKey,
72 | };
73 |
74 | const [decodedAccountData, setDecodedAccountData] = useState();
75 |
76 | useEffect(() => {
77 | setDecodedAccountData('');
78 | const decodeAnchor = async () => {
79 | try {
80 | if (
81 | account?.accountInfo &&
82 | !account.accountInfo.owner.equals(sol.SystemProgram.programId) &&
83 | wallet
84 | ) {
85 | // TODO: Why do I have to set this every time
86 | setProvider(
87 | new AnchorProvider(
88 | GetValidatorConnection(net),
89 | wallet,
90 | AnchorProvider.defaultOptions()
91 | )
92 | );
93 | const info = account.accountInfo;
94 | const program = await Program.at(info.owner);
95 |
96 | program?.idl?.accounts?.forEach((accountType) => {
97 | try {
98 | const decodedAccount = program.coder.accounts.decode(
99 | accountType.name,
100 | info.data
101 | );
102 | setDecodedAccountData(JSON.stringify(decodedAccount, null, 2));
103 | } catch (e) {
104 | const err = e as Error;
105 | // TODO: only log when error != invalid discriminator
106 | if (err.message !== 'Invalid account discriminator') {
107 | logger.silly(
108 | `Account decode failed err="${e}" attempted_type=${accountType.name}`
109 | );
110 | }
111 | }
112 | });
113 | }
114 | } catch (e) {
115 | logger.error(e);
116 | setDecodedAccountData(renderRawData(account));
117 | }
118 | };
119 | decodeAnchor();
120 | }, [account, net, wallet]);
121 |
122 | useEffect(() => {
123 | const alias = getHumanName(accountMeta);
124 | setHumanName(alias);
125 | }, [pubKey, accountMeta]);
126 |
127 | const handleHumanNameSave = (val: string) => {
128 | if (!pubKey) {
129 | return;
130 | }
131 | dispatch(
132 | setAccountValues({
133 | key: pubKey,
134 | value: {
135 | ...accountMeta,
136 | humanname: val,
137 | },
138 | })
139 | );
140 | };
141 |
142 | // const humanName = getHumanName(accountMeta);
143 | return (
144 |
145 |
146 |
147 |
148 |
154 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | Editable Alias
179 |
180 |
181 |
182 |
183 |
184 |
188 |
189 |
190 |
191 |
192 |
193 | Address
194 |
195 |
196 | {pubKey ? : 'None selected'}
197 |
198 |
199 |
200 |
201 | Assigned Program Id
202 |
203 |
204 | {account ? (
205 |
208 | ) : (
209 | 'Not on chain'
210 | )}
211 |
212 |
213 |
214 |
215 |
216 | SOL
217 |
218 |
219 |
220 | {account ? truncateLamportAmount(account) : 0}
221 |
222 |
223 |
224 |
225 |
226 | Executable
227 |
228 |
229 | {account?.accountInfo?.executable ? (
230 |
231 |
235 | Yes
236 |
237 | ) : (
238 |
239 | No
240 |
241 | )}
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 | Data :
251 |
252 |
253 | {decodedAccountData !== '' ? (
254 |
255 | {decodedAccountData}
256 |
257 | ) : (
258 | No Data
259 | )}
260 |
261 |
262 |
263 |
264 |
265 | Token Accounts
266 | {/* this button should only be enabled for accounts that you can create a new mint for... */}
267 | {
276 | if (!accountPubKey) {
277 | return newMint;
278 | }
279 | ensureAtaFor(connection, fromKey, newMint, accountPubKey); // needed as we create the Mintlist using the ATA's the user wallet has ATA's for...
280 | return newMint;
281 | }}
282 | />
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 | );
292 | }
293 |
294 | export default AccountView;
295 |
--------------------------------------------------------------------------------