explorer: retain inspector signatures and show simulation error (#18178)
This commit is contained in:
parent
fc3b2f0f8d
commit
522062350b
|
@ -19,8 +19,9 @@ import {
|
||||||
createFeePayerValidator,
|
createFeePayerValidator,
|
||||||
} from "./AddressWithContext";
|
} from "./AddressWithContext";
|
||||||
import { SimulatorCard } from "./SimulatorCard";
|
import { SimulatorCard } from "./SimulatorCard";
|
||||||
import { RawInput } from "./RawInputCard";
|
import { MIN_MESSAGE_LENGTH, RawInput } from "./RawInputCard";
|
||||||
import { InstructionsSection } from "./InstructionsSection";
|
import { InstructionsSection } from "./InstructionsSection";
|
||||||
|
import base58 from "bs58";
|
||||||
|
|
||||||
export type TransactionData = {
|
export type TransactionData = {
|
||||||
rawMessage: Uint8Array;
|
rawMessage: Uint8Array;
|
||||||
|
@ -28,6 +29,108 @@ export type TransactionData = {
|
||||||
signatures?: (string | null)[];
|
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({
|
export function TransactionInspectorPage({
|
||||||
signature,
|
signature,
|
||||||
}: {
|
}: {
|
||||||
|
@ -43,14 +146,31 @@ export function TransactionInspectorPage({
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (signature) return;
|
if (signature) return;
|
||||||
if (transaction) {
|
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(
|
const base64 = btoa(
|
||||||
String.fromCharCode.apply(null, [...transaction.rawMessage])
|
String.fromCharCode.apply(null, [...transaction.rawMessage])
|
||||||
);
|
);
|
||||||
const newParam = encodeURIComponent(base64);
|
const newParam = encodeURIComponent(base64);
|
||||||
if (query.get("message") === newParam) return;
|
if (query.get("message") !== newParam) {
|
||||||
|
shouldRefreshUrl = true;
|
||||||
query.set("message", newParam);
|
query.set("message", newParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRefreshUrl) {
|
||||||
history.push({ ...location, search: query.toString() });
|
history.push({ ...location, search: query.toString() });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [query, transaction, signature, history, location]);
|
}, [query, transaction, signature, history, location]);
|
||||||
|
|
||||||
const reset = React.useCallback(() => {
|
const reset = React.useCallback(() => {
|
||||||
|
@ -63,39 +183,15 @@ export function TransactionInspectorPage({
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (transaction || signature) return;
|
if (transaction || signature) return;
|
||||||
|
|
||||||
let messageParam = query.get("message");
|
const [result, refreshUrl] = decodeUrlParams(query);
|
||||||
if (messageParam !== null) {
|
if (refreshUrl) {
|
||||||
let messageString;
|
|
||||||
try {
|
|
||||||
messageString = decodeURIComponent(messageParam);
|
|
||||||
} catch (err) {
|
|
||||||
query.delete("message");
|
|
||||||
history.push({ ...location, search: query.toString() });
|
history.push({ ...location, search: query.toString() });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (typeof result === "string") {
|
||||||
const buffer = Uint8Array.from(atob(messageString), (c) =>
|
setParamString(result);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setParamString(undefined);
|
setTransaction(result);
|
||||||
}
|
}
|
||||||
}, [query, transaction, signature, history, location]);
|
}, [query, transaction, signature, history, location]);
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ function deserializeTransaction(bytes: Uint8Array): {
|
||||||
return { message, signatures };
|
return { message, signatures };
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_MESSAGE_LENGTH =
|
export const MIN_MESSAGE_LENGTH =
|
||||||
3 + // header
|
3 + // header
|
||||||
1 + // accounts length
|
1 + // accounts length
|
||||||
32 + // accounts, must have at least one address for fees
|
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 }) {
|
export function SimulatorCard({ message }: { message: Message }) {
|
||||||
const { cluster } = useCluster();
|
const { cluster } = useCluster();
|
||||||
const { simulate, simulating, simulationLogs: logs } = useSimulator(message);
|
const {
|
||||||
|
simulate,
|
||||||
|
simulating,
|
||||||
|
simulationLogs: logs,
|
||||||
|
simulationError,
|
||||||
|
} = useSimulator(message);
|
||||||
if (simulating) {
|
if (simulating) {
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
@ -42,6 +47,12 @@ export function SimulatorCard({ message }: { message: Message }) {
|
||||||
Simulate
|
Simulate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="card-body text-muted">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -54,6 +65,7 @@ export function SimulatorCard({ message }: { message: Message }) {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -114,14 +126,17 @@ function useSimulator(message: Message) {
|
||||||
const { cluster, url } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const [simulating, setSimulating] = React.useState(false);
|
const [simulating, setSimulating] = React.useState(false);
|
||||||
const [logs, setLogs] = React.useState<Array<InstructionLogs> | null>(null);
|
const [logs, setLogs] = React.useState<Array<InstructionLogs> | null>(null);
|
||||||
|
const [error, setError] = React.useState<string>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLogs(null);
|
setLogs(null);
|
||||||
setSimulating(false);
|
setSimulating(false);
|
||||||
|
setError(undefined);
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
const onClick = React.useCallback(() => {
|
const onClick = React.useCallback(() => {
|
||||||
if (simulating) return;
|
if (simulating) return;
|
||||||
|
setError(undefined);
|
||||||
setSimulating(true);
|
setSimulating(true);
|
||||||
|
|
||||||
const connection = new Connection(url, "confirmed");
|
const connection = new Connection(url, "confirmed");
|
||||||
|
@ -146,17 +161,19 @@ function useSimulator(message: Message) {
|
||||||
|
|
||||||
let instructionError;
|
let instructionError;
|
||||||
const responseLogs = resp.value.logs;
|
const responseLogs = resp.value.logs;
|
||||||
|
const responseErr = resp.value.err;
|
||||||
if (!responseLogs) {
|
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");
|
throw new Error("No logs detected");
|
||||||
} else if (resp.value.err) {
|
} else if (responseErr) {
|
||||||
const err = resp.value.err;
|
if (typeof responseErr !== "string") {
|
||||||
if (err && typeof err !== "string") {
|
let ixError = (responseErr as any)["InstructionError"];
|
||||||
let ixError = (err as any)["InstructionError"];
|
|
||||||
const [index, message] = ixError;
|
const [index, message] = ixError;
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
instructionError = { index, message };
|
instructionError = { index, message };
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(responseErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,10 +257,16 @@ function useSimulator(message: Message) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setLogs(null);
|
setLogs(null);
|
||||||
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setSimulating(false);
|
setSimulating(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [cluster, url, message, simulating]);
|
}, [cluster, url, message, simulating]);
|
||||||
return { simulate: onClick, simulating, simulationLogs: logs };
|
return {
|
||||||
|
simulate: onClick,
|
||||||
|
simulating,
|
||||||
|
simulationLogs: logs,
|
||||||
|
simulationError: error,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue