Skip to content

Commit

Permalink
Add support for streaming transaction logs (#31)
Browse files Browse the repository at this point in the history
* Stream transaction logs in addition to program changes

* Make logs subscribe work across multiple nets

* Add Program Changes back in as a tab

Signed-off-by: Nathan LeClaire <[email protected]>

* Import relatively (CI thing)
  • Loading branch information
nathanleclaire authored Mar 23, 2022
1 parent 903da2d commit 4722889
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 12 deletions.
10 changes: 10 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
subscribeProgramChanges,
unsubscribeProgramChanges,
} from './programChanges';
import {
subscribeTransactionLogs,
unsubscribeTransactionLogs,
} from './transactionLogs';
import { RESOURCES_PATH } from './const';
import { db, initDB } from './db';
import wbConfig from './config';
Expand Down Expand Up @@ -84,6 +88,12 @@ ipcMain.on(
case 'get-validator-network-info':
res = await fetchValidatorNetworkInfo(msg);
break;
case 'subscribe-transaction-logs':
await subscribeTransactionLogs(event, msg);
break;
case 'unsubscribe-transaction-logs':
await unsubscribeTransactionLogs(event, msg);
break;
default:
}
let loggedRes = res;
Expand Down
6 changes: 6 additions & 0 deletions src/main/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ contextBridge.exposeInMainWorld('electron', {
config(msg) {
send('config', msg);
},
subscribeTransactionLogs(msg) {
send('subscribe-transaction-logs', msg);
},
unsubscribeTransactionLogs(msg) {
send('unsubscribe-transaction-logs', msg);
},
on(method, func) {
ipcRenderer.on(method, (event, ...args) => func(...args));
},
Expand Down
31 changes: 31 additions & 0 deletions src/main/transactionLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as sol from '@solana/web3.js';
import Electron from 'electron';
import { LogSubscriptionMap } from 'types/types';
import { netToURL } from '../common/strings';

const logSubscriptions: LogSubscriptionMap = {};
const subscribeTransactionLogs = async (
event: Electron.IpcMainEvent,
msg: any
) => {
const solConn = new sol.Connection(netToURL(msg.net));
const subscriptionID = solConn.onLogs(
'all',
(logsInfo) => {
event.reply('transaction-logs', logsInfo);
},
'processed'
);
logSubscriptions[msg.net] = { subscriptionID, solConn };
};

const unsubscribeTransactionLogs = async (
_event: Electron.IpcMainEvent,
msg: any
) => {
const sub = logSubscriptions[msg.net];
await sub.solConn.removeOnLogsListener(sub.subscriptionID);
delete logSubscriptions[msg.net];
};

export { unsubscribeTransactionLogs, subscribeTransactionLogs };
55 changes: 55 additions & 0 deletions src/renderer/components/LogView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { RootState } from 'renderer/slices/mainSlice';

const LogView = () => {
const [logs, setLogs] = useState<string[]>([]);
const validator = useSelector((state: RootState) => state.validator);
const { net } = validator;

useEffect(() => {
const listener = (logsResp: any) => {
ReactDOM.unstable_batchedUpdates(() => {
setLogs((prevLogs: string[]) => {
const newLogs = [...logsResp.logs.reverse(), ...prevLogs];

// utter pseudo-science -- determine max log lines from window size
const MAX_DISPLAYED_LOG_LINES = window.innerHeight / 22;
if (newLogs.length > MAX_DISPLAYED_LOG_LINES) {
return newLogs.slice(0, MAX_DISPLAYED_LOG_LINES);
}
return newLogs;
});
});
};
setLogs([]);
window.electron.ipcRenderer.on('transaction-logs', listener);
window.electron.ipcRenderer.subscribeTransactionLogs({
net,
});

return () => {
window.electron.ipcRenderer.removeAllListeners('transaction-logs');
window.electron.ipcRenderer.unsubscribeTransactionLogs({
net,
});
};
}, [net]);

return (
<div>
{logs.length > 0 ? (
<pre>
<code>{logs.join('\n')}</code>
</pre>
) : (
<p className="text-secondary">
Logs will appear here once transactions are processed.
</p>
)}
</div>
);
};

export default LogView;
57 changes: 45 additions & 12 deletions src/renderer/nav/Accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { NavLink } from 'react-router-dom';
import AccountListView from 'renderer/components/AccountListView';
import AccountView from 'renderer/components/AccountView';
import InlinePK from 'renderer/components/InlinePK';
import ProgramChangeView from 'renderer/components/ProgramChangeView';
import LogView from '../components/LogView';
import ProgramChangeView from '../components/ProgramChangeView';

import {
accountsActions,
RootState,
Expand All @@ -27,6 +29,9 @@ import {
WBAccount,
} from 'types/types';

const LIVE_TAB_CHANGES = 'changes';
const LIVE_TAB_TXN_LOGS = 'logs';

const Accounts = () => {
const dispatch = useDispatch();
const accounts: AccountsState = useSelector(
Expand All @@ -36,6 +41,8 @@ const Accounts = () => {
const { net } = validator;
const { rootKey, selectedAccount, listedAccounts } = accounts;
const [addBtnClicked, setAddBtnClicked] = useState<boolean>(false);
const [selectedLiveTab, setSelectedLiveTab] =
useState<string>(LIVE_TAB_CHANGES);

const attemptAccountAdd = (pubKey: string, initializing: boolean) => {
if (initializing && pubKey === ACCOUNTS_NONE_KEY) {
Expand Down Expand Up @@ -149,6 +156,16 @@ const Accounts = () => {
);
}

let selectedLiveComponent = <LogView />;
if (selectedLiveTab === LIVE_TAB_CHANGES) {
selectedLiveComponent = (
<ProgramChangeView
accounts={listedAccounts}
attemptAccountAdd={attemptAccountAdd}
/>
);
}

let display = <></>;
if (validator.status === NetStatus.Running) {
display = (
Expand All @@ -162,8 +179,9 @@ const Accounts = () => {
</span>
<button
type="button"
className={`ms-2 btn rounded btn-block btn-sm no-box-shadow ${addBtnClicked ? 'btn-primary-darker' : 'btn-primary'
}`}
className={`ms-2 btn rounded btn-block btn-sm no-box-shadow ${
addBtnClicked ? 'btn-primary-darker' : 'btn-primary'
}`}
onMouseDown={(
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
): void => {
Expand All @@ -190,32 +208,47 @@ const Accounts = () => {
<div>
<ul className="nav">
<li
className={`${selectedAccount
className={`${
selectedAccount
? 'border-bottom active'
: 'opacity-25 cursor-not-allowed'
} ms-3 me-3 pt-1 pb-1 border-3 nav-item text-secondary nav-link-tab`}
} ms-3 me-3 pt-1 pb-1 border-3 nav-item text-secondary nav-link-tab`}
>
<small>Account</small>
</li>
<li
className={`${selectedAccountInfo ? '' : 'border-bottom active'
} ms-3 me-3 pt-1 pb-1 border-3 cursor-pointer nav-item text-secondary nav-link-tab`}
className={`${
!selectedAccountInfo && selectedLiveTab === LIVE_TAB_TXN_LOGS
? 'border-bottom active'
: ''
} ms-3 me-3 pt-1 pb-1 border-3 cursor-pointer nav-item text-secondary nav-link-tab`}
onClick={() => {
dispatch(accountsActions.setSelected(''));
setSelectedLiveTab(LIVE_TAB_TXN_LOGS);
}}
>
<small>Logs</small>
</li>
<li
className={`${
!selectedAccountInfo && selectedLiveTab === LIVE_TAB_CHANGES
? 'border-bottom active'
: ''
} ms-3 me-3 pt-1 pb-1 border-3 cursor-pointer nav-item text-secondary nav-link-tab`}
onClick={() => {
dispatch(accountsActions.setSelected(''));
setSelectedLiveTab(LIVE_TAB_CHANGES);
}}
>
<small>Live</small>
<small>Program Changes</small>
</li>
</ul>
</div>
<div className="m-2">
{selectedAccountInfo ? (
<AccountView account={selectedAccountInfo} />
) : (
<ProgramChangeView
accounts={listedAccounts}
attemptAccountAdd={attemptAccountAdd}
/>
selectedLiveComponent
)}
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/types/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ export interface ChangeSubscriptionMap {
};
}

export interface LogSubscriptionMap {
[net: string]: {
subscriptionID: number;
solConn: sol.Connection;
};
}

export interface ImportedAccountMap {
[pubKey: string]: boolean;
}
Expand Down

0 comments on commit 4722889

Please sign in to comment.