@shyft.to/solana-transaction-parser
Version:
Tool for parsing arbitrary Solana transactions with IDL/custom parsers
259 lines • 12.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.hexToBuffer = hexToBuffer;
exports.parseTransactionAccounts = parseTransactionAccounts;
exports.compiledInstructionToInstruction = compiledInstructionToInstruction;
exports.parsedInstructionToInstruction = parsedInstructionToInstruction;
exports.flattenTransactionResponse = flattenTransactionResponse;
exports.flattenParsedTransaction = flattenParsedTransaction;
exports.parseLogs = parseLogs;
const anchor_1 = require("@project-serum/anchor");
const web3_js_1 = require("@solana/web3.js");
function hexToBuffer(data) {
const rawHex = data.startsWith("0x") ? data.slice(2) : data;
return Buffer.from(rawHex);
}
/**
* Parse transaction message and extract account metas
* @param message transaction message
* @returns parsed accounts metas
*/
function parseTransactionAccounts(message, loadedAddresses = undefined) {
const accounts = message.version === "legacy" ? message.accountKeys : message.staticAccountKeys;
const readonlySignedAccountsCount = message.header.numReadonlySignedAccounts;
const readonlyUnsignedAccountsCount = message.header.numReadonlyUnsignedAccounts;
const requiredSignaturesAccountsCount = message.header.numRequiredSignatures;
const totalAccounts = accounts.length;
let parsedAccounts = accounts.map((account, idx) => {
const isWritable = idx < requiredSignaturesAccountsCount - readonlySignedAccountsCount ||
(idx >= requiredSignaturesAccountsCount && idx < totalAccounts - readonlyUnsignedAccountsCount);
return {
isSigner: idx < requiredSignaturesAccountsCount,
isWritable,
pubkey: new web3_js_1.PublicKey(account),
};
});
const [ALTWritable, ALTReadOnly] = message.version === "legacy" ? [[], []] : loadedAddresses ? [loadedAddresses.writable, loadedAddresses.readonly] : [[], []]; // message.getAccountKeys({ accountKeysFromLookups: loadedAddresses }).keySegments().slice(1); // omit static keys
if (ALTWritable)
parsedAccounts = [...parsedAccounts, ...ALTWritable.map((pubkey) => ({ isSigner: false, isWritable: true, pubkey }))];
if (ALTReadOnly)
parsedAccounts = [...parsedAccounts, ...ALTReadOnly.map((pubkey) => ({ isSigner: false, isWritable: false, pubkey }))];
return parsedAccounts;
}
/**
* Converts compiled instruction into common TransactionInstruction
* @param compiledInstruction
* @param parsedAccounts account meta, result of {@link parseTransactionAccounts}
* @returns TransactionInstruction
*/
function compiledInstructionToInstruction(compiledInstruction, parsedAccounts) {
if (typeof compiledInstruction.data === "string") {
const ci = compiledInstruction;
return new web3_js_1.TransactionInstruction({
data: anchor_1.utils.bytes.bs58.decode(ci.data),
programId: parsedAccounts[ci.programIdIndex].pubkey,
keys: ci.accounts.map((accountIdx) => parsedAccounts[accountIdx]),
});
}
else {
const ci = compiledInstruction;
return new web3_js_1.TransactionInstruction({
data: Buffer.from(ci.data),
programId: parsedAccounts[ci.programIdIndex].pubkey,
keys: ci.accountKeyIndexes.map((accountIndex) => {
if (accountIndex >= parsedAccounts.length)
throw new Error(`Trying to resolve account at index ${accountIndex} while parsedAccounts is only ${parsedAccounts.length}. \
Looks like you're trying to parse versioned transaction, make sure that LoadedAddresses are passed to the \
parseTransactionAccounts function`);
return parsedAccounts[accountIndex];
}),
});
}
}
function parsedAccountsToMeta(accounts, accountMeta) {
const meta = accountMeta.map((m) => ({ pk: m.pubkey.toString(), ...m }));
return accounts.map((account) => {
const encoded = account.toString();
const found = meta.find((item) => item.pk === encoded);
if (!found)
throw new Error(`Account ${encoded} not present in account meta!`);
return found;
});
}
function parsedInstructionToInstruction(parsedInstruction, accountMeta) {
return new web3_js_1.TransactionInstruction({
data: anchor_1.utils.bytes.bs58.decode(parsedInstruction.data),
programId: parsedInstruction.programId,
keys: parsedAccountsToMeta(parsedInstruction.accounts, accountMeta),
});
}
/**
* Converts transaction response with CPI into artifical transaction that contains all instructions from tx and CPI
* @param transaction transactionResponse to convert from
* @returns Transaction object
*/
function flattenTransactionResponse(transaction) {
var _a, _b, _c;
const result = [];
if (transaction === null || transaction === undefined)
return [];
const txInstructions = transaction.transaction.message.compiledInstructions;
const accountsMeta = parseTransactionAccounts(transaction.transaction.message, (_a = transaction.meta) === null || _a === void 0 ? void 0 : _a.loadedAddresses);
const orderedCII = (((_b = transaction === null || transaction === void 0 ? void 0 : transaction.meta) === null || _b === void 0 ? void 0 : _b.innerInstructions) || []).sort((a, b) => a.index - b.index);
const totalCalls = (((_c = transaction.meta) === null || _c === void 0 ? void 0 : _c.innerInstructions) || []).reduce((accumulator, cii) => accumulator + cii.instructions.length, 0) + txInstructions.length;
let lastPushedIx = -1;
let callIndex = -1;
for (const CII of orderedCII) {
// push original instructions until we meet CPI
while (lastPushedIx !== CII.index) {
lastPushedIx += 1;
callIndex += 1;
result.push(compiledInstructionToInstruction(txInstructions[lastPushedIx], accountsMeta));
}
for (const CIIEntry of CII.instructions) {
const parentProgramId = accountsMeta[txInstructions[lastPushedIx].programIdIndex].pubkey;
result.push({
...compiledInstructionToInstruction(CIIEntry, accountsMeta),
parentProgramId,
});
callIndex += 1;
}
}
while (callIndex < totalCalls - 1) {
lastPushedIx += 1;
callIndex += 1;
result.push(compiledInstructionToInstruction(txInstructions[lastPushedIx], accountsMeta));
}
return result;
}
function flattenParsedTransaction(transaction) {
var _a, _b;
const result = [];
if (!transaction) {
return result;
}
const txInstructions = transaction.transaction.message.instructions;
const orderedCII = (((_a = transaction === null || transaction === void 0 ? void 0 : transaction.meta) === null || _a === void 0 ? void 0 : _a.innerInstructions) || []).sort((a, b) => a.index - b.index);
const totalCalls = (((_b = transaction.meta) === null || _b === void 0 ? void 0 : _b.innerInstructions) || []).reduce((accumulator, cii) => accumulator + cii.instructions.length, 0) + txInstructions.length;
let lastPushedIx = -1;
let callIndex = -1;
for (const CII of orderedCII) {
// push original instructions until we meet CPI
while (lastPushedIx !== CII.index) {
lastPushedIx += 1;
callIndex += 1;
result.push(txInstructions[lastPushedIx]);
}
for (const CIIEntry of CII.instructions) {
const parentProgramId = txInstructions[lastPushedIx].programId;
result.push({
...CIIEntry,
parentProgramId,
});
callIndex += 1;
}
}
while (callIndex < totalCalls - 1) {
lastPushedIx += 1;
callIndex += 1;
result.push(txInstructions[lastPushedIx]);
}
return result;
}
/**
* @private
*/
function newLogContext(programId, depth, id, instructionIndex) {
return {
logMessages: [],
dataLogs: [],
rawLogs: [],
errors: [],
programId,
depth,
id,
instructionIndex,
};
}
/**
* Parses transaction logs and provides additional context such as
* - programId that generated the message
* - call id of instruction, that generated the message
* - call depth of instruction
* - data messages, log messages and error messages
* @param logs logs from TransactionResponse.meta.logs
* @returns parsed logs with call depth and additional context
*/
function parseLogs(logs) {
const parserRe = /(?<logTruncated>^Log truncated$)|(?<programInvoke>^Program (?<invokeProgramId>[1-9A-HJ-NP-Za-km-z]{32,}) invoke \[(?<level>\d+)\]$)|(?<programSuccessResult>^Program (?<successResultProgramId>[1-9A-HJ-NP-Za-km-z]{32,}) success$)|(?<programFailedResult>^Program (?<failedResultProgramId>[1-9A-HJ-NP-Za-km-z]{32,}) failed: (?<failedResultErr>.*)$)|(?<programCompleteFailedResult>^Program failed to complete: (?<failedCompleteError>.*)$)|(?<programLog>^^Program log: (?<logMessage>.*)$)|(?<programData>^Program data: (?<data>.*)$)|(?<programConsumed>^Program (?<consumedProgramId>[1-9A-HJ-NP-Za-km-z]{32,}) consumed (?<consumedComputeUnits>\d*) of (?<allComputedUnits>\d*) compute units$)|(?<programReturn>^Program return: (?<returnProgramId>[1-9A-HJ-NP-Za-km-z]{32,}) (?<returnMessage>.*)$)|(?<insufficientLamports>^Transfer: insufficient lamports)|(?<programConsumption>^Program consumption: (?<unitsRemaining>\d+) units remaining$)/s;
const result = [];
let id = -1;
let currentInstruction = 0;
let currentDepth = 0;
const callStack = [];
const callIds = [];
for (const log of logs) {
const match = parserRe.exec(log);
if (!match || !match.groups) {
throw new Error(`Failed to parse log line: ${log}`);
}
if (match.groups.logTruncated) {
result[callIds[callIds.length - 1]].invokeResult = "Log truncated";
}
else if (match.groups.programInvoke) {
callStack.push(match.groups.invokeProgramId);
id += 1;
currentDepth += 1;
callIds.push(id);
if (match.groups.level != currentDepth.toString())
throw new Error(`invoke depth mismatch, log: ${match.groups.level}, expected: ${currentDepth}`);
result.push(newLogContext(callStack[callStack.length - 1], callStack.length, id, currentInstruction));
result[result.length - 1].rawLogs.push(log);
}
else if (match.groups.programSuccessResult) {
const lastProgram = callStack.pop();
const lastCallIndex = callIds.pop();
if (lastCallIndex === undefined)
throw new Error("callIds malformed");
if (lastProgram != match.groups.successResultProgramId)
throw new Error("[ProramSuccess] callstack mismatch");
result[lastCallIndex].rawLogs.push(log);
currentDepth -= 1;
if (currentDepth === 0) {
currentInstruction += 1;
}
}
else if (match.groups.programFailedResult) {
const lastProgram = callStack.pop();
if (lastProgram != match.groups.failedResultProgramId)
throw new Error("[ProgramFailed] callstack mismatch");
result[callIds[callIds.length - 1]].rawLogs.push(log);
result[callIds[callIds.length - 1]].errors.push(match.groups.failedResultErr);
}
else if (match.groups.programCompleteFailedResult) {
result[callIds[callIds.length - 1]].rawLogs.push(log);
result[callIds[callIds.length - 1]].errors.push(match.groups.failedCompleteError);
}
else if (match.groups.programLog) {
result[callIds[callIds.length - 1]].rawLogs.push(log);
result[callIds[callIds.length - 1]].logMessages.push(match.groups.logMessage);
}
else if (match.groups.programData) {
result[callIds[callIds.length - 1]].rawLogs.push(log);
result[callIds[callIds.length - 1]].dataLogs.push(match.groups.data);
}
else if (match.groups.programConsumed) {
result[callIds[callIds.length - 1]].rawLogs.push(log);
}
else if (match.groups.programReturn) {
if (callStack[callStack.length - 1] != match.groups.returnProgramId)
throw new Error("[InvokeReturn]: callstack mismatch");
result[callIds[callIds.length - 1]].invokeResult = match.groups.returnMessage;
}
else if (match.groups.insufficientLamports) {
result[callIds[callIds.length - 1]].rawLogs.push(log);
}
}
return result;
}
//# sourceMappingURL=helpers.js.map