explorer: retain inspector signatures and show simulation error (#18178)
This commit is contained in:
parent
fc3b2f0f8d
commit
522062350b
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<div className="card">
|
||||
|
@ -42,18 +47,25 @@ export function SimulatorCard({ message }: { message: Message }) {
|
|||
Simulate
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body text-muted">
|
||||
<ul>
|
||||
<li>
|
||||
Simulation is free and will run this transaction against the
|
||||
latest confirmed ledger state.
|
||||
</li>
|
||||
<li>
|
||||
No state changes will be persisted and all signature checks will
|
||||
be disabled.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{simulationError ? (
|
||||
<div className="card-body">
|
||||
Failed to run simulation:
|
||||
<span className="text-warning ml-2">{simulationError}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-body text-muted">
|
||||
<ul>
|
||||
<li>
|
||||
Simulation is free and will run this transaction against the
|
||||
latest confirmed ledger state.
|
||||
</li>
|
||||
<li>
|
||||
No state changes will be persisted and all signature checks will
|
||||
be disabled.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -114,14 +126,17 @@ function useSimulator(message: Message) {
|
|||
const { cluster, url } = useCluster();
|
||||
const [simulating, setSimulating] = React.useState(false);
|
||||
const [logs, setLogs] = React.useState<Array<InstructionLogs> | null>(null);
|
||||
const [error, setError] = React.useState<string>();
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue