autrace
Version:
Account Update analyser for MINA
284 lines • 13 kB
JavaScript
export class BlockchainFlowAnalyzer {
constructor() {
this.analyzeTransactionFlow = (blockchainTx) => {
const txMeta = {
txHash: blockchainTx.txHash,
status: blockchainTx.txStatus,
blockHeight: blockchainTx.blockHeight,
timestamp: blockchainTx.timestamp,
fee: blockchainTx.fee,
feePayerAddress: blockchainTx.feePayerAddress,
memo: blockchainTx.memo || ''
};
const accountUpdates = this.processAccountUpdates(blockchainTx.updatedAccounts);
this.mapFailureInfo(accountUpdates, blockchainTx.failures);
const relationships = [
// Various relationship types in order of precedence
this.buildCallDepthRelationships(accountUpdates),
this.buildStateDepRelationships(accountUpdates),
this.buildTokenRelationships(accountUpdates),
this.buildFeepayerRelationships(accountUpdates, txMeta.feePayerAddress),
this.buildCallDataRelationships(accountUpdates),
this.buildTemporalRelationships(accountUpdates)
];
const edges = this.mergeRelationships(relationships);
return {
metadata: txMeta,
nodes: accountUpdates,
edges: edges
};
};
this.processAccountUpdates = (rawAccounts) => {
return rawAccounts.map((account, index) => ({
id: `au-${index}`,
index,
address: account.accountAddress,
shortAddress: account.accountAddress.substring(0, 12) + '...',
isContract: account.isZkappAccount || !!account.update?.verificationKey,
callDepth: account.callDepth || 0,
tokenId: account.tokenId,
tokenSymbol: this.extractTokenSymbol(account),
balanceChange: account.totalBalanceChange || 0,
failed: false, // Will be updated in mapFailureInfo
failureReason: null,
stateValues: this.extractStateValues(account.update?.appState),
callData: account.callData !== '0' ? account.callData : null,
permissions: account.update?.permissions || {}
}));
};
this.mapFailureInfo = (accounts, failures) => {
if (!failures || !Array.isArray(failures))
return;
failures.forEach(failure => {
// Adjust for 0-based vs 1-based indexing if needed
const accountIndex = failure.index - 1;
if (accountIndex >= 0 && accountIndex < accounts.length) {
accounts[accountIndex].failed = true;
accounts[accountIndex].failureReason = failure.failureReason;
}
});
// Find root cause of failure to highlight it
const rootFailure = accounts.find(a => a.failed);
if (rootFailure) {
rootFailure.isRootFailure = true;
}
};
this.buildCallDepthRelationships = (accounts) => {
const relationships = [];
// Group accounts by call depth
const accountsByDepth = new Map();
accounts.forEach(account => {
if (!accountsByDepth.has(account.callDepth)) {
accountsByDepth.set(account.callDepth, []);
}
accountsByDepth.get(account.callDepth).push(account);
});
// Process accounts with depth > 0
for (let depth = 1; depth <= Math.max(...accountsByDepth.keys()); depth++) {
const highDepthAccounts = accountsByDepth.get(depth) || [];
const lowerDepthAccounts = accountsByDepth.get(depth - 1) || [];
highDepthAccounts.forEach(highAccount => {
// Find potential parent account(s) with same address at lower depth
const potentialParents = lowerDepthAccounts.filter(lowAccount => lowAccount.address === highAccount.address);
if (potentialParents.length > 0) {
// Use the closest previous parent in sequence
const validParents = potentialParents.filter(p => p.index < highAccount.index);
if (validParents.length > 0) {
const parent = validParents.reduce((prev, curr) => (curr.index > prev.index) ? curr : prev);
relationships.push({
from: parent.id,
to: highAccount.id,
type: 'call_depth',
label: 'calls (depth)',
color: 'blue'
});
}
}
});
}
return relationships;
};
this.buildStateDepRelationships = (accounts) => {
const relationships = [];
const stateValues = new Map();
// Track all non-zero state values
accounts.forEach(account => {
if (account.stateValues) {
account.stateValues.forEach((value, index) => {
if (value && value !== '0') {
const valueStr = value.toString();
if (!stateValues.has(valueStr)) {
stateValues.set(valueStr, []);
}
stateValues.get(valueStr).push({ accountId: account.id, index });
}
});
}
});
// Create relationships for accounts that share state values
stateValues.forEach((appearances, value) => {
if (appearances.length > 1) {
// Sort by account index to get chronological order
appearances.sort((a, b) => {
const accountA = accounts.find(acc => acc.id === a.accountId);
const accountB = accounts.find(acc => acc.id === b.accountId);
return accountA.index - accountB.index;
});
// Create edges between consecutive appearances
for (let i = 0; i < appearances.length - 1; i++) {
relationships.push({
from: appearances[i].accountId,
to: appearances[i + 1].accountId,
type: 'state_dependency',
label: 'state dep',
color: 'black'
});
}
}
});
return relationships;
};
this.buildTokenRelationships = (accounts) => {
const relationships = [];
const defaultTokenId = 'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf'; // Mina default token
// Group accounts by token ID
const accountsByToken = new Map();
accounts.forEach(account => {
if (!accountsByToken.has(account.tokenId)) {
accountsByToken.set(account.tokenId, []);
}
accountsByToken.get(account.tokenId).push(account);
});
// Create relationships for non-default tokens
accountsByToken.forEach((tokenAccounts, tokenId) => {
if (tokenId !== defaultTokenId && tokenAccounts.length > 1) {
// Sort by account index
tokenAccounts.sort((a, b) => a.index - b.index);
// Create a chain of token operations
for (let i = 0; i < tokenAccounts.length - 1; i++) {
relationships.push({
from: tokenAccounts[i].id,
to: tokenAccounts[i + 1].id,
type: 'token_operation',
label: 'token op',
color: 'purple'
});
}
}
});
return relationships;
};
this.buildFeepayerRelationships = (accounts, feePayerAddress) => {
const relationships = [];
const feePayer = accounts.find(account => account.address === feePayerAddress);
if (!feePayer)
return relationships;
// Connect fee payer to the first non-fee-payer account update
const firstOp = accounts.find(account => account.address !== feePayerAddress);
if (firstOp) {
relationships.push({
from: feePayer.id,
to: firstOp.id,
type: 'fee_payer',
label: 'initiates',
color: 'green'
});
}
return relationships;
};
this.buildCallDataRelationships = (accounts) => {
const relationships = [];
accounts.forEach((account, i) => {
if (account.callData) {
// Connect to next account in sequence as a heuristic
// More sophisticated matching could be done based on actual callData analysis
if (i < accounts.length - 1) {
relationships.push({
from: account.id,
to: accounts[i + 1].id,
type: 'call_data',
label: 'calls (data)',
color: 'orange'
});
}
}
});
return relationships;
};
this.buildTemporalRelationships = (accounts) => {
const relationships = [];
for (let i = 0; i < accounts.length - 1; i++) {
relationships.push({
from: accounts[i].id,
to: accounts[i + 1].id,
type: 'sequence',
label: 'sequence',
color: 'gray'
});
}
return relationships;
};
this.mergeRelationships = (relationshipSets) => {
const merged = [];
const edgeMap = new Map();
// Process each set of relationships in priority order
relationshipSets.forEach(relationships => {
relationships.forEach(rel => {
const edgeKey = `${rel.from}-${rel.to}`;
if (!edgeMap.has(edgeKey)) {
edgeMap.set(edgeKey, true);
merged.push(rel);
}
});
});
return merged;
};
this.applyFailureStatus = (relationships, accounts) => {
return relationships.map(rel => {
const fromAccount = accounts.find(acc => acc.id === rel.from);
const toAccount = accounts.find(acc => acc.id === rel.to);
if ((fromAccount && fromAccount.failed) || (toAccount && toAccount.failed)) {
return {
...rel,
failed: true,
style: 'dashed'
};
}
return rel;
});
};
this.extractTokenSymbol = (account) => {
if (account.update?.tokenSymbol) {
return account.update.tokenSymbol;
}
// Default Mina token
if (account.tokenId === 'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf') {
return 'MINA';
}
return 'Custom';
};
this.extractStateValues = (appState) => {
if (!appState || !Array.isArray(appState)) {
return [];
}
return appState.map(state => state ? state.toString() : '0');
};
this.enhanceTransactionState = (txState, blockchainTx) => {
// Only proceed if this is a blockchain transaction
if (!blockchainTx || !blockchainTx.txHash) {
return txState; // Return unchanged if not a blockchain transaction
}
// Generate flow graph
const flowGraph = this.analyzeTransactionFlow(blockchainTx);
// Create a new transaction state with the original data plus flow graph
return {
...txState,
blockchainData: {
...(txState.blockchainData || {}),
flowGraph: flowGraph
}
};
};
}
}
//# sourceMappingURL=BlockchainFlowAnalyzer.js.map