Compare commits

..

1 Commits

Author SHA1 Message Date
Valtteri Karesto
09f58f18ae Increment sequence on every transaction 2022-03-08 13:16:45 +02:00
12 changed files with 65 additions and 246 deletions

View File

@@ -27,7 +27,7 @@ const labelStyle = css({
mb: "$0.5", mb: "$0.5",
}); });
export const AccountDialog = ({ const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
setActiveAccountAddress, setActiveAccountAddress,
}: { }: {

View File

@@ -1,136 +1,84 @@
import { useCallback, useEffect } from "react"; import { useEffect, useState } from "react";
import { proxy, ref, useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { Select } from "."; import { Select } from ".";
import state, { ILog } from "../state"; import state from "../state";
import { extractJSON } from "../utils/json";
import LogBox from "./LogBox"; import LogBox from "./LogBox";
import Text from "./Text";
interface ISelect<T = string> {
label: string;
value: T;
}
const streamState = proxy({
selectedAccount: null as ISelect | null,
logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
});
const DebugStream = () => { const DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState); const snap = useSnapshot(state);
const { accounts } = useSnapshot(state);
const accountOptions = accounts.map(acc => ({ const accountOptions = snap.accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})); }));
const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
const renderNav = () => ( const renderNav = () => (
<> <>
<Text css={{ mx: "$2", fontSize: "inherit" }}>Account: </Text>
<Select <Select
instanceId="DSAccount" instanceId="debugStreamAccount"
placeholder="Select account" placeholder="Select account"
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} value={selectedAccount}
onChange={acc => (streamState.selectedAccount = acc as any)} onChange={acc => setSelectedAccount(acc as any)}
css={{ width: "100%" }} css={{ width: "30%" }}
/> />
</> </>
); );
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?.close(); return;
streamState.socket = ref(
new WebSocket(
`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
} }
}, [selectedAccount?.value, socket]); const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => { const onOpen = () => {
streamState.logs = []; state.debugLogs = [];
streamState.logs.push({ state.debugLogs.push({
type: "success", type: "success",
message: `Debug stream opened for account ${account}`, message: `Debug stream opened for account ${account}`,
}); });
}; };
const onError = () => { const onError = () => {
streamState.logs.push({ state.debugLogs.push({
type: "error", type: "error",
message: "Something went wrong! Check your connection and try again.", message: "Something went wrong in establishing connection!",
}); });
}; setSelectedAccount(null);
const onClose = (e: CloseEvent) => {
streamState.logs.push({
type: "error",
message: `Connection was closed. [code: ${e.code}]`,
});
streamState.selectedAccount = null;
}; };
const onMessage = (event: any) => { const onMessage = (event: any) => {
if (!event.data) return; if (!event.data) return;
streamState.logs.push(prepareLog(event.data)); state.debugLogs.push({
type: "log",
message: event.data,
});
}; };
socket.addEventListener("open", onOpen); socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose); socket.addEventListener("close", onError);
socket.addEventListener("error", onError); socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage); socket.addEventListener("message", onMessage);
return () => { return () => {
socket.removeEventListener("open", onOpen); socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose); socket.removeEventListener("close", onError);
socket.removeEventListener("message", onMessage); socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
socket.close();
}; };
}, [prepareLog, selectedAccount?.value, socket]); }, [selectedAccount]);
return ( return (
<LogBox <LogBox
enhanced enhanced
renderNav={renderNav} renderNav={renderNav}
title="Debug stream" title="Debug stream"
logs={logs} logs={snap.debugLogs}
clearLog={() => (streamState.logs = [])} clearLog={() => (state.debugLogs = [])}
/> />
); );
}; };

View File

@@ -3,14 +3,6 @@ import { styled } from "../stitches.config";
const StyledLink = styled("a", { const StyledLink = styled("a", {
color: "CurrentColor", color: "CurrentColor",
textDecoration: "underline", textDecoration: "underline",
cursor: 'pointer',
variants: {
highlighted: {
true: {
color: '$blue9'
}
}
}
}); });
export default StyledLink; export default StyledLink;

View File

@@ -1,15 +1,17 @@
import { useRef, useLayoutEffect, ReactNode, FC, useState, useCallback } from "react"; import React, { useRef, useLayoutEffect, ReactNode } from "react";
import { Notepad, Prohibit } from "phosphor-react"; import { Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled"; import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link"; import NextLink from "next/link";
import Container from "./Container"; import Container from "./Container";
import Box from "./Box";
import Flex from "./Flex";
import LogText from "./LogText"; import LogText from "./LogText";
import state, { ILog } from "../state"; import { ILog } from "../state";
import { Pre, Link, Heading, Button, Text, Flex, Box } from "."; import Text from "./Text";
import regexifyString from "regexify-string"; import Button from "./Button";
import { useSnapshot } from "valtio"; import Heading from "./Heading";
import { AccountDialog } from "./Accounts"; import Link from "./Link";
interface ILogBox { interface ILogBox {
title: string; title: string;
@@ -19,7 +21,14 @@ interface ILogBox {
enhanced?: boolean; enhanced?: boolean;
} }
const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => { const LogBox: React.FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
}) => {
const logRef = useRef<HTMLPreElement>(null); const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef); const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -46,7 +55,6 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
}} }}
> >
<Flex <Flex
fluid
css={{ css={{
height: "48px", height: "48px",
alignItems: "center", alignItems: "center",
@@ -70,15 +78,7 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
> >
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text> <Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading> </Heading>
<Flex {renderNav?.()}
row
align="center"
css={{
width: "50%", // TODO make it max without breaking layout!
}}
>
{renderNav?.()}
</Flex>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}> <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && ( {clearLog && (
<Button ghost size="xs" onClick={clearLog}> <Button ghost size="xs" onClick={clearLog}>
@@ -117,11 +117,17 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
backgroundColor: enhanced ? "$backgroundAlt" : undefined, backgroundColor: enhanced ? "$backgroundAlt" : undefined,
}, },
}, },
p: enhanced ? "$1" : undefined, p: enhanced ? "$2 $1" : undefined,
my: enhanced ? "$1" : undefined,
}} }}
> >
<Log {...log} /> <LogText variant={log.type}>
{log.message}{" "}
{log.link && (
<NextLink href={log.link} shallow passHref>
<Link as="a">{log.linkText}</Link>
</NextLink>
)}
</LogText>
</Box> </Box>
))} ))}
{children} {children}
@@ -131,74 +137,4 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
); );
}; };
export const Log: FC<ILog> = ({
type,
timestamp: timestamp,
message: _message,
link,
linkText,
defaultCollapsed,
jsonData: _jsonData,
}) => {
const [expanded, setExpanded] = useState(!defaultCollapsed);
const { accounts } = useSnapshot(state);
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
const enrichAccounts = useCallback(
(str?: string): ReactNode => {
if (!str || !accounts.length) return null;
const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({
pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => {
const name = accounts.find(acc => acc.address === match)?.name;
return (
<Link
key={match + idx}
as="a"
onClick={() => setDialogAccount(match)}
title={match}
highlighted
>
{name || match}
</Link>
);
},
input: str,
});
return <>{res}</>;
},
[accounts]
);
_message = _message.trim().replace(/\n /gi, "\n");
const message = enrichAccounts(_message);
const jsonData = enrichAccounts(_jsonData);
return (
<>
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestamp && <Text muted monospace>{timestamp} </Text>}
<Pre>{message} </Pre>
{link && (
<NextLink href={link} shallow passHref>
<Link as="a">{linkText}</Link>
</NextLink>
)}
{jsonData && (
<Link onClick={() => setExpanded(!expanded)} as="a">
{expanded ? "Collapse" : "Expand"}
</Link>
)}
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
</LogText>
</>
);
};
export default LogBox; export default LogBox;

View File

@@ -1,27 +0,0 @@
import { styled } from "../stitches.config";
const Pre = styled("span", {
m: 0,
wordBreak: "break-all",
fontFamily: '$monospace',
whiteSpace: 'pre-wrap',
variants: {
fluid: {
true: {
width: "100%",
},
},
line: {
true: {
whiteSpace: 'pre-line'
}
},
block: {
true: {
display: 'block'
}
}
},
});
export default Pre;

View File

@@ -14,11 +14,6 @@ const Text = styled("span", {
true: { true: {
color: '$mauve9' color: '$mauve9'
} }
},
monospace: {
true: {
fontFamily: '$monospace'
}
} }
} }
}); });

View File

@@ -10,7 +10,6 @@ export * from "./Tabs";
export * from "./AlertDialog"; export * from "./AlertDialog";
export { default as Box } from "./Box"; export { default as Box } from "./Box";
export { default as Button } from "./Button"; export { default as Button } from "./Button";
export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup"; export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter"; export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog"; export * from "./Dialog";

View File

@@ -47,7 +47,6 @@
"react-stay-scrolled": "^7.4.0", "react-stay-scrolled": "^7.4.0",
"react-time-ago": "^7.1.9", "react-time-ago": "^7.1.9",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"regexify-string": "^1.0.17",
"valtio": "^1.2.5", "valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",

View File

@@ -24,6 +24,10 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
Fee, // TODO auto-fillable Fee, // TODO auto-fillable
...opts ...opts
}; };
const currAcc = state.accounts.find(acc => acc.address === account.address);
if (currAcc) {
currAcc.sequence = account.sequence + 1;
}
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
try { try {
const signedAccount = derive.familySeed(account.secret); const signedAccount = derive.familySeed(account.secret);

View File

@@ -33,11 +33,8 @@ export interface IAccount {
export interface ILog { export interface ILog {
type: "error" | "warning" | "log" | "success"; type: "error" | "warning" | "log" | "success";
message: string; message: string;
jsonData?: any,
timestamp?: string;
link?: string; link?: string;
linkText?: string; linkText?: string;
defaultCollapsed?: boolean
} }
export interface IState { export interface IState {
@@ -54,6 +51,7 @@ export interface IState {
logs: ILog[]; logs: ILog[];
deployLogs: ILog[]; deployLogs: ILog[];
transactionLogs: ILog[]; transactionLogs: ILog[];
debugLogs: ILog[];
editorCtx?: typeof monaco.editor; editorCtx?: typeof monaco.editor;
editorSettings: { editorSettings: {
tabSize: number; tabSize: number;
@@ -76,6 +74,7 @@ let initialState: IState = {
logs: [], logs: [],
deployLogs: [], deployLogs: [],
transactionLogs: [], transactionLogs: [],
debugLogs: [],
editorCtx: undefined, editorCtx: undefined,
gistId: undefined, gistId: undefined,
gistOwner: undefined, gistOwner: undefined,

View File

@@ -1,21 +0,0 @@
export const extractJSON = (str?: string) => {
if (!str) return
let firstOpen = 0, firstClose = 0, candidate = '';
firstOpen = str.indexOf('{', firstOpen + 1);
do {
firstClose = str.lastIndexOf('}');
if (firstClose <= firstOpen) {
return;
}
do {
candidate = str.substring(firstOpen, firstClose + 1);
try {
let result = JSON.parse(candidate);
return { result, start: firstOpen < 0 ? 0 : firstOpen, end: firstClose }
}
catch (e) { }
firstClose = str.substring(0, firstClose).lastIndexOf('}');
} while (firstClose > firstOpen);
firstOpen = str.indexOf('{', firstOpen + 1);
} while (firstOpen != -1);
}

View File

@@ -3989,11 +3989,6 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regexify-string@^1.0.17:
version "1.0.17"
resolved "https://registry.yarnpkg.com/regexify-string/-/regexify-string-1.0.17.tgz#b9e571b51c8ec566eb82b7121744dae0d8e829de"
integrity sha512-mmD0AUNaY/piGW2AyACWdQOjIAwNuWz+KIvxfBZPDdCBAexiROeQxdxTaYAWcIxwtUAOwojdTta6CMMil84jXw==
regexp.prototype.flags@^1.3.1: regexp.prototype.flags@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"