txray 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/SECURITY.md +9 -0
- package/dist/adapters/examples.js +173 -0
- package/dist/adapters/rpc.js +132 -0
- package/dist/app.js +111 -0
- package/dist/cli.js +38 -0
- package/dist/domain/analyze.js +161 -0
- package/dist/domain/base58.js +48 -0
- package/dist/domain/errors.js +192 -0
- package/dist/domain/instructions.js +54 -0
- package/dist/domain/logs.js +47 -0
- package/dist/domain/schema.js +90 -0
- package/dist/public/app.js +7 -0
- package/dist/public/architecture.html +79 -0
- package/dist/public/architecture.svg +17 -0
- package/dist/public/chevron.svg +1 -0
- package/dist/public/guide.html +66 -0
- package/dist/public/index.html +180 -0
- package/dist/public/mark.svg +1 -0
- package/dist/public/styles.css +865 -0
- package/dist/server.js +26 -0
- package/docs/api.md +37 -0
- package/docs/architecture.md +32 -0
- package/docs/architecture.svg +17 -0
- package/docs/guide.md +41 -0
- package/docs/screenshot.png +0 -0
- package/package.json +80 -4
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { transactionSchema } from "./schema.js";
|
|
2
|
+
import { diagnose } from "./errors.js";
|
|
3
|
+
import { parseLogs } from "./logs.js";
|
|
4
|
+
import { programName, decodeInstruction, formatLamports, COMPUTE, } from "./instructions.js";
|
|
5
|
+
export function analyzeTransaction(input, context) {
|
|
6
|
+
const raw = transactionSchema.parse(input);
|
|
7
|
+
const meta = raw.meta;
|
|
8
|
+
const message = raw.transaction.message;
|
|
9
|
+
const status = meta === null ? "unknown" : meta.err === null ? "succeeded" : "failed";
|
|
10
|
+
const diagnosis = diagnose(meta?.err ?? null, status);
|
|
11
|
+
const warnings = [];
|
|
12
|
+
if (meta === null)
|
|
13
|
+
warnings.push("Transaction metadata is missing. Outcome, fee and compute usage are unknown.");
|
|
14
|
+
if (raw.version === undefined)
|
|
15
|
+
warnings.push("Transaction version was not provided. Compute configuration is not inferred.");
|
|
16
|
+
if (raw.version === 1 && !message.transactionConfig)
|
|
17
|
+
warnings.push("The v1 transaction config is missing. Requested compute and priority fee are unknown.");
|
|
18
|
+
const keys = [
|
|
19
|
+
...message.accountKeys,
|
|
20
|
+
...(meta?.loadedAddresses?.writable ?? []),
|
|
21
|
+
...(meta?.loadedAddresses?.readonly ?? []),
|
|
22
|
+
];
|
|
23
|
+
const resolve = (index) => {
|
|
24
|
+
const key = keys[index];
|
|
25
|
+
if (!key && !warnings.some((w) => w.startsWith("Some account")))
|
|
26
|
+
warnings.push("Some account addresses could not be resolved. Lookup-table addresses may be missing.");
|
|
27
|
+
return key ?? null;
|
|
28
|
+
};
|
|
29
|
+
if (diagnosis.instructionIndex !== null &&
|
|
30
|
+
diagnosis.instructionIndex >= message.instructions.length) {
|
|
31
|
+
warnings.push("The reported failed instruction index is outside the instruction list. Its location is unknown.");
|
|
32
|
+
diagnosis.instructionIndex = null;
|
|
33
|
+
}
|
|
34
|
+
let requestedLimit = null;
|
|
35
|
+
let price = null;
|
|
36
|
+
let limitCount = 0;
|
|
37
|
+
let priceCount = 0;
|
|
38
|
+
const instructions = message.instructions.map((instruction, index) => {
|
|
39
|
+
const programId = resolve(instruction.programIdIndex);
|
|
40
|
+
const decoded = decodeInstruction(programId, instruction.data);
|
|
41
|
+
if (raw.version === "legacy" || raw.version === 0) {
|
|
42
|
+
if (decoded.limit !== null) {
|
|
43
|
+
requestedLimit = decoded.limit;
|
|
44
|
+
limitCount++;
|
|
45
|
+
}
|
|
46
|
+
if (decoded.price !== null) {
|
|
47
|
+
price = decoded.price;
|
|
48
|
+
priceCount++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const state = status === "unknown"
|
|
52
|
+
? "unknown"
|
|
53
|
+
: status === "succeeded"
|
|
54
|
+
? "succeeded"
|
|
55
|
+
: diagnosis.instructionIndex === null
|
|
56
|
+
? "unknown"
|
|
57
|
+
: index < diagnosis.instructionIndex
|
|
58
|
+
? "rolled_back"
|
|
59
|
+
: index === diagnosis.instructionIndex
|
|
60
|
+
? "failed"
|
|
61
|
+
: "not_run";
|
|
62
|
+
return {
|
|
63
|
+
index,
|
|
64
|
+
number: index + 1,
|
|
65
|
+
programId,
|
|
66
|
+
programName: programName(programId),
|
|
67
|
+
accounts: instruction.accounts.map(resolve),
|
|
68
|
+
data: instruction.data,
|
|
69
|
+
status: state,
|
|
70
|
+
action: raw.version === 1 && programId === COMPUTE
|
|
71
|
+
? "Compute Budget instruction (configuration ignored in v1)"
|
|
72
|
+
: decoded.action,
|
|
73
|
+
inner: (meta?.innerInstructions
|
|
74
|
+
?.filter((group) => group.index === index)
|
|
75
|
+
.flatMap((group) => group.instructions) ?? []).map((inner) => {
|
|
76
|
+
const id = resolve(inner.programIdIndex);
|
|
77
|
+
return {
|
|
78
|
+
programId: id,
|
|
79
|
+
programName: programName(id),
|
|
80
|
+
accounts: inner.accounts.map(resolve),
|
|
81
|
+
data: inner.data,
|
|
82
|
+
stackHeight: inner.stackHeight ?? null,
|
|
83
|
+
action: decodeInstruction(id, inner.data).action,
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
if (limitCount > 1 || priceCount > 1) {
|
|
89
|
+
warnings.push("Duplicate compute settings were recorded. Requested limits and prices are not inferred.");
|
|
90
|
+
requestedLimit = null;
|
|
91
|
+
price = null;
|
|
92
|
+
}
|
|
93
|
+
if (raw.version === 1)
|
|
94
|
+
requestedLimit = message.transactionConfig?.computeUnitLimit ?? null;
|
|
95
|
+
diagnosis.programId =
|
|
96
|
+
diagnosis.instructionIndex === null
|
|
97
|
+
? null
|
|
98
|
+
: (instructions[diagnosis.instructionIndex]?.programId ?? null);
|
|
99
|
+
const logs = meta?.logMessages ? parseLogs(meta.logMessages) : null;
|
|
100
|
+
if (logs === null)
|
|
101
|
+
warnings.push("Execution logs are unavailable. No log-based execution sequence is inferred.");
|
|
102
|
+
if (logs?.some((log) => log.kind === "truncated"))
|
|
103
|
+
warnings.push("The runtime truncated its logs. This execution log is incomplete.");
|
|
104
|
+
if (meta?.computeUnitsConsumed == null)
|
|
105
|
+
warnings.push("Compute consumption was not reported. Missing compute is not zero.");
|
|
106
|
+
const fee = {
|
|
107
|
+
lamports: meta?.fee ?? null,
|
|
108
|
+
sol: meta ? formatLamports(meta.fee) : null,
|
|
109
|
+
baseLamports: null,
|
|
110
|
+
requestedPriorityLamports: raw.version === 1
|
|
111
|
+
? (message.transactionConfig?.priorityFee ?? null)
|
|
112
|
+
: null,
|
|
113
|
+
requestedMicroLamportsPerCu: price,
|
|
114
|
+
payer: message.accountKeys[0] ?? null,
|
|
115
|
+
};
|
|
116
|
+
const report = {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
signature: context.signature,
|
|
119
|
+
network: context.network,
|
|
120
|
+
source: context.source,
|
|
121
|
+
exampleId: context.exampleId ?? null,
|
|
122
|
+
checkedAt: new Date().toISOString(),
|
|
123
|
+
slot: raw.slot,
|
|
124
|
+
blockTime: raw.blockTime ?? null,
|
|
125
|
+
version: raw.version ?? "unknown",
|
|
126
|
+
status,
|
|
127
|
+
diagnosis,
|
|
128
|
+
fee,
|
|
129
|
+
compute: {
|
|
130
|
+
consumed: meta?.computeUnitsConsumed ?? null,
|
|
131
|
+
costUnits: meta?.costUnits ?? null,
|
|
132
|
+
requestedLimit,
|
|
133
|
+
},
|
|
134
|
+
instructions,
|
|
135
|
+
logs,
|
|
136
|
+
warnings,
|
|
137
|
+
raw,
|
|
138
|
+
summaryText: "",
|
|
139
|
+
};
|
|
140
|
+
report.summaryText = [
|
|
141
|
+
`txray | ${context.source === "example" ? "Fictional offline example" : "Live RPC inspection"}`,
|
|
142
|
+
`Network: ${context.network}`,
|
|
143
|
+
`Signature: ${context.signature}`,
|
|
144
|
+
`Outcome: ${status}`,
|
|
145
|
+
`Diagnosis: ${diagnosis.title}`,
|
|
146
|
+
diagnosis.explanation,
|
|
147
|
+
`Failed instruction: ${diagnosis.instructionIndex === null ? "Not identified" : `#${diagnosis.instructionIndex + 1} (RPC index ${diagnosis.instructionIndex})`}`,
|
|
148
|
+
`Top-level program: ${diagnosis.programId ?? "Not identified"}`,
|
|
149
|
+
`Recorded fee: ${fee.sol === null ? "Not reported" : `${fee.sol} SOL (${fee.lamports} lamports)`}`,
|
|
150
|
+
"Base/priority fee split: not provided by getTransaction",
|
|
151
|
+
`Compute used: ${report.compute.consumed ?? "Not reported"} CU`,
|
|
152
|
+
...(status === "failed"
|
|
153
|
+
? [
|
|
154
|
+
"Ordinary program state changes roll back on failure. Fees and durable-nonce advancement can persist.",
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
...warnings.map((w) => `Note: ${w}`),
|
|
158
|
+
`Observed: ${report.checkedAt}`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2
|
+
export function fromBase58(value) {
|
|
3
|
+
if (value.length > 8192)
|
|
4
|
+
throw new Error("Base58 input too large");
|
|
5
|
+
let number = 0n;
|
|
6
|
+
for (const char of value) {
|
|
7
|
+
const index = alphabet.indexOf(char);
|
|
8
|
+
if (index < 0)
|
|
9
|
+
throw new Error("Invalid base58");
|
|
10
|
+
number = number * 58n + BigInt(index);
|
|
11
|
+
}
|
|
12
|
+
const bytes = [];
|
|
13
|
+
while (number > 0n) {
|
|
14
|
+
bytes.push(Number(number & 255n));
|
|
15
|
+
number >>= 8n;
|
|
16
|
+
}
|
|
17
|
+
let zeros = 0;
|
|
18
|
+
while (value[zeros] === "1")
|
|
19
|
+
zeros++;
|
|
20
|
+
return Uint8Array.from([
|
|
21
|
+
...new Array(zeros).fill(0),
|
|
22
|
+
...bytes.reverse(),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
export function toBase58(bytes) {
|
|
26
|
+
let number = 0n;
|
|
27
|
+
for (const byte of bytes)
|
|
28
|
+
number = number * 256n + BigInt(byte);
|
|
29
|
+
let result = "";
|
|
30
|
+
while (number > 0n) {
|
|
31
|
+
result = alphabet[Number(number % 58n)] + result;
|
|
32
|
+
number /= 58n;
|
|
33
|
+
}
|
|
34
|
+
let zeros = 0;
|
|
35
|
+
while (zeros < bytes.length && bytes[zeros] === 0)
|
|
36
|
+
zeros++;
|
|
37
|
+
return "1".repeat(zeros) + result;
|
|
38
|
+
}
|
|
39
|
+
export function isSignature(value) {
|
|
40
|
+
if (value.length < 64 || value.length > 88)
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
return fromBase58(value).length === 64;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export class TxrayError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
retryAfter;
|
|
5
|
+
constructor(code, message, status = 502, retryAfter) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.retryAfter = retryAfter;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const instructionErrors = {
|
|
13
|
+
InsufficientFunds: [
|
|
14
|
+
"Insufficient funds",
|
|
15
|
+
"An account did not have the funds required by this instruction. The record does not identify a specific account.",
|
|
16
|
+
],
|
|
17
|
+
ComputationalBudgetExceeded: [
|
|
18
|
+
"Compute budget exceeded",
|
|
19
|
+
"Execution used more compute than allowed. Review the program and the requested compute limit before retrying.",
|
|
20
|
+
],
|
|
21
|
+
ProgramFailedToComplete: [
|
|
22
|
+
"Program did not complete",
|
|
23
|
+
"The program stopped before completing. Inspect the runtime logs; this error alone does not establish why.",
|
|
24
|
+
],
|
|
25
|
+
InvalidArgument: [
|
|
26
|
+
"Invalid argument",
|
|
27
|
+
"An instruction supplied an argument the program rejected.",
|
|
28
|
+
],
|
|
29
|
+
InvalidInstructionData: [
|
|
30
|
+
"Invalid instruction data",
|
|
31
|
+
"The program could not accept the instruction data. Check its encoding and the program interface.",
|
|
32
|
+
],
|
|
33
|
+
InvalidAccountData: [
|
|
34
|
+
"Invalid account data",
|
|
35
|
+
"The program rejected the contents or type of an account.",
|
|
36
|
+
],
|
|
37
|
+
MissingRequiredSignature: [
|
|
38
|
+
"Missing required signature",
|
|
39
|
+
"An instruction required a signing authority that was not supplied.",
|
|
40
|
+
],
|
|
41
|
+
IncorrectProgramId: [
|
|
42
|
+
"Incorrect program",
|
|
43
|
+
"The instruction was given an account owned by an unexpected program, or invoked the wrong program.",
|
|
44
|
+
],
|
|
45
|
+
NotEnoughAccountKeys: [
|
|
46
|
+
"Missing account",
|
|
47
|
+
"The instruction did not supply all required accounts.",
|
|
48
|
+
],
|
|
49
|
+
AccountAlreadyInitialized: [
|
|
50
|
+
"Account already initialized",
|
|
51
|
+
"The instruction tried to initialize an account that was already initialized.",
|
|
52
|
+
],
|
|
53
|
+
UninitializedAccount: [
|
|
54
|
+
"Account not initialized",
|
|
55
|
+
"The program expected an initialized account.",
|
|
56
|
+
],
|
|
57
|
+
PrivilegeEscalation: [
|
|
58
|
+
"Account privilege escalation",
|
|
59
|
+
"An inner call requested signing or writing permissions that its caller did not provide.",
|
|
60
|
+
],
|
|
61
|
+
ReadonlyDataModified: [
|
|
62
|
+
"Read-only account modified",
|
|
63
|
+
"A program tried to modify data it was not allowed to write.",
|
|
64
|
+
],
|
|
65
|
+
ArithmeticOverflow: [
|
|
66
|
+
"Arithmetic overflow",
|
|
67
|
+
"An arithmetic operation could not be represented within the allowed numeric range.",
|
|
68
|
+
],
|
|
69
|
+
InvalidSeeds: [
|
|
70
|
+
"Invalid PDA seeds",
|
|
71
|
+
"The supplied seeds did not produce a valid program-derived address.",
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
const transactionErrors = {
|
|
75
|
+
InsufficientFundsForFee: [
|
|
76
|
+
"Insufficient funds for fee",
|
|
77
|
+
"The fee payer could not cover the transaction fee.",
|
|
78
|
+
],
|
|
79
|
+
InsufficientFundsForRent: [
|
|
80
|
+
"Insufficient funds for account storage",
|
|
81
|
+
"An account would not retain the minimum balance needed for its storage.",
|
|
82
|
+
],
|
|
83
|
+
AccountNotFound: [
|
|
84
|
+
"Account not found",
|
|
85
|
+
"A required account was not available on the selected network.",
|
|
86
|
+
],
|
|
87
|
+
ProgramAccountNotFound: [
|
|
88
|
+
"Program not found",
|
|
89
|
+
"A required program account was not available.",
|
|
90
|
+
],
|
|
91
|
+
BlockhashNotFound: [
|
|
92
|
+
"Blockhash not found",
|
|
93
|
+
"The transaction blockhash was unavailable or expired.",
|
|
94
|
+
],
|
|
95
|
+
InvalidProgramForExecution: [
|
|
96
|
+
"Program cannot execute",
|
|
97
|
+
"An instruction referenced an account that cannot execute as a program.",
|
|
98
|
+
],
|
|
99
|
+
InvalidAccountIndex: [
|
|
100
|
+
"Invalid account index",
|
|
101
|
+
"An instruction referenced an account outside the transaction account list.",
|
|
102
|
+
],
|
|
103
|
+
SignatureFailure: [
|
|
104
|
+
"Signature verification failed",
|
|
105
|
+
"At least one transaction signature did not verify.",
|
|
106
|
+
],
|
|
107
|
+
AddressLookupTableNotFound: [
|
|
108
|
+
"Address lookup table not found",
|
|
109
|
+
"A referenced address lookup table was unavailable.",
|
|
110
|
+
],
|
|
111
|
+
InvalidAddressLookupTableIndex: [
|
|
112
|
+
"Invalid lookup table index",
|
|
113
|
+
"The transaction referenced an entry outside its address lookup table.",
|
|
114
|
+
],
|
|
115
|
+
MaxLoadedAccountsDataSizeExceeded: [
|
|
116
|
+
"Account data limit exceeded",
|
|
117
|
+
"The transaction needed more loaded account data than its configured limit.",
|
|
118
|
+
],
|
|
119
|
+
UnsupportedVersion: [
|
|
120
|
+
"Unsupported transaction version",
|
|
121
|
+
"The transaction uses a version the runtime does not support.",
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
export function diagnose(error, status) {
|
|
125
|
+
const result = {
|
|
126
|
+
title: status === "succeeded"
|
|
127
|
+
? "Transaction succeeded"
|
|
128
|
+
: status === "unknown"
|
|
129
|
+
? "Outcome unavailable"
|
|
130
|
+
: "Unknown transaction error",
|
|
131
|
+
explanation: status === "succeeded"
|
|
132
|
+
? "All instructions completed. This records execution, not service quality or token safety."
|
|
133
|
+
: status === "unknown"
|
|
134
|
+
? "The RPC did not provide transaction metadata, so the outcome cannot be determined."
|
|
135
|
+
: "The runtime returned an error txray does not recognize. The raw error is preserved below.",
|
|
136
|
+
known: status === "succeeded",
|
|
137
|
+
instructionIndex: null,
|
|
138
|
+
programId: null,
|
|
139
|
+
raw: error,
|
|
140
|
+
};
|
|
141
|
+
if (status !== "failed")
|
|
142
|
+
return result;
|
|
143
|
+
let detail = error;
|
|
144
|
+
let map = transactionErrors;
|
|
145
|
+
if (typeof error === "object" &&
|
|
146
|
+
error !== null &&
|
|
147
|
+
!Array.isArray(error) &&
|
|
148
|
+
"InstructionError" in error) {
|
|
149
|
+
const value = error.InstructionError;
|
|
150
|
+
if (Array.isArray(value) &&
|
|
151
|
+
value.length === 2 &&
|
|
152
|
+
Number.isSafeInteger(value[0]) &&
|
|
153
|
+
value[0] >= 0) {
|
|
154
|
+
result.instructionIndex = value[0];
|
|
155
|
+
detail = value[1];
|
|
156
|
+
map = instructionErrors;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (typeof detail === "object" &&
|
|
160
|
+
detail !== null &&
|
|
161
|
+
!Array.isArray(detail) &&
|
|
162
|
+
"Custom" in detail) {
|
|
163
|
+
result.title = "Unknown program error";
|
|
164
|
+
result.explanation = `The program returned custom error ${JSON.stringify(detail.Custom)}. Its meaning is program-specific; txray has no verified definition for it. Read the program documentation and logs. Log text alone is not a verified error definition.`;
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
let code;
|
|
168
|
+
if (typeof detail === "string")
|
|
169
|
+
code = detail;
|
|
170
|
+
else if (map === transactionErrors &&
|
|
171
|
+
typeof detail === "object" &&
|
|
172
|
+
detail !== null &&
|
|
173
|
+
!Array.isArray(detail) &&
|
|
174
|
+
Object.keys(detail).length === 1 &&
|
|
175
|
+
"InsufficientFundsForRent" in detail) {
|
|
176
|
+
const rent = detail
|
|
177
|
+
.InsufficientFundsForRent;
|
|
178
|
+
if (typeof rent === "object" &&
|
|
179
|
+
rent !== null &&
|
|
180
|
+
"account_index" in rent &&
|
|
181
|
+
Number.isSafeInteger(rent.account_index) &&
|
|
182
|
+
Number(rent.account_index) >= 0)
|
|
183
|
+
code = "InsufficientFundsForRent";
|
|
184
|
+
}
|
|
185
|
+
const known = code && Object.hasOwn(map, code) ? map[code] : undefined;
|
|
186
|
+
if (known) {
|
|
187
|
+
result.title = known[0];
|
|
188
|
+
result.explanation = known[1];
|
|
189
|
+
result.known = true;
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fromBase58 } from "./base58.js";
|
|
2
|
+
export const COMPUTE = "ComputeBudget111111111111111111111111111111";
|
|
3
|
+
export const SYSTEM = "11111111111111111111111111111111";
|
|
4
|
+
const programs = {
|
|
5
|
+
[SYSTEM]: "System Program",
|
|
6
|
+
[COMPUTE]: "Compute Budget",
|
|
7
|
+
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "Token Program",
|
|
8
|
+
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "Token-2022",
|
|
9
|
+
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "Associated Token",
|
|
10
|
+
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr: "Memo",
|
|
11
|
+
BPFLoaderUpgradeab1e11111111111111111111111: "Upgradeable Loader",
|
|
12
|
+
};
|
|
13
|
+
export const programName = (id) => id
|
|
14
|
+
? Object.hasOwn(programs, id)
|
|
15
|
+
? programs[id]
|
|
16
|
+
: "Unknown program"
|
|
17
|
+
: "Unresolved program";
|
|
18
|
+
export function formatLamports(value) {
|
|
19
|
+
const number = BigInt(value);
|
|
20
|
+
const whole = number / 1000000000n;
|
|
21
|
+
const fraction = (number % 1000000000n)
|
|
22
|
+
.toString()
|
|
23
|
+
.padStart(9, "0")
|
|
24
|
+
.replace(/0+$/, "");
|
|
25
|
+
return whole.toString() + (fraction ? "." + fraction : "");
|
|
26
|
+
}
|
|
27
|
+
function le(bytes) {
|
|
28
|
+
return bytes.reduceRight((value, byte) => value * 256n + BigInt(byte), 0n);
|
|
29
|
+
}
|
|
30
|
+
export function decodeInstruction(programId, data) {
|
|
31
|
+
const result = { action: null, limit: null, price: null };
|
|
32
|
+
try {
|
|
33
|
+
const bytes = fromBase58(data);
|
|
34
|
+
if (programId === COMPUTE) {
|
|
35
|
+
if (bytes[0] === 2 && bytes.length === 5) {
|
|
36
|
+
result.limit = le(bytes.slice(1)).toString();
|
|
37
|
+
result.action = `Set compute unit limit: ${result.limit} CU`;
|
|
38
|
+
}
|
|
39
|
+
if (bytes[0] === 3 && bytes.length === 9) {
|
|
40
|
+
result.price = le(bytes.slice(1)).toString();
|
|
41
|
+
result.action = `Set compute unit price: ${result.price} micro-lamports/CU`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (programId === SYSTEM &&
|
|
45
|
+
bytes.length === 12 &&
|
|
46
|
+
le(bytes.slice(0, 4)) === 2n) {
|
|
47
|
+
result.action = `Transfer requested: ${formatLamports(le(bytes.slice(4)).toString())} SOL`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* Unrecognized or malformed instruction data remains undecoded. */
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function parseLogs(lines) {
|
|
2
|
+
const stack = [];
|
|
3
|
+
return lines.map((text, index) => {
|
|
4
|
+
const event = {
|
|
5
|
+
line: index + 1,
|
|
6
|
+
text,
|
|
7
|
+
kind: "message",
|
|
8
|
+
depth: stack.at(-1)?.depth ?? null,
|
|
9
|
+
programId: null,
|
|
10
|
+
computeUsed: null,
|
|
11
|
+
computeLimit: null,
|
|
12
|
+
};
|
|
13
|
+
const invoke = /^Program ([1-9A-HJ-NP-Za-km-z]{32,44}) invoke \[([0-9]+)\]$/.exec(text);
|
|
14
|
+
const end = /^Program ([1-9A-HJ-NP-Za-km-z]{32,44}) (success|failed: .*)$/.exec(text);
|
|
15
|
+
const compute = /^Program ([1-9A-HJ-NP-Za-km-z]{32,44}) consumed ([0-9]+) of ([0-9]+) compute units$/.exec(text);
|
|
16
|
+
if (invoke && Number(invoke[2]) > 0 && Number(invoke[2]) <= 64) {
|
|
17
|
+
const depth = Number(invoke[2]);
|
|
18
|
+
while (stack.length && (stack.at(-1)?.depth ?? 0) >= depth)
|
|
19
|
+
stack.pop();
|
|
20
|
+
stack.push({ programId: invoke[1], depth });
|
|
21
|
+
Object.assign(event, { kind: "invoke", programId: invoke[1], depth });
|
|
22
|
+
}
|
|
23
|
+
else if (end) {
|
|
24
|
+
const frame = stack.findLastIndex((f) => f.programId === end[1]);
|
|
25
|
+
Object.assign(event, {
|
|
26
|
+
kind: end[2] === "success" ? "success" : "failure",
|
|
27
|
+
programId: end[1],
|
|
28
|
+
depth: frame < 0 ? null : stack[frame].depth,
|
|
29
|
+
});
|
|
30
|
+
if (frame >= 0)
|
|
31
|
+
stack.splice(frame);
|
|
32
|
+
}
|
|
33
|
+
else if (compute) {
|
|
34
|
+
Object.assign(event, {
|
|
35
|
+
kind: "compute",
|
|
36
|
+
programId: compute[1],
|
|
37
|
+
depth: stack.findLast((f) => f.programId === compute[1])?.depth ?? null,
|
|
38
|
+
computeUsed: compute[2],
|
|
39
|
+
computeLimit: compute[3],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else if (/^Log truncated(?:$|:)/i.test(text)) {
|
|
43
|
+
event.kind = "truncated";
|
|
44
|
+
}
|
|
45
|
+
return event;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSignature } from "./base58.js";
|
|
3
|
+
export const networkSchema = z.enum(["mainnet-beta", "devnet"]);
|
|
4
|
+
export const requestSchema = z.strictObject({
|
|
5
|
+
signature: z
|
|
6
|
+
.string()
|
|
7
|
+
.refine(isSignature, "Enter a base58 Solana transaction signature (64 bytes)."),
|
|
8
|
+
network: networkSchema,
|
|
9
|
+
});
|
|
10
|
+
export const uint = z
|
|
11
|
+
.union([
|
|
12
|
+
z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.nonnegative()
|
|
16
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
17
|
+
.transform(String),
|
|
18
|
+
z.string().regex(/^(0|[1-9][0-9]{0,19})$/),
|
|
19
|
+
])
|
|
20
|
+
.refine((v) => BigInt(v) <= 18446744073709551615n, "Value exceeds u64");
|
|
21
|
+
const index = z.number().int().nonnegative().max(65535);
|
|
22
|
+
const instruction = z
|
|
23
|
+
.object({
|
|
24
|
+
programIdIndex: index,
|
|
25
|
+
accounts: z.array(index).max(4096),
|
|
26
|
+
data: z.string().max(8192),
|
|
27
|
+
stackHeight: index.nullable().optional(),
|
|
28
|
+
})
|
|
29
|
+
.passthrough();
|
|
30
|
+
export const transactionSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
slot: uint,
|
|
33
|
+
blockTime: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.nonnegative()
|
|
37
|
+
.max(8640000000000)
|
|
38
|
+
.nullable()
|
|
39
|
+
.optional(),
|
|
40
|
+
version: z
|
|
41
|
+
.union([z.literal("legacy"), z.literal(0), z.literal(1)])
|
|
42
|
+
.optional(),
|
|
43
|
+
transaction: z
|
|
44
|
+
.object({
|
|
45
|
+
signatures: z.array(z.string().refine(isSignature)).min(1).max(256),
|
|
46
|
+
message: z
|
|
47
|
+
.object({
|
|
48
|
+
accountKeys: z.array(z.string().min(1).max(64)).max(4096),
|
|
49
|
+
instructions: z.array(instruction).max(4096),
|
|
50
|
+
transactionConfig: z
|
|
51
|
+
.object({
|
|
52
|
+
computeUnitLimit: uint.nullable(),
|
|
53
|
+
priorityFee: uint.nullable(),
|
|
54
|
+
heapSize: uint.nullable().optional(),
|
|
55
|
+
loadedAccountsDataSizeLimit: uint.nullable().optional(),
|
|
56
|
+
})
|
|
57
|
+
.passthrough()
|
|
58
|
+
.optional(),
|
|
59
|
+
})
|
|
60
|
+
.passthrough(),
|
|
61
|
+
})
|
|
62
|
+
.passthrough(),
|
|
63
|
+
meta: z
|
|
64
|
+
.object({
|
|
65
|
+
err: z.json(),
|
|
66
|
+
fee: uint,
|
|
67
|
+
computeUnitsConsumed: uint.nullable().optional(),
|
|
68
|
+
costUnits: uint.nullable().optional(),
|
|
69
|
+
logMessages: z
|
|
70
|
+
.array(z.string().max(200000))
|
|
71
|
+
.max(20000)
|
|
72
|
+
.nullable()
|
|
73
|
+
.optional(),
|
|
74
|
+
loadedAddresses: z
|
|
75
|
+
.object({
|
|
76
|
+
writable: z.array(z.string().max(64)).max(4096),
|
|
77
|
+
readonly: z.array(z.string().max(64)).max(4096),
|
|
78
|
+
})
|
|
79
|
+
.nullable()
|
|
80
|
+
.optional(),
|
|
81
|
+
innerInstructions: z
|
|
82
|
+
.array(z.object({ index, instructions: z.array(instruction).max(4096) }))
|
|
83
|
+
.max(4096)
|
|
84
|
+
.nullable()
|
|
85
|
+
.optional(),
|
|
86
|
+
})
|
|
87
|
+
.passthrough()
|
|
88
|
+
.nullable(),
|
|
89
|
+
})
|
|
90
|
+
.passthrough();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var S="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";function k(e){if(e.length>8192)throw new Error("Base58 input too large");let o=0n;for(let r of e){let d=S.indexOf(r);if(d<0)throw new Error("Invalid base58");o=o*58n+BigInt(d)}let a=[];for(;o>0n;)a.push(Number(o&255n)),o>>=8n;let n=0;for(;e[n]==="1";)n++;return Uint8Array.from([...new Array(n).fill(0),...a.reverse()])}function L(e){if(e.length<64||e.length>88)return!1;try{return k(e).length===64}catch{return!1}}function t(e){let o=document.getElementById(e);if(!o)throw new Error(`Missing element: ${e}`);return o}function i(e,o="",a){let n=document.createElement(e);return n.className=o,a!==void 0&&(n.textContent=a),n}var R=t("inspect-form"),v=t("signature"),M=t("network"),u=t("activity"),h=t("error"),T=t("result"),w=t("empty"),c=null,U={succeeded:"Succeeded",failed:"Failed",rolled_back:"Ran; rolled back",not_run:"Not run",unknown:"Not determined"};function p(e){return e===null?"Not reported":BigInt(e).toLocaleString("en-US")}function l(e,o,a,n=""){e.append(i("dt","",o),i("dd",n,a))}function I(e){c=e;let o=e.source==="example";t("source-label").textContent=o?"Offline example \xB7 Fictional values":`Live RPC \xB7 ${e.network==="devnet"?"Devnet":"Mainnet"}`,t("source-label").className=`source-label${o?" example":""}`,t("diagnosis").className=`diagnosis ${e.status}`,t("outcome").textContent=e.status==="failed"?"Transaction failed":e.status==="succeeded"?"Execution completed":"Incomplete record",t("diagnosis-title").textContent=e.diagnosis.title,t("explanation").textContent=e.diagnosis.explanation;let a=t("failure-note");a.hidden=e.status!=="failed",a.textContent="Program state changes ordinarily roll back on failure. Fees and durable-nonce advancement can persist. A recorded fee is not refunded by this debugger.";let n=e.diagnosis.instructionIndex;t("failed-instruction").textContent=n===null?"Not identified":`#${n+1} (RPC index ${n})`,t("fee").textContent=e.fee.sol===null?"Not reported":`${e.fee.sol} SOL`,t("compute").textContent=e.compute.consumed===null?"Not reported":`${p(e.compute.consumed)} CU`,t("failed-program").textContent=e.diagnosis.programId??"Not identified";let r=t("resources");r.replaceChildren(),l(r,"Recorded fee",e.fee.lamports===null?"Not reported":`${p(e.fee.lamports)} lamports`),l(r,"Base / priority split","Not provided by getTransaction"),l(r,"Requested compute limit",e.compute.requestedLimit===null?"Not reported":`${p(e.compute.requestedLimit)} CU`),e.version===1?l(r,"Requested priority fee (v1)",e.fee.requestedPriorityLamports===null?"Not set or unavailable":`${p(e.fee.requestedPriorityLamports)} lamports`):l(r,"Requested CU price",e.fee.requestedMicroLamportsPerCu===null?"Not reported":`${p(e.fee.requestedMicroLamportsPerCu)} micro-lamports/CU`),l(r,"Scheduling cost",e.compute.costUnits===null?"Not reported":`${p(e.compute.costUnits)} cost units`);let d=t("identity");d.replaceChildren(),l(d,"Signature",e.signature,"address"),l(d,"Version",String(e.version)),l(d,"Slot",p(e.slot)),l(d,"Fee payer",e.fee.payer??"Not reported","address");let y=e.blockTime===null?null:new Date(e.blockTime*1e3);l(d,"Block time",y&&!Number.isNaN(y.getTime())?y.toISOString():"Not reported"),l(d,"Observed",new Date(e.checkedAt).toISOString()),t("warnings").replaceChildren(...e.warnings.map(s=>i("li","",s))),t("warnings-section").hidden=e.warnings.length===0,t("raw-error").textContent=JSON.stringify(e.diagnosis.raw,null,2),t("instruction-count").textContent=`${e.instructions.length} top-level`,t("instructions").replaceChildren(...e.instructions.map(s=>{let m=i("li",`instruction ${s.status}`);m.append(i("span","instruction-number",`#${s.number}`));let x=i("div"),E=i("div","instruction-header");E.append(i("strong","",s.programName),i("span","instruction-state",U[s.status]??"Not determined")),x.append(E,i("p","instruction-id address",s.programId??"Program address not available")),s.action&&x.append(i("p","instruction-action",s.action));let b=i("details");b.append(i("summary","",`Accounts & data (RPC index ${s.index})`)),b.append(i("pre","",`Accounts:
|
|
2
|
+
${s.accounts.map((g,f)=>`${f}: ${g??"Unresolved address"}`).join(`
|
|
3
|
+
`)||"None"}
|
|
4
|
+
|
|
5
|
+
Data (base58):
|
|
6
|
+
${s.data||"(empty)"}`));for(let g of s.inner){let f=i("div","inner-call");f.append(i("strong","",`Inner call: ${g.programName}`),i("p","address",g.programId??"Unresolved address")),g.action&&f.append(i("p","",g.action)),b.append(f)}return x.append(b),m.append(x),m})),t("log-count").textContent=e.logs===null?"Unavailable":`${e.logs.length} lines`;let C=t("logs");if(C.replaceChildren(),e.logs===null||e.logs.length===0)C.append(i("p","logs-empty",e.logs===null?"The RPC did not provide logs for this transaction.":"No execution log lines were recorded."));else for(let s of e.logs){let m=i("div",`log-line log-${s.kind} log-depth-${Math.min(8,s.depth??1)}`);m.append(i("span","log-number",String(s.line)),i("span","log-text",s.text)),C.append(m)}let N=t("explorer");N.hidden=o,N.href=`https://explorer.solana.com/tx/${encodeURIComponent(e.signature)}${e.network==="devnet"?"?cluster=devnet":""}`,w.hidden=!0,T.hidden=!1}async function $(e,o){h.hidden=!0,u.textContent=o?"Looking up transaction\u2026":"Loading offline example\u2026",T.hidden=!0,w.hidden=!0;let a=document.querySelectorAll("#inspect-form button,#inspect-form input,#inspect-form select,#examples button");a.forEach(n=>n.disabled=!0);try{let n=await fetch(e,{...o?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(16e3)}),r=await n.json();if(!n.ok)throw new Error(r.error?.message??"Lookup failed. Try again.");I(r),u.textContent=""}catch(n){c=null,h.textContent=n instanceof Error&&n.name==="TimeoutError"?"Lookup timed out. Please try again.":n instanceof Error?n.message:"Could not reach the local server.",h.hidden=!1,w.hidden=!1,u.textContent=""}finally{a.forEach(n=>n.disabled=!1)}}R.addEventListener("submit",e=>{e.preventDefault();let o=v.value.trim();if(!L(o)){h.textContent="Enter a valid Solana transaction signature: a base58 value representing 64 bytes.",h.hidden=!1,v.setAttribute("aria-invalid","true"),v.focus();return}v.removeAttribute("aria-invalid"),$("/api/transactions",{signature:o,network:M.value})});t("copy").addEventListener("click",()=>{c&&(navigator.clipboard?.writeText(c.summaryText).then(()=>{u.textContent="Report copied."}).catch(()=>{u.textContent="Clipboard unavailable. Export JSON to save this report."}),navigator.clipboard||(u.textContent="Clipboard unavailable. Export JSON to save this report."))});t("export").addEventListener("click",()=>{if(!c)return;let e=new Blob([JSON.stringify(c,null,2)+`
|
|
7
|
+
`],{type:"application/json"}),o=URL.createObjectURL(e),a=i("a");a.href=o,a.download=`txray-${c.source==="example"?"example-"+c.exampleId:c.signature.slice(0,12)}.json`,document.body.append(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(o),1e3),u.textContent="JSON export downloaded."});async function O(){try{let e=await fetch("/api/examples");if(!e.ok)throw new Error("Examples unavailable");let o=await e.json();for(let a of o){let n=i("button","",a.title);n.type="button",n.title=a.description,n.addEventListener("click",()=>{$(`/api/examples/${encodeURIComponent(a.id)}`)}),t("examples").append(n)}}catch{u.textContent="Offline examples could not be loaded. Refresh the page or inspect a signature."}}O();
|