autrace
Version:
Account Update analyser for MINA
454 lines • 21.2 kB
JavaScript
import { SmartContractAnalyzer } from './ContractAnalyser.js';
import { AccountUpdateAnalyzer } from './AccountUpdateAnalyzer.js';
import { adaptBlockchainTransaction } from './BlockchainAdapter.js';
import { BlockchainFlowAnalyzer } from './BlockchainFlowAnalyzer.js';
import { UInt64, Field } from 'o1js';
export class AUTrace {
constructor() {
this.transactionSnapshots = [];
this.traverseTransaction = (transaction) => {
if (!transaction)
return;
// Extract blockchain data if available
if (transaction.originalBlockchainData) {
const blockchainTx = transaction.originalBlockchainData;
this.transactionState.blockchainData = {
blockHeight: blockchainTx.blockHeight,
txHash: blockchainTx.txHash,
timestamp: blockchainTx.timestamp,
memo: blockchainTx.memo || '',
status: blockchainTx.txStatus || 'unknown',
failures: blockchainTx.failures || []
};
}
const accountUpdates = transaction.transaction.accountUpdates || [];
this.transactionState.metadata.accountUpdates = accountUpdates.length;
accountUpdates.forEach((au) => {
this.processAccountUpdate(au);
});
};
this.processAccountUpdate = (au) => {
const auMetadata = this.extractAUMetadata(au);
if (au.authorization.proof) {
this.transactionState.metadata.totalProofs++;
}
else if (au.authorization.signature) {
this.transactionState.metadata.totalSignatures++;
}
/*if (auMetadata.type === 'proof') {
this.transactionState.metadata.totalProofs++;
} else if (auMetadata.type === 'signature') {
this.transactionState.metadata.totalSignatures++;
}*/
// Calculate fees
if (au.body.balanceChange) {
//const magnitude = au.body.balanceChange.magnitude.toString();
let magnitudeRaw = au.body.balanceChange.magnitude;
let magnitude;
if (typeof magnitudeRaw === "bigint") {
magnitude = magnitudeRaw;
}
else if (typeof magnitudeRaw === "string") {
if (/^\d+$/.test(magnitudeRaw)) {
// String contains an integer (e.g., "1000000000"), convert directly
magnitude = BigInt(magnitudeRaw);
}
else if (/^\d+\.\d+$/.test(magnitudeRaw)) {
// String contains a decimal (e.g., "0.467684313"), convert safely
const magnitudeFloat = parseFloat(magnitudeRaw);
magnitude = BigInt(Math.round(magnitudeFloat * 1e9)); // Convert MINA to nanomina
}
else {
throw new Error(`Unexpected magnitude string format: ${magnitudeRaw}`);
}
}
else if (typeof magnitudeRaw === "number") {
// If it's already a float, multiply and round before converting
const magnitudeInteger = Math.round(magnitudeRaw * 1e9); // Convert MINA to nanomina
magnitude = BigInt(magnitudeInteger);
}
else if (typeof magnitudeRaw === "object" && magnitudeRaw !== null && "value" in magnitudeRaw) {
// If the object contains a `value` field, extract it
//console.log("DEBUG: magnitudeRaw keys:", Object.keys(magnitudeRaw));
// ✅ Handle case where magnitudeRaw is already UInt64
if (magnitudeRaw instanceof UInt64) {
magnitude = magnitudeRaw.toBigInt();
//console.log("DEBUG: magnitudeRaw is already UInt64, extracted BigInt:", magnitude);
}
else if (magnitudeRaw.value instanceof Field) {
magnitude = magnitudeRaw.value.toBigInt();
//console.log("DEBUG: Converted Field to BigInt:", magnitude);
}
else {
throw new Error(`Unexpected magnitude type inside object: ${typeof magnitudeRaw.value}, value: ${magnitudeRaw.value}`);
}
}
else {
throw new Error(`Unexpected magnitude type or structure: ${typeof magnitudeRaw}, value: ${JSON.stringify(magnitudeRaw)}`);
}
//console.log("DEBUG: Converted magnitude to BigInt:", magnitude)
// If balance change is negative, it's a fee
if (au.body.balanceChange.isNegative()) {
// Convert to nanomina
//const feesInNanomina = BigInt(magnitude);
const feesInNanomina = magnitude;
// Convert current total from MINA to nanomina for calculation
const currentTotalNanomina = this.transactionState.metadata.totalFees === 0
//? BigInt(0)
? 0n
: BigInt(Math.round(Number(this.transactionState.metadata.totalFees) * 1e9));
const newTotalNanomina = currentTotalNanomina + feesInNanomina;
// Convert back to MINA and store as number
this.transactionState.metadata.totalFees = Number(newTotalNanomina) / 1000000000;
}
}
if (!this.transactionState.nodes.has(auMetadata.id)) {
const nodeType = this.determineNodeType(au);
// Extract additional metadata
const failed = au.metadata?.failed || false;
const failureReason = au.metadata?.failureReason;
const tokenId = au.metadata?.tokenId;
const callDepth = au.metadata?.callDepth || 0;
const node = {
id: auMetadata.id,
type: nodeType,
label: auMetadata.label,
publicKey: auMetadata.publicKey,
contractType: this.extractContractType(au),
failed,
failureReason,
tokenId,
callDepth
};
this.transactionState.nodes.set(auMetadata.id, node);
}
this.auAnalyzer.processAccountUpdate(au);
this.updateBalanceState(auMetadata);
};
this.extractAUMetadata = (au) => {
// Extract label, checking if it's a failed operation
let label = au.label || 'Unnamed Update';
if (au.metadata?.failed) {
label = `[FAILED] ${label}`;
}
return {
id: au.id.toString(),
label: au.label || 'Unnamed Update',
type: this.determineAuthorizationType(au),
publicKey: au.body.publicKey.toBase58(),
balanceChange: au.body.balanceChange.toString(),
methodName: au.lazyAuthorization?.methodName,
args: au.lazyAuthorization?.args,
// Additional metadata
failed: au.metadata?.failed || false,
tokenId: au.metadata?.tokenId,
callDepth: au.metadata?.callDepth || 0
};
};
this.extractContractType = (au) => {
if (au.label) {
return au.label;
}
/*// Look for contract type patterns in the label
if (au.label.includes('FungibleTokenAdmin')) {
return 'FungibleTokenAdmin';
}
if (au.label.includes('FungibleToken')) {
return 'FungibleToken';
}
if (au.label.includes('TokenEscrow')) {
return 'TokenEscrow';
}
if (au.label.includes('ZkApp')) {
return 'SmartContract';
}*/
return undefined;
};
this.determineNodeType = (au) => {
const isContract = this.isContractAccount(au);
if (isContract) {
return 'contract';
}
return 'account';
};
this.updateBalanceState = (auMetadata) => {
const currentBalance = this.transactionState.balanceStates.get(auMetadata.id) || [0];
// Get the last known balance
const lastBalance = currentBalance[currentBalance.length - 1] ?? 0;
//console.log("DEBUG: Converting balanceChange to BigInt:", auMetadata.balanceChange, "Type:", typeof auMetadata.balanceChange);
const balanceChange = auMetadata.balanceChange
? BigInt(Math.round(Number(auMetadata.balanceChange) * 1e9))
: 0n;
const newBalance = BigInt(lastBalance?.toString()) + balanceChange;
currentBalance.push(Number(newBalance));
this.transactionState.balanceStates.set(auMetadata.id, currentBalance);
};
this.clearTransactionState = () => {
this.transactionState = {
nodes: new Map(),
edges: [],
balanceStates: new Map(),
metadata: {
totalProofs: 0,
totalSignatures: 0,
totalFees: 0,
accountUpdates: 0
},
relationships: new Map(),
blockchainData: undefined
};
// Also reset the account update analyzer
this.auAnalyzer.reset();
};
this.getTransactionState = (transaction) => {
if (!this.transactionState) {
this.clearTransactionState();
}
else {
this.transactionState.nodes = new Map();
this.transactionState.edges = [];
this.transactionState.relationships = new Map();
this.transactionState.blockchainData = undefined;
}
// Check if this is a blockchain transaction and adapt it if needed
const isBlockchainTx = !transaction?.transaction?.accountUpdates &&
(transaction.updatedAccounts || transaction.txHash);
// Use the adapter if this is a blockchain transaction
const processableTx = isBlockchainTx ?
adaptBlockchainTransaction(transaction) :
transaction;
this.traverseTransaction(processableTx);
// Reset the account update analyzer before processing
this.auAnalyzer.reset();
const accountUpdates = processableTx.transaction.accountUpdates || [];
accountUpdates.forEach((au) => {
this.auAnalyzer.processAccountUpdate(au);
});
const auRelationships = this.auAnalyzer.getRelationships();
const plainRelationships = new Map();
auRelationships.forEach((rel, key) => {
const contractName = rel.method?.contract || '';
const contractAnalysis = this.contractAnalyzer.getContract(contractName);
const stateNames = contractAnalysis
? contractAnalysis.stateFields.map(f => f.name).join(', ')
: '';
const expandedChildren = Array.isArray(rel.children) && rel.children.length > 0
? rel.children.join(', ')
: '';
let expandedMethod = 'N/A';
if (rel.method) {
if (typeof rel.method === 'object') {
expandedMethod = `Contract: ${rel.method.contract ?? ''}, Method: ${rel.method.name ?? ''}`;
}
else {
expandedMethod = String(rel.method);
}
}
let expandedStateChanges = 'No State Changes';
if (Array.isArray(rel.stateChanges) && rel.stateChanges.length > 0) {
expandedStateChanges = rel.stateChanges
.map(change => {
if (typeof change === 'object') {
return `Field: ${change.field ?? ''}, Value: ${typeof change.value === 'object'
? (change.value?.value ?? '0')
: (change.value ?? '0')}`;
}
else {
return String(change);
}
})
.join(' | ');
}
plainRelationships.set(key, {
...rel,
children: expandedChildren,
method: expandedMethod,
onChainStates: stateNames,
stateChanges: expandedStateChanges
});
});
const expandedEdges = this.buildEdgesFromRelationships(auRelationships)
.map(edge => {
const operation = edge.operation;
const amountValue = (typeof operation.amount?.value === 'number' && !isNaN(operation.amount.value))
? operation.amount.value
: 0;
const denomination = operation.amount?.denomination || 'unknown';
// Build the amount string if amount exists
const amount = operation.amount
? `, Amount: ${amountValue} ${denomination}`
: '';
// Validate fee
const fee = (typeof operation.fee === 'number' || typeof operation.fee === 'string')
? `, Fee: ${operation.fee}`
: '';
const status = edge.failed ? 'failed' : (operation.status ?? 'success');
const flattenedOperation = `Sequence: ${operation.sequence ?? 'N/A'}, Type: ${operation.type ?? 'N/A'}, Status: ${operation.status ?? 'N/A'}${amount}${fee}`;
return {
id: edge.id,
fromNode: edge.fromNode,
toNode: edge.toNode,
operation: flattenedOperation,
failed: edge.failed,
failureReason: edge.failureReason
};
});
/*const state = {
nodes: Object.fromEntries(this.transactionState.nodes),
edges: expandedEdges,
balanceStates: Object.fromEntries(this.transactionState.balanceStates),
metadata: this.transactionState.metadata,
relationships: Object.fromEntries(plainRelationships)
};*/
const finalState = {
nodes: this.transactionState.nodes,
edges: expandedEdges,
balanceStates: this.transactionState.balanceStates,
/*metadata: {
...this.transactionState.metadata,
totalFees: this.getTotalFeesInMina() // Add here to convert to MINA
},*/
metadata: this.transactionState.metadata,
relationships: plainRelationships,
// Add metadata from blockchain transaction if available
blockchainData: this.transactionState.blockchainData
};
//this.transactionSnapshots = [...this.transactionSnapshots, state];
this.transactionSnapshots = [...this.transactionSnapshots, finalState];
return finalState;
};
this.getBlockchainTransactionState = (blockchainTx) => {
return this.getTransactionState(blockchainTx);
};
this.getTransactions = (...transactionStates) => {
for (const txState of transactionStates) {
if (txState) {
this.getTransactionState(txState);
}
}
};
this.getBlockchainTxnStateWithFlowAnalysis = (blockchainTx) => {
// First get normal transaction state
const txState = this.getBlockchainTransactionState(blockchainTx);
const onchainFlowAnalyzer = new BlockchainFlowAnalyzer();
// Then enhance it with flow analysis
return onchainFlowAnalyzer.enhanceTransactionState(txState, blockchainTx);
};
this.auAnalyzer = new AccountUpdateAnalyzer();
this.contractAnalyzer = new SmartContractAnalyzer();
this.contractAnalysis = new Map();
this.transactionState = {
nodes: new Map(),
edges: [],
balanceStates: new Map(),
metadata: {
totalProofs: 0,
totalSignatures: 0,
totalFees: 0,
accountUpdates: 0
},
relationships: new Map(),
blockchainData: undefined
};
}
initializeContracts(contracts) {
contracts.forEach(contract => {
this.contractAnalyzer.analyzeContractInstance(contract);
});
this.contractAnalysis = this.contractAnalyzer.getContracts();
}
getContractAnalysis() {
return this.contractAnalysis;
}
getContractAnalysisFor(contractName) {
return this.contractAnalysis.get(contractName);
}
getTotalFeesInMina() {
const feesInNanomina = BigInt(this.transactionState.metadata.totalFees);
const feesInMina = Number(feesInNanomina) / 1e9;
return Number(feesInMina);
}
determineAuthorizationType(au) {
if (au.lazyAuthorization?.kind === 'lazy-proof')
return 'proof';
if (au.lazyAuthorization?.kind === 'lazy-signature')
return 'signature';
return 'none';
}
isContractAccount(au) {
//console.log(`Verification Key of ${au.body.publicKey.toBase58()} : ${au.body?.update?.verificationKey.value.hash}`)
//console.log(`Auth kind of ${au.body.publicKey.toBase58()} : ${au.lazyAuthorization?.kind}`)
// 1. Check the label for contract indicators
if (au.label) {
const labelLower = au.label.toLowerCase();
//console.log('Label: ', labelLower)
if (labelLower.includes('contract') ||
labelLower.includes('zkapp') ||
labelLower.includes('deploy')) {
return true;
}
}
//2. Check for verification key updates
// Contracts typically have verification keys
if (au.body?.update?.verificationKey.value.data) {
return true;
}
/*
// 3. Check for proof-based permissions
// Contracts typically use proofs for authorization
if (au.body?.update?.permissions.value) {
const permissions = au.body.update.permissions.value;
const requiresProof = (
(
permissions.editState.constant.toBoolean() === false &&
permissions.editState?.signatureNecessary?.toBoolean() === false &&
permissions.editState?.signatureSufficient?.toBoolean() === false
) || (
permissions.send?.constant?.toBoolean() === false &&
permissions.send?.signatureNecessary?.toBoolean() === false &&
permissions.send?.signatureSufficient?.toBoolean() === false
)
)
if (requiresProof) {
return true;
}
}*/
// 4. Check authorization type
// Contracts often use proof-based authorization
if (au.lazyAuthorization?.kind === 'lazy-proof') {
return true;
}
// If none of the above, it's probably a regular account
return false;
}
buildEdgesFromRelationships(relationships) {
const edges = [];
let sequence = 1;
relationships.forEach(relationship => {
if (relationship.parentId) {
const edge = {
id: `op${sequence++}`,
fromNode: relationship.parentId,
toNode: relationship.id,
operation: {
sequence,
type: relationship.method?.name || 'update',
status: 'success'
}
};
if (relationship.stateChanges?.length) {
edge.operation.amount = {
value: Number(relationship.stateChanges[0].value),
denomination: 'USD'
};
}
edges.push(edge);
}
});
return edges;
}
getStateHistory() {
return this.transactionSnapshots;
}
}
//# sourceMappingURL=autrace.js.map