From 522062350b5358973c77919b4d1b6a43d30a36f6 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 23 Jun 2021 16:00:49 -0500 Subject: [PATCH] explorer: retain inspector signatures and show simulation error (#18178) --- .../src/pages/inspector/InspectorPage.tsx | 166 ++++++++++++++---- explorer/src/pages/inspector/RawInputCard.tsx | 2 +- .../src/pages/inspector/SimulatorCard.tsx | 61 +++++-- 3 files changed, 174 insertions(+), 55 deletions(-) diff --git a/explorer/src/pages/inspector/InspectorPage.tsx b/explorer/src/pages/inspector/InspectorPage.tsx index 25720365f..aac5c5542 100644 --- a/explorer/src/pages/inspector/InspectorPage.tsx +++ b/explorer/src/pages/inspector/InspectorPage.tsx @@ -19,8 +19,9 @@ import { createFeePayerValidator, } from "./AddressWithContext"; import { SimulatorCard } from "./SimulatorCard"; -import { RawInput } from "./RawInputCard"; +import { MIN_MESSAGE_LENGTH, RawInput } from "./RawInputCard"; import { InstructionsSection } from "./InstructionsSection"; +import base58 from "bs58"; export type TransactionData = { rawMessage: Uint8Array; @@ -28,6 +29,108 @@ export type TransactionData = { signatures?: (string | null)[]; }; +// Decode a url param and return the result. If decoding fails, return whether +// the param should be deleted. +function decodeParam(params: URLSearchParams, name: string): string | boolean { + const param = params.get(name); + if (param === null) return false; + try { + return decodeURIComponent(param); + } catch (err) { + return true; + } +} + +// Decode a signatures param and throw an error on failure +function decodeSignatures(signaturesParam: string): (string | null)[] { + let signatures; + try { + signatures = JSON.parse(signaturesParam); + } catch (err) { + throw new Error("Signatures param is not valid JSON"); + } + + if (!Array.isArray(signatures)) { + throw new Error("Signatures param is not a JSON array"); + } + + const validSignatures: (string | null)[] = []; + for (const signature of signatures) { + if (signature === null) { + validSignatures.push(signature); + continue; + } + + if (typeof signature !== "string") { + throw new Error("Signature is not a string"); + } + + try { + base58.decode(signature); + validSignatures.push(signature); + } catch (err) { + throw new Error("Signature is not valid base58"); + } + } + + return validSignatures; +} + +// Decodes url params into transaction data if possible. If decoding fails, +// URL params are returned as a string that will prefill the transaction +// message input field for debugging. Returns a tuple of [result, shouldRefreshUrl] +function decodeUrlParams( + params: URLSearchParams +): [TransactionData | string, boolean] { + const messageParam = decodeParam(params, "message"); + const signaturesParam = decodeParam(params, "signatures"); + + let refreshUrl = false; + if (signaturesParam === true) { + params.delete("signatures"); + refreshUrl = true; + } + + if (typeof messageParam === "boolean") { + if (messageParam) { + params.delete("message"); + params.delete("signatures"); + refreshUrl = true; + } + return ["", refreshUrl]; + } + + let signatures: (string | null)[] | undefined = undefined; + if (typeof signaturesParam === "string") { + try { + signatures = decodeSignatures(signaturesParam); + } catch (err) { + params.delete("signatures"); + refreshUrl = true; + } + } + + try { + const buffer = Uint8Array.from(atob(messageParam), (c) => c.charCodeAt(0)); + + if (buffer.length < MIN_MESSAGE_LENGTH) { + throw new Error("message buffer is too short"); + } + + const message = Message.from(buffer); + const data = { + message, + rawMessage: buffer, + signatures, + }; + return [data, refreshUrl]; + } catch (err) { + params.delete("message"); + refreshUrl = true; + return [messageParam, true]; + } +} + export function TransactionInspectorPage({ signature, }: { @@ -43,13 +146,30 @@ export function TransactionInspectorPage({ React.useEffect(() => { if (signature) return; if (transaction) { + let shouldRefreshUrl = false; + + if (transaction.signatures !== undefined) { + const signaturesParam = encodeURIComponent( + JSON.stringify(transaction.signatures) + ); + if (query.get("signatures") !== signaturesParam) { + shouldRefreshUrl = true; + query.set("signatures", signaturesParam); + } + } + const base64 = btoa( String.fromCharCode.apply(null, [...transaction.rawMessage]) ); const newParam = encodeURIComponent(base64); - if (query.get("message") === newParam) return; - query.set("message", newParam); - history.push({ ...location, search: query.toString() }); + if (query.get("message") !== newParam) { + shouldRefreshUrl = true; + query.set("message", newParam); + } + + if (shouldRefreshUrl) { + history.push({ ...location, search: query.toString() }); + } } }, [query, transaction, signature, history, location]); @@ -63,39 +183,15 @@ export function TransactionInspectorPage({ React.useEffect(() => { if (transaction || signature) return; - let messageParam = query.get("message"); - if (messageParam !== null) { - let messageString; - try { - messageString = decodeURIComponent(messageParam); - } catch (err) { - query.delete("message"); - history.push({ ...location, search: query.toString() }); - return; - } + const [result, refreshUrl] = decodeUrlParams(query); + if (refreshUrl) { + history.push({ ...location, search: query.toString() }); + } - try { - const buffer = Uint8Array.from(atob(messageString), (c) => - c.charCodeAt(0) - ); - - if (buffer.length < 36) { - query.delete("message"); - history.push({ ...location, search: query.toString() }); - throw new Error("buffer is too short"); - } - - const message = Message.from(buffer); - setParamString(undefined); - setTransaction({ - message, - rawMessage: buffer, - }); - } catch (err) { - setParamString(messageString); - } + if (typeof result === "string") { + setParamString(result); } else { - setParamString(undefined); + setTransaction(result); } }, [query, transaction, signature, history, location]); diff --git a/explorer/src/pages/inspector/RawInputCard.tsx b/explorer/src/pages/inspector/RawInputCard.tsx index 9d1ba21e1..21f78bba7 100644 --- a/explorer/src/pages/inspector/RawInputCard.tsx +++ b/explorer/src/pages/inspector/RawInputCard.tsx @@ -33,7 +33,7 @@ function deserializeTransaction(bytes: Uint8Array): { return { message, signatures }; } -const MIN_MESSAGE_LENGTH = +export const MIN_MESSAGE_LENGTH = 3 + // header 1 + // accounts length 32 + // accounts, must have at least one address for fees diff --git a/explorer/src/pages/inspector/SimulatorCard.tsx b/explorer/src/pages/inspector/SimulatorCard.tsx index aba5e99dc..18021305d 100644 --- a/explorer/src/pages/inspector/SimulatorCard.tsx +++ b/explorer/src/pages/inspector/SimulatorCard.tsx @@ -20,7 +20,12 @@ const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0)); export function SimulatorCard({ message }: { message: Message }) { const { cluster } = useCluster(); - const { simulate, simulating, simulationLogs: logs } = useSimulator(message); + const { + simulate, + simulating, + simulationLogs: logs, + simulationError, + } = useSimulator(message); if (simulating) { return (
@@ -42,18 +47,25 @@ export function SimulatorCard({ message }: { message: Message }) { Simulate
-
- -
+ {simulationError ? ( +
+ Failed to run simulation: + {simulationError} +
+ ) : ( +
+ +
+ )} ); } @@ -114,14 +126,17 @@ function useSimulator(message: Message) { const { cluster, url } = useCluster(); const [simulating, setSimulating] = React.useState(false); const [logs, setLogs] = React.useState | null>(null); + const [error, setError] = React.useState(); React.useEffect(() => { setLogs(null); setSimulating(false); + setError(undefined); }, [url]); const onClick = React.useCallback(() => { if (simulating) return; + setError(undefined); setSimulating(true); const connection = new Connection(url, "confirmed"); @@ -146,17 +161,19 @@ function useSimulator(message: Message) { let instructionError; const responseLogs = resp.value.logs; + const responseErr = resp.value.err; if (!responseLogs) { - if (resp.value.err) throw new Error(JSON.stringify(resp.value.err)); + if (resp.value.err) throw new Error(JSON.stringify(responseErr)); throw new Error("No logs detected"); - } else if (resp.value.err) { - const err = resp.value.err; - if (err && typeof err !== "string") { - let ixError = (err as any)["InstructionError"]; + } else if (responseErr) { + if (typeof responseErr !== "string") { + let ixError = (responseErr as any)["InstructionError"]; const [index, message] = ixError; if (typeof message === "string") { instructionError = { index, message }; } + } else { + throw new Error(responseErr); } } @@ -240,10 +257,16 @@ function useSimulator(message: Message) { } catch (err) { console.error(err); setLogs(null); + setError(err.message); } finally { setSimulating(false); } })(); }, [cluster, url, message, simulating]); - return { simulate: onClick, simulating, simulationLogs: logs }; + return { + simulate: onClick, + simulating, + simulationLogs: logs, + simulationError: error, + }; }