diff --git a/components/DebugStream.tsx b/components/DebugStream.tsx index a95c2dd..4a3057e 100644 --- a/components/DebugStream.tsx +++ b/components/DebugStream.tsx @@ -5,6 +5,7 @@ import { subscribeKey } from "valtio/utils"; import { Select } from "."; import state, { ILog, transactionsState } from "../state"; import { extractJSON } from "../utils/json"; +import EnrichAccounts from "./EnrichAccounts"; import LogBox from "./LogBox"; interface ISelect { @@ -99,12 +100,17 @@ const addListeners = (account: ISelect | null) => { subscribeKey(streamState, "selectedAccount", addListeners); +const clearLog = () => { + streamState.logs = []; + streamState.statusChangeTimestamp = Date.now(); +}; + const DebugStream = () => { const { selectedAccount, logs } = useSnapshot(streamState); const { activeHeader: activeTxTab } = useSnapshot(transactionsState); const { accounts } = useSnapshot(state); - const accountOptions = accounts.map((acc) => ({ + const accountOptions = accounts.map(acc => ({ label: acc.name, value: acc.address, })); @@ -117,7 +123,7 @@ const DebugStream = () => { options={accountOptions} hideSelectedOptions value={selectedAccount} - onChange={(acc) => { + onChange={acc => { streamState.socket?.close( 4999, "Old connection closed because user switched account" @@ -131,18 +137,13 @@ const DebugStream = () => { useEffect(() => { const account = transactionsState.transactions.find( - (tx) => tx.header === activeTxTab + tx => tx.header === activeTxTab )?.state.selectedAccount; if (account && account.value !== streamState.selectedAccount?.value) streamState.selectedAccount = account; }, [activeTxTab]); - const clearLog = () => { - streamState.logs = []; - streamState.statusChangeTimestamp = Date.now(); - }; - return ( ); - const jsonData = extracted + const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined; + const jsonData = _jsonData + ? ref() + : undefined; if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) { return; diff --git a/components/EnrichAccounts.tsx b/components/EnrichAccounts.tsx new file mode 100644 index 0000000..e0cd129 --- /dev/null +++ b/components/EnrichAccounts.tsx @@ -0,0 +1,48 @@ +import { FC, useState } from "react"; +import regexifyString from "regexify-string"; +import { useSnapshot } from "valtio"; +import { Link } from "."; +import state from "../state"; +import { AccountDialog } from "./Accounts"; + +interface EnrichAccountsProps { + str?: string; +} + +const EnrichAccounts: FC = ({ str }) => { + const { accounts } = useSnapshot(state); + const [dialogAccount, setDialogAccount] = useState(null); + if (!str || !accounts.length) return <>{str}; + + 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 ( + setDialogAccount(match)} + title={match} + highlighted + > + {name || match} + + ); + }, + input: str, + }); + + return ( + <> + {res} + + + ); +}; + +export default EnrichAccounts; diff --git a/components/LogBox.tsx b/components/LogBox.tsx index 1411cb4..e3c3300 100644 --- a/components/LogBox.tsx +++ b/components/LogBox.tsx @@ -1,22 +1,12 @@ -import { - useRef, - useLayoutEffect, - ReactNode, - FC, - useState, - useCallback, -} from "react"; +import { useRef, useLayoutEffect, ReactNode, FC, useState } from "react"; import { IconProps, Notepad, Prohibit } from "phosphor-react"; import useStayScrolled from "react-stay-scrolled"; import NextLink from "next/link"; import Container from "./Container"; import LogText from "./LogText"; -import state, { ILog } from "../state"; +import { ILog } from "../state"; import { Pre, Link, Heading, Button, Text, Flex, Box } from "."; -import regexifyString from "regexify-string"; -import { useSnapshot } from "valtio"; -import { AccountDialog } from "./Accounts"; interface ILogBox { title: string; @@ -150,70 +140,24 @@ const LogBox: FC = ({ export const Log: FC = ({ type, timestring, - message: _message, + message, link, linkText, defaultCollapsed, - jsonData: _jsonData, + jsonData, }) => { const [expanded, setExpanded] = useState(!defaultCollapsed); - const { accounts } = useSnapshot(state); - const [dialogAccount, setDialogAccount] = useState(null); - - const enrichAccounts = useCallback( - (str?: string): ReactNode => { - if (!str || !accounts.length) return str; - - 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 ( - setDialogAccount(match)} - title={match} - highlighted - > - {name || match} - - ); - }, - input: str, - }); - - return <>{res}; - }, - [accounts] - ); - - let message: ReactNode; - - if (typeof _message === "string") { - _message = _message.trim().replace(/\n /gi, "\n"); - if (_message) message = enrichAccounts(_message); - else message = {'""'} - } else { - message = _message; - } - - const jsonData = enrichAccounts(_jsonData); + if (!message) message = {'""'}; return ( <> - {timestring && ( {timestring}{" "} )} -
{message} 
+
{message}
{link && ( {linkText} @@ -226,7 +170,6 @@ export const Log: FC = ({ )} {expanded && jsonData &&
{jsonData}
}
-
); }; diff --git a/components/ResultLink.tsx b/components/ResultLink.tsx new file mode 100644 index 0000000..d6610bb --- /dev/null +++ b/components/ResultLink.tsx @@ -0,0 +1,23 @@ +import { FC } from "react"; +import { Link } from "."; + +interface Props { + result?: string; +} + +const ResultLink: FC = ({ result }) => { + if (!result) return null; + return ( + + {result} + + ); +}; + +export default ResultLink; diff --git a/state/actions/deployHook.tsx b/state/actions/deployHook.tsx index 8653782..95f8858 100644 --- a/state/actions/deployHook.tsx +++ b/state/actions/deployHook.tsx @@ -6,14 +6,15 @@ import calculateHookOn, { TTS } from "../../utils/hookOnCalculator"; import { Link } from "../../components"; import { ref } from "valtio"; import estimateFee from "../../utils/estimateFee"; -import { SetHookData } from '../../utils/setHook'; +import { SetHookData } from "../../utils/setHook"; +import ResultLink from "../../components/ResultLink"; export const sha256 = async (string: string) => { const utf8 = new TextEncoder().encode(string); const hashBuffer = await crypto.subtle.digest("SHA-256", utf8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray - .map((bytes) => bytes.toString(16).padStart(2, "0")) + .map(bytes => bytes.toString(16).padStart(2, "0")) .join(""); return hashHex; }; @@ -56,7 +57,7 @@ export const prepareDeployHookTx = async ( ) => { const activeFile = state.files[state.active]?.compiledContent ? state.files[state.active] - : state.files.filter((file) => file.compiledContent)[0]; + : state.files.filter(file => file.compiledContent)[0]; if (!state.files || state.files.length === 0) { return; @@ -69,12 +70,12 @@ export const prepareDeployHookTx = async ( return; } 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 filteredHookParameters = HookParameters.filter( - (hp) => + hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue - )?.map((aa) => ({ + )?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ""), HookParameterValue: aa.HookParameter.HookParameterValue || "", @@ -128,7 +129,7 @@ export const deployHook = async ( if (typeof window !== "undefined") { const activeFile = state.files[state.active]?.compiledContent ? state.files[state.active] - : state.files.filter((file) => file.compiledContent)[0]; + : state.files.filter(file => file.compiledContent)[0]; state.deployValues[activeFile.name] = data; const tx = await prepareDeployHookTx(account, data); if (!tx) { @@ -141,7 +142,7 @@ export const deployHook = async ( const { signedTransaction } = sign(tx, keypair); const currentAccount = state.accounts.find( - (acc) => acc.address === account.address + acc => acc.address === account.address ); if (currentAccount) { currentAccount.isLoading = true; @@ -154,6 +155,26 @@ export const deployHook = async ( tx_blob: signedTransaction, }); + const txHash = submitRes.tx_json?.hash; + const resultMsg = ref( + <> + []{" "} + {submitRes.engine_result_message}{" "} + {txHash && ( + <> + Transaction hash:{" "} + + {txHash} + + + )} + + ); if (submitRes.engine_result === "tesSUCCESS") { state.deployLogs.push({ type: "success", @@ -161,28 +182,17 @@ export const deployHook = async ( }); state.deployLogs.push({ type: "success", - message: ref( - <> - [{submitRes.engine_result}] {submitRes.engine_result_message}{" "} - Transaction hash:{" "} - - {submitRes.tx_json?.hash} - - - ), - // message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`, + message: resultMsg, + }); + } else if (submitRes.engine_result) { + state.deployLogs.push({ + type: "error", + message: resultMsg, }); } else { state.deployLogs.push({ type: "error", - message: `[${submitRes.engine_result || submitRes.error}] ${ - submitRes.engine_result_message || submitRes.error_exception - }`, + message: `[${submitRes.error}] ${submitRes.error_exception}`, }); } } catch (err) { @@ -204,7 +214,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => { return; } const currentAccount = state.accounts.find( - (acc) => acc.address === account.address + acc => acc.address === account.address ); if (currentAccount?.isLoading || !currentAccount?.hooks.length) { return; diff --git a/state/actions/sendTransaction.ts b/state/actions/sendTransaction.ts deleted file mode 100644 index 54ea1b6..0000000 --- a/state/actions/sendTransaction.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { derive, sign } from "xrpl-accountlib"; - -import state from '..' -import type { IAccount } from ".."; - -interface TransactionOptions { - TransactionType: string, - Account?: string, - Fee?: string, - Destination?: string - [index: string]: any -} -interface OtherOptions { - logPrefix?: string -} - -export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => { - if (!state.client) throw Error('XRPL client not initalized') - - const { Fee = "1000", ...opts } = txOptions - const tx: TransactionOptions = { - Account: account.address, - Sequence: account.sequence, - Fee, // TODO auto-fillable default - ...opts - }; - const { logPrefix = '' } = options || {} - try { - const signedAccount = derive.familySeed(account.secret); - const { signedTransaction } = sign(tx, signedAccount); - const response = await state.client.send({ - command: "submit", - tx_blob: signedTransaction, - }); - if (response.engine_result === "tesSUCCESS") { - state.transactionLogs.push({ - type: 'success', - message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}` - }) - } else { - state.transactionLogs.push({ - type: "error", - message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`, - }); - } - const currAcc = state.accounts.find(acc => acc.address === account.address); - if (currAcc && response.account_sequence_next) { - currAcc.sequence = response.account_sequence_next; - } - } catch (err) { - console.error(err); - state.transactionLogs.push({ - type: "error", - message: err instanceof Error ? `${logPrefix}Error: ${err.message}` : `${logPrefix}Something went wrong, try again later`, - }); - } -}; \ No newline at end of file diff --git a/state/actions/sendTransaction.tsx b/state/actions/sendTransaction.tsx new file mode 100644 index 0000000..34e1d6c --- /dev/null +++ b/state/actions/sendTransaction.tsx @@ -0,0 +1,78 @@ +import { derive, sign } from "xrpl-accountlib"; + +import state from ".."; +import type { IAccount } from ".."; +import ResultLink from "../../components/ResultLink"; +import { ref } from "valtio"; + +interface TransactionOptions { + TransactionType: string; + Account?: string; + Fee?: string; + Destination?: string; + [index: string]: any; +} +interface OtherOptions { + logPrefix?: string; +} + +export const sendTransaction = async ( + account: IAccount, + txOptions: TransactionOptions, + options?: OtherOptions +) => { + if (!state.client) throw Error("XRPL client not initalized"); + + const { Fee = "1000", ...opts } = txOptions; + const tx: TransactionOptions = { + Account: account.address, + Sequence: account.sequence, + Fee, // TODO auto-fillable default + ...opts, + }; + const { logPrefix = "" } = options || {}; + try { + const signedAccount = derive.familySeed(account.secret); + const { signedTransaction } = sign(tx, signedAccount); + const response = await state.client.send({ + command: "submit", + tx_blob: signedTransaction, + }); + + const resultMsg = ref( + <> + {logPrefix}[]{" "} + {response.engine_result_message} + + ); + if (response.engine_result === "tesSUCCESS") { + state.transactionLogs.push({ + type: "success", + message: resultMsg, + }); + } else if (response.engine_result) { + state.transactionLogs.push({ + type: "error", + message: resultMsg, + }); + } else { + state.transactionLogs.push({ + type: "error", + message: `${logPrefix}[${response.error}] ${response.error_exception}`, + }); + } + const currAcc = state.accounts.find(acc => acc.address === account.address); + if (currAcc && response.account_sequence_next) { + currAcc.sequence = response.account_sequence_next; + } + } catch (err) { + console.error(err); + state.transactionLogs.push({ + type: "error", + message: + err instanceof Error + ? `${logPrefix}Error: ${err.message}` + : `${logPrefix}Something went wrong, try again later`, + }); + } +};