Compare commits

..

11 Commits

Author SHA1 Message Date
muzam1l
9296ea1acc remove a console.log 2022-04-20 16:44:49 +05:30
muzam1l
582fb17c94 Link ledger index to explorer 2022-04-20 13:45:17 +05:30
muzam1l
aff0142870 Debug history log only after clear timestamp 2022-04-19 20:51:11 +05:30
muzamil
df51d87cb2 Merge branch 'main' into feat/improved-logs 2022-04-19 20:43:05 +05:30
muzam1l
6a46f5f173 Merge branch 'main' into feat/improved-logs 2022-04-19 18:52:13 +05:30
muzamil
9e25cefef9 Merge pull request #162 from eqlabs/feat/transaction-persistence
Persisted transactions and debug stream state.
2022-04-19 18:42:46 +05:30
muzam1l
dfe5589074 fix 2022-04-14 17:05:36 +05:30
muzam1l
cdc50da840 don't show legacy logs in debug stream 2022-04-14 16:31:19 +05:30
muzam1l
4893b41936 revert to browser time parsing only 2022-04-14 15:02:00 +05:30
muzam1l
bf21fe36c3 timestamp and log fixes 2022-04-13 21:44:24 +05:30
muzam1l
a33a3eb6e2 Background logs 2022-04-13 16:59:02 +05:30
8 changed files with 12845 additions and 664 deletions

View File

@@ -10,10 +10,18 @@ interface ISelect<T = string> {
value: T; value: T;
} }
export const streamState = proxy({ export interface IStreamState {
selectedAccount: ISelect | null;
status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number;
logs: ILog[];
socket?: WebSocket;
}
export const streamState = proxy<IStreamState>({
selectedAccount: null as ISelect | null, selectedAccount: null as ISelect | null,
status: "idle",
logs: [] as ILog[], logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
}); });
const DebugStream = () => { const DebugStream = () => {
@@ -40,33 +48,6 @@ const DebugStream = () => {
</> </>
); );
const prepareLog = useCallback((str: any): ILog => {
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const extracted = extractJSON(msg);
const timestamp = isNaN(Date.parse(tm || ""))
? tm
: new Date(tm).toLocaleTimeString();
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
return {
type: "log",
message,
timestamp,
jsonData,
defaultCollapsed: true,
};
}, []);
useEffect(() => { useEffect(() => {
const account = selectedAccount?.value; const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) { if (account && (!socket || !socket.url.endsWith(account))) {
@@ -82,6 +63,50 @@ const DebugStream = () => {
} }
}, [selectedAccount?.value, socket]); }, [selectedAccount?.value, socket]);
const onMount = useCallback(async () => {
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
const acc = streamState.selectedAccount;
const status = streamState.status;
if (status === "opened" && acc) {
// fetch the missing ones
try {
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
// TODO Remove after api sets cors properly
const res = await fetch("/api/proxy", {
method: "POST",
body: JSON.stringify({ url }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) return;
const body = await res.json();
if (!body?.logs) return;
const start = streamState.statusChangeTimestamp || 0;
streamState.logs = [];
pushLog(`Debug stream opened for account ${acc.value}`, {
type: "success",
});
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
logs.forEach(([tm, log]) => pushLog(log));
} catch (error) {
console.error(error);
}
}
}, []);
useEffect(() => {
onMount();
}, [onMount]);
useEffect(() => { useEffect(() => {
const account = selectedAccount?.value; const account = selectedAccount?.value;
const socket = streamState.socket; const socket = streamState.socket;
@@ -89,37 +114,27 @@ const DebugStream = () => {
const onOpen = () => { const onOpen = () => {
streamState.logs = []; streamState.logs = [];
streamState.logs.push({ streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success", type: "success",
message: `Debug stream opened for account ${account}`,
}); });
}; };
const onError = () => { const onError = () => {
streamState.logs.push({ pushLog("Something went wrong! Check your connection and try again.", {
type: "error", type: "error",
message: "Something went wrong! Check your connection and try again.",
}); });
}; };
const onClose = (e: CloseEvent) => { const onClose = (e: CloseEvent) => {
streamState.logs.push({ pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error", type: "error",
message: `Connection was closed. [code: ${e.code}]`,
}); });
streamState.selectedAccount = null; streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
}; };
const onMessage = (event: any) => { const onMessage = (event: any) => {
if (!event.data) return; pushLog(event.data);
const log = prepareLog(event.data);
// Filter out account_info and account_objects requests
try {
const parsed = JSON.parse(log.jsonData);
if (parsed?.id?._Request?.includes("hooks-builder-req")) {
return;
}
} catch (err) {
// Lets just skip if we cannot parse the message
}
return streamState.logs.push(log);
}; };
socket.addEventListener("open", onOpen); socket.addEventListener("open", onOpen);
@@ -133,7 +148,7 @@ const DebugStream = () => {
socket.removeEventListener("message", onMessage); socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError); socket.removeEventListener("error", onError);
}; };
}, [prepareLog, selectedAccount?.value, socket]); }, [selectedAccount?.value, socket]);
useEffect(() => { useEffect(() => {
const account = transactionsState.transactions.find( const account = transactionsState.transactions.find(
@@ -144,15 +159,59 @@ const DebugStream = () => {
streamState.selectedAccount = account; streamState.selectedAccount = account;
}, [activeTxTab]); }, [activeTxTab]);
const clearLog = () => {
streamState.logs = [];
streamState.statusChangeTimestamp = Date.now();
};
return ( return (
<LogBox <LogBox
enhanced enhanced
renderNav={renderNav} renderNav={renderNav}
title="Debug stream" title="Debug stream"
logs={logs} logs={logs}
clearLog={() => (streamState.logs = [])} clearLog={clearLog}
/> />
); );
}; };
export default DebugStream; export default DebugStream;
export const pushLog = (
str: any,
opts: Partial<Pick<ILog, "type">> = {}
): ILog | undefined => {
if (!str) return;
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const timestamp = Date.parse(tm || "") || undefined;
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString();
const extracted = extractJSON(msg);
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) {
return;
}
const { type = "log" } = opts;
const log: ILog = {
type,
message,
timestring,
jsonData,
defaultCollapsed: true,
};
if (log) streamState.logs.push(log);
return log;
};

View File

@@ -147,7 +147,7 @@ const LogBox: FC<ILogBox> = ({
export const Log: FC<ILog> = ({ export const Log: FC<ILog> = ({
type, type,
timestamp: timestamp, timestring,
message: _message, message: _message,
link, link,
linkText, linkText,
@@ -186,8 +186,17 @@ export const Log: FC<ILog> = ({
}, },
[accounts] [accounts]
); );
let message: ReactNode;
if (typeof _message === 'string') {
_message = _message.trim().replace(/\n /gi, "\n"); _message = _message.trim().replace(/\n /gi, "\n");
const message = enrichAccounts(_message); message = enrichAccounts(_message)
}
else {
message = _message
}
const jsonData = enrichAccounts(_jsonData); const jsonData = enrichAccounts(_jsonData);
return ( return (
@@ -197,9 +206,9 @@ export const Log: FC<ILog> = ({
activeAccountAddress={dialogAccount} activeAccountAddress={dialogAccount}
/> />
<LogText variant={type}> <LogText variant={type}>
{timestamp && ( {timestring && (
<Text muted monospace> <Text muted monospace>
{timestamp}{" "} {timestring}{" "}
</Text> </Text>
)} )}
<Pre>{message} </Pre> <Pre>{message} </Pre>

View File

@@ -351,6 +351,7 @@ const Transaction: FC<TransactionProps> = ({
left: 0, left: 0,
bottom: 0, bottom: 0,
width: "100%", width: "100%",
mb: "$1"
}} }}
> >
<Button outline>VIEW AS JSON</Button> <Button outline>VIEW AS JSON</Button>

12057
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
pages/api/proxy.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const { url, opts } = req.body
const r = await fetch(url, opts);
if (!r.ok) throw (r.statusText)
const data = await r.json()
return res.json(data)
} catch (error) {
console.warn(error)
return res.status(500).json({ message: "Something went wrong!" })
}
}

View File

@@ -4,19 +4,21 @@ import toast from "react-hot-toast";
import state, { IAccount } from "../index"; import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator"; import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { SetHookData } from "../../components/SetHookDialog"; import { SetHookData } from "../../components/SetHookDialog";
import { Link } from "../../components";
import { ref } from "valtio";
export const sha256 = async (string: string) => { export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string); const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8); const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0')) .map(bytes => bytes.toString(16).padStart(2, "0"))
.join(''); .join("");
return hashHex; return hashHex;
} };
function toHex(str: string) { function toHex(str: string) {
var result = ''; var result = "";
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16); result += str.charCodeAt(i).toString(16);
} }
@@ -51,7 +53,10 @@ function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
* hex string, signs the transaction and deploys it to * hex string, signs the transaction and deploys it to
* Hooks testnet. * Hooks testnet.
*/ */
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => { export const deployHook = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
if ( if (
!state.files || !state.files ||
state.files.length === 0 || state.files.length === 0 ||
@@ -69,7 +74,15 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase(); const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value); const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data; const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter(hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue)?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ''), HookParameterValue: toHex(aa.HookParameter.HookParameterValue || '') } })); const filteredHookParameters = HookParameters.filter(
hp =>
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.map(aa => ({
HookParameter: {
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
HookParameterValue: toHex(aa.HookParameter.HookParameterValue || ""),
},
}));
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => { // const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return { // return {
// HookGrant: { // HookGrant: {
@@ -97,16 +110,18 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
HookApiVersion: 0, HookApiVersion: 0,
Flags: 1, Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }), // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters }), ...(filteredHookParameters.length > 0 && {
} HookParameters: filteredHookParameters,
} }),
] },
},
],
}; };
const keypair = derive.familySeed(account.secret); const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair); const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find( const currentAccount = state.accounts.find(
(acc) => acc.address === account.address acc => acc.address === account.address
); );
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = true; currentAccount.isLoading = true;
@@ -125,12 +140,28 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
}); });
state.deployLogs.push({ state.deployLogs.push({
type: "success", type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`, message: ref(
<>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Validated ledger index:{" "}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.validated_ledger_index}`}
target="_blank"
rel="noopener noreferrer"
>
{submitRes.validated_ledger_index}
</Link>
</>
),
// message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
}); });
} else { } else {
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`, message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`,
}); });
} }
} catch (err) { } catch (err) {
@@ -152,10 +183,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
return; return;
} }
const currentAccount = state.accounts.find( const currentAccount = state.accounts.find(
(acc) => acc.address === account.address acc => acc.address === account.address
); );
if (currentAccount?.isLoading || !currentAccount?.hooks.length) { if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return return;
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const tx = { const tx = {
@@ -168,9 +199,9 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
Hook: { Hook: {
CreateCode: "", CreateCode: "",
Flags: 1, Flags: 1,
} },
} },
] ],
}; };
const keypair = derive.familySeed(account.secret); const keypair = derive.familySeed(account.secret);
@@ -188,7 +219,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}); });
if (submitRes.engine_result === "tesSUCCESS") { if (submitRes.engine_result === "tesSUCCESS") {
toast.success('Hook deleted successfully ✅', { id: toastId }) toast.success("Hook deleted successfully ✅", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: "success", type: "success",
message: "Hook deleted successfully ✅", message: "Hook deleted successfully ✅",
@@ -199,15 +230,20 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}); });
currentAccount.hooks = []; currentAccount.hooks = [];
} else { } else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { id: toastId }) toast.error(
`${submitRes.engine_result_message || submitRes.error_exception}`,
{ id: toastId }
);
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`, message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`,
}); });
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
toast.error('Error occured while deleting hoook', { id: toastId }) toast.error("Error occured while deleting hoook", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deleting hook", message: "Error occured while deleting hook",

View File

@@ -39,9 +39,10 @@ export interface IAccount {
export interface ILog { export interface ILog {
type: "error" | "warning" | "log" | "success"; type: "error" | "warning" | "log" | "success";
message: string; message: string | JSX.Element;
key?: string;
jsonData?: any, jsonData?: any,
timestamp?: string; timestring?: string;
link?: string; link?: string;
linkText?: string; linkText?: string;
defaultCollapsed?: boolean defaultCollapsed?: boolean

1168
yarn.lock

File diff suppressed because it is too large Load Diff