Bootstrap Bootstrap
            {
   0xb20a534816befbdf9867f03817473eded9884fef772f2fdaf7af3444266729cd: {
      data: {
         blockHash: "0x005498770261f4434741d0cf1dfa974209ba73a8612d9137eb0491ecc252ff63",
         blockNumber: "1",
         transactionIndex: 0,
         hash: "0xb20a534816befbdf9867f03817473eded9884fef772f2fdaf7af3444266729cd",
         from: "0x0000000000000000000000000000000000000001",
         gasLimit: "1000000000000000000",
         gasPrice: "0",
         data: "0x",
         nonce: "0",
         to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113",
         value: "250000000000000000000",
         chainId: "3615",
         origin: "1651572503570",
         v: "",
         r: "",
         s: ""
      },
      hash: "0xb20a534816befbdf9867f03817473eded9884fef772f2fdaf7af3444266729cd",
      load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572503759", status: 1, gasUsed: "28281" }, 0x1964b5ae0f32659b2cc50f7aa8e2f2d7cb213197adb64008206f1b32ba49aa2f: { data: { blockHash: "0x005498770261f4434741d0cf1dfa974209ba73a8612d9137eb0491ecc252ff63", blockNumber: "1", transactionIndex: 1, hash: "0x1964b5ae0f32659b2cc50f7aa8e2f2d7cb213197adb64008206f1b32ba49aa2f", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1073150", gasPrice: "1000000000", data: "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000028c000000000000000000000000000000000000000000000000000000000000028532f2a0a4e616d653a205772617070656420424e42203d3e2057726170706564204b41524d410a56657273696f6e3a20302e302e330a536f757263653a0a202020202d2057424e423a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d6661726d2f626c6f622f6d61737465722f636f6e7472616374732f6c6962732f57424e422e736f6c0a4465706c6f793a2068747470733a2f2f6273637363616e2e636f6d2f616464726573732f3078626234436442394342643336423031624431634261454246324465303864393137336263303935630a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a0a0a0a636c61737320574b41524d4120657874656e6473204552433230546f6b656e207b0a202020206e616d65203d202757726170706564204b41524d41273b0a2020202073796d626f6c203d2027574b41524d41273b0a202020200a202020200a202020206465706f7369742829207b0a2020202020202020696620282120286d73672e73656e64657220696e20746869732e62616c616e6365732929207b0a202020202020202020202020746869732e62616c616e6365735b6d73672e73656e6465725d203d206e756d6265722830293b0a20202020202020207d0a0a2020202020202020746869732e62616c616e6365735b6d73672e73656e6465725d203d20746869732e62616c616e6365735b6d73672e73656e6465725d2e616464286d73672e76616c7565293b0a2020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e616464286d73672e76616c7565293b0a0a20202020202020207265717569726528746869732e746f74616c537570706c792e657128616464726573732874686973292e62616c616e6365292c2027544f54414c5f535550504c595f4d49534d4154434827293b0a2020202020202020746869732e647261696e28293b0a2020202020202020656d697428274465706f736974272c205b6d73672e73656e6465722c206d73672e76616c75655d293b0a202020207d0a0a0a202020204465706f7369742873656e6465722c2076616c756529207b7d202f2f206576656e740a0a0a2020202077697468647261772877616429207b0a202020202020202072657175697265286d73672e73656e64657220696e20746869732e62616c616e636573202626202120746869732e62616c616e6365735b6d73672e73656e6465725d2e6c742877616429293b0a2020202020202020746869732e647261696e28293b0a0a2020202020202020746869732e62616c616e6365735b6d73672e73656e6465725d203d20746869732e62616c616e6365735b6d73672e73656e6465725d2e73756228776164293b0a2020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228776164293b0a0a202020202020202061646472657373286d73672e73656e646572292e7472616e7366657228776164293b0a0a20202020202020207265717569726528746869732e746f74616c537570706c792e657128616464726573732874686973292e62616c616e6365292c2027544f54414c5f535550504c595f4d49534d4154434827293b0a2020202020202020656d697428275769746864726177616c272c205b6d73672e73656e6465722c207761645d293b0a202020207d0a0a0a202020205769746864726177616c2873656e6465722c2076616c756529207b7d202f2f206576656e740a202020200a0a20202020647261696e2829207b0a202020202020202069662028616464726573732874686973292e62616c616e63652e677428746869732e746f74616c537570706c792929207b0a202020202020202020202020636f6e7374206f76657242616c616e6365203d20616464726573732874686973292e62616c616e63652e73756228746869732e746f74616c537570706c79293b0a2020202020202020202020206164647265737328746869732e6f776e6572292e7472616e73666572286f76657242616c616e6365293b0a202020202020202020202020656d69742827447261696e272c205b6d73672e73656e6465722c206f76657242616c616e63655d293b0a20202020202020207d0a202020207d0a0a0a20202020447261696e2873656e6465722c2076616c756529207b7d202f2f206576656e740a0a7d0a0a0a2f2f204576656e74730a574b41524d412e70726f746f747970652e4465706f7369742e6576656e74203d20747275653b0a574b41524d412e70726f746f747970652e4465706f7369742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a574b41524d412e70726f746f747970652e5769746864726177616c2e6576656e74203d20747275653b0a574b41524d412e70726f746f747970652e5769746864726177616c2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a574b41524d412e70726f746f747970652e447261696e2e6576656e74203d20747275653b0a574b41524d412e70726f746f747970652e447261696e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a574b41524d412e70726f746f747970652e6465706f7369742e70617961626c65203d20747275653b0a0a574b41524d412e70726f746f747970652e77697468647261772e696e70757473203d205b2775696e74323536275d3b0a0a0a72657475726e20574b41524d413b0a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "0", to: "0x", value: "0", chainId: "3615", origin: "1651572502964", v: "1c62", r: "e3db003d21373351623e8b936590ad7a0bc1da1ce0bf6902d27d57ad16faecc7", s: "40d44b6306b2e3c59f2b18cd11143707bac54b876218af8defda8cdff57ea39d" }, hash: "0x1964b5ae0f32659b2cc50f7aa8e2f2d7cb213197adb64008206f1b32ba49aa2f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572503759", status: 1, gasUsed: "43025" }, 0x04abb86e2b5e444838e6b3bd8ba622183c57a135c08f96fff703520d4d5a886b: { data: { blockHash: "0x00a4160dc73d5ae90b2375fe405b56005fa5693009c9c23d62b129f878aec822", blockNumber: "2", transactionIndex: 0, hash: "0x04abb86e2b5e444838e6b3bd8ba622183c57a135c08f96fff703520d4d5a886b", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "1", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572508924", v: "", r: "", s: "" }, hash: "0x04abb86e2b5e444838e6b3bd8ba622183c57a135c08f96fff703520d4d5a886b", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572509409", status: 1, gasUsed: "882" }, 0x5f688782a2de8e88327e6930d4133fd833252e1255a7595d619964a2f58bba67: { data: { blockHash: "0x00a4160dc73d5ae90b2375fe405b56005fa5693009c9c23d62b129f878aec822", blockNumber: "2", transactionIndex: 1, hash: "0x5f688782a2de8e88327e6930d4133fd833252e1255a7595d619964a2f58bba67", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "679950", gasPrice: "1000000000", data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000099c0a2f2a0a4e616d653a2046617563657420436f6e74726163740a56657273696f6e3a20302e302e330a2a2f0a0a0a636c61737320466175636574207b0a20202020616c6c6f636174696f6e73203d207b7d3b202f2f206c6973746520646520416c6c6f636174696f6e203a207b746f74616c416d6f756e742c206c617374416d6f756e742c206c61737454696d657374616d707d20696e646578c3a9652070617220616464726573730a202020206d696e416d6f756e74506572446179203d206e756d6265722827313027202b202730303030303030303030303030303030303027293b202f2f2031300a202020206d6178416d6f756e74506572446179203d206e756d6265722827353027202b202730303030303030303030303030303030303027293b202f2f2035300a20202020746f74616c416c6c6f6361746564203d206e756d6265722830293b0a20202020616c6c6f636174696f6e7344656c6179203d2038363430303b0a0a0a2020202046617563657428726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a0a0a20202020676574467265654d6f6e657928726563697069656e7429207b0a2020202020202020636f6e737420746f6b656e416c6c6f636174696f6e73203d20746869732e616c6c6f636174696f6e73207c7c207b7d3b0a0a20202020202020207265717569726528616464726573732874686973292e62616c616e63652e67742830292c2022494e53554646494349454e545f4641554345545f42414c414e434522293b0a20202020202020200a2020202020202020696620282120726563697069656e7429207b0a2020202020202020202020205f7468726f7728274d495353494e475f53454e4445525f4144445245535327293b0a20202020202020207d0a0a202020202020202069662028726563697069656e7420696e20746f6b656e416c6c6f636174696f6e7320262620746f6b656e416c6c6f636174696f6e735b726563697069656e745d2e6c61737454696d657374616d70203e20446174652e6e6f772829202d20746869732e616c6c6f636174696f6e7344656c6179202a203130303029207b0a2020202020202020202020205f7468726f772827544f4f5f534f4f4e27293b0a20202020202020207d0a0a2020202020202020636f6e737420646966664d696e4d6178203d20746869732e6d6178416d6f756e745065724461792e73756228746869732e6d696e416d6f756e74506572446179293b0a2020202020202020636f6e737420726e64203d204d6174682e726f756e64284d6174682e72616e646f6d2829202a2031303030293b0a20202020202020206c657420616d6f756e74203d20746869732e6d696e416d6f756e745065724461792e6164642820646966664d696e4d61782e6d756c28726e64292e64697628313030302920293b0a20202020202020202f2f636f6e737420616d6f756e74203d206e756d62657228273230303030303030303030303030303030303027293b0a0a202020202020202069662028616d6f756e742e6c7428746869732e6d696e416d6f756e745065724461792929207b0a202020202020202020202020616d6f756e74203d20746869732e6d696e416d6f756e745065724461793b0a20202020202020207d0a0a202020202020202069662028616d6f756e742e677428616464726573732874686973292e62616c616e63652929207b0a202020202020202020202020616d6f756e74203d20616464726573732874686973292e62616c616e63653b0a20202020202020207d0a0a202020202020202069662028616d6f756e742e677428746869732e6d6178416d6f756e745065724461792929207b0a202020202020202020202020616d6f756e74203d20746869732e6d6178416d6f756e745065724461793b0a20202020202020207d0a20202020202020200a202020202020202069662028212028726563697069656e7420696e20746f6b656e416c6c6f636174696f6e732929207b0a202020202020202020202020746f6b656e416c6c6f636174696f6e735b726563697069656e745d203d207b0a20202020202020202020202020202020746f74616c416d6f756e743a206e756d6265722830292c0a202020202020202020202020202020206c617374416d6f756e743a206e756d6265722830292c0a202020202020202020202020202020206c61737454696d657374616d703a206e756d6265722830292c0a2020202020202020202020207d3b0a20202020202020207d0a0a0a2020202020202020746f6b656e416c6c6f636174696f6e735b726563697069656e745d2e746f74616c416d6f756e74203d20746f6b656e416c6c6f636174696f6e735b726563697069656e745d2e746f74616c416d6f756e742e61646428616d6f756e74293b0a2020202020202020746f6b656e416c6c6f636174696f6e735b726563697069656e745d2e6c617374416d6f756e74203d20616d6f756e743b0a2020202020202020746f6b656e416c6c6f636174696f6e735b726563697069656e745d2e6c61737454696d657374616d70203d20446174652e6e6f7728293b0a0a20202020202020206164647265737328726563697069656e74292e7472616e7366657228616d6f756e74293b202f2f2073656e642066726565206d6f6e657920746f20726563697069656e740a0a202020202020202061646472657373286d73672e73656e646572292e7472616e7366657228206e756d62657228316531382e746f537472696e6728292920293b202f2f2073656e64206d6f6e657920666f7220676173207061796d656e74207468616e6b730a0a2020202020202020746869732e746f74616c416c6c6f6361746564203d20746869732e746f74616c416c6c6f63617465642e61646428616d6f756e74293b0a0a2020202020202020656d69742827466175636574272c205b726563697069656e742c20616d6f756e745d293b0a0a202020202020202072657475726e205b616d6f756e745d3b0a202020207d0a7d0a0a2f2f204576656e74730a4661756365742e70726f746f747970652e4661756365742e6576656e74203d20747275653b0a4661756365742e70726f746f747970652e4661756365742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4661756365742e70726f746f747970652e676574467265654d6f6e65792e696e70757473203d205b2761646472657373275d3b0a4661756365742e70726f746f747970652e676574467265654d6f6e65792e6f757470757473203d205b2775696e74323536275d3b0a0a0a72657475726e204661756365743b0a0000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "1", to: "0x", value: "0", chainId: "3615", origin: "1651572507298", v: "1c61", r: "52d7b13a7aa39476956574523caae17c0a7b9a6db8ce5d2d6f93cd892a9712fa", s: "16f7aa69823edaf5a24fb214149fd1a93223244a52397f767a9babb696ced4c1" }, hash: "0x5f688782a2de8e88327e6930d4133fd833252e1255a7595d619964a2f58bba67", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572509409", status: 1, gasUsed: "7139" }, 0x591adc77dc95f7fdcbf421db667bf86454d0dffcd9c9ecd7b29a726a7edb75ec: { data: { blockHash: "0x00159c1cfcf8717f506966ffcaa50f6f1ee46bf48ac7f60b1ee60979600d6436", blockNumber: "3", transactionIndex: 0, hash: "0x591adc77dc95f7fdcbf421db667bf86454d0dffcd9c9ecd7b29a726a7edb75ec", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "2", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572514573", v: "", r: "", s: "" }, hash: "0x591adc77dc95f7fdcbf421db667bf86454d0dffcd9c9ecd7b29a726a7edb75ec", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572515672", status: 1, gasUsed: "17662" }, 0x242f750a46ec75c386606f20ed7d4c2a1486cf0150919aef761010e89c5b43eb: { data: { blockHash: "0x00159c1cfcf8717f506966ffcaa50f6f1ee46bf48ac7f60b1ee60979600d6436", blockNumber: "3", transactionIndex: 1, hash: "0x242f750a46ec75c386606f20ed7d4c2a1486cf0150919aef761010e89c5b43eb", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1026150", gasPrice: "1000000000", data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000252000000000000000000000000000000000000000000000000000000000000024a72f2a0a4e616d653a20594f20546f6b656e0a56657273696f6e3a20302e302e330a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a0a636c61737320596f546f6b656e20657874656e6473204552433230546f6b656e207b2020200a202020206e616d65203d2027594f20546f6b656e273b0a2020202073796d626f6c203d2027594f273b0a0a20202020737461636b696e6753657276696365203d2027273b0a0a0a20202020696e697469616c697a6528737461636b696e67536572766963654164647265737329207b0a20202020202020202f2f7265717569726528746869732e737461636b696e6753657276696365203d3d2027272c2022414c52454144595f494e495449414c495a454422293b0a2020202020202020746869732e737461636b696e6753657276696365203d20737461636b696e6753657276696365416464726573732e746f4c6f7765724361736528293b0a0a2020202020202020636f6e737420696e6974416d6f756e74203d206e756d6265722827343230303030303027202b202730303030303030303030303030303030303027293b202f2f203432203030302030303020594f0a2020202020202020746869732e5f6d696e74286d73672e73656e6465722c20696e6974416d6f756e74293b0a0a2020202020202020656d69742827496e697469616c697a65272c205b737461636b696e6753657276696365416464726573735d293b0a202020207d0a0a0a20202020496e697469616c697a6528737461636b696e67536572766963654164647265737329207b7d202f2f206576656e740a0a0a202020206d696e7428726563697069656e742c20616d6f756e74546f4d696e7429207b0a20202020202020207265717569726528746869732e737461636b696e675365727669636520213d2027272c20224e4f545f494e495449414c495a454422293b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e737461636b696e67536572766963652c20224f4e4c595f535441434b494e475f5345525649434522293b0a2020202020202020746869732e5f6d696e7428726563697069656e742c20616d6f756e74546f4d696e74293b0a202020207d0a0a7d0a0a0a2f2f204576656e74730a596f546f6b656e2e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a596f546f6b656e2e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a596f546f6b656e2e70726f746f747970652e696e697469616c697a652e696e70757473203d205b2761646472657373275d3b0a2f2f596f546f6b656e2e70726f746f747970652e696e697469616c697a652e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a596f546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a0a0a0a72657475726e20596f546f6b656e3b0a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "2", to: "0x", value: "0", chainId: "3615", origin: "1651572512663", v: "1c61", r: "ba56678227bb332eb4d1c3f12cc364089430e2cb540e18d039f5f052c6d5bb4b", s: "38b0c26a130a04692fc548a89f8e69704dc4cf4133c357cd511ecd0cbb3717d5" }, hash: "0x242f750a46ec75c386606f20ed7d4c2a1486cf0150919aef761010e89c5b43eb", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572515672", status: 1, gasUsed: "8192" }, 0x27e73c42af396d9235a8853bbcefb4616a038980141c7a5b4c38ee527ff91477: { data: { blockHash: "0x00984edd40de5df064bb772c113d288d9d12599ba750074e6150184f409f87c5", blockNumber: "4", transactionIndex: 0, hash: "0x27e73c42af396d9235a8853bbcefb4616a038980141c7a5b4c38ee527ff91477", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "3", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572520847", v: "", r: "", s: "" }, hash: "0x27e73c42af396d9235a8853bbcefb4616a038980141c7a5b4c38ee527ff91477", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572522237", status: 1, gasUsed: "2870" }, 0x484d9c454cc34fc752903e492ce5c573118ba68824594a6fe0eabedd938361f6: { data: { blockHash: "0x00984edd40de5df064bb772c113d288d9d12599ba750074e6150184f409f87c5", blockNumber: "4", transactionIndex: 1, hash: "0x484d9c454cc34fc752903e492ce5c573118ba68824594a6fe0eabedd938361f6", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1034900", gasPrice: "1000000000", data: "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000025c0000000000000000000000000000000000000000000000000000000000000255d2f2a0a4e616d653a20537461636b696e67536572766963650a56657273696f6e3a20302e302e330a536f757263653a0a202d2043616b655661756c74203a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d736d6172742d636f6e7472616374732f626c6f622f6d61737465722f70726f6a656374732f63616b652d7661756c742f636f6e7472616374732f43616b655661756c742e736f6c0a202d20536d61727443686566496e697469616c697a61626c65203a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d736d6172742d636f6e7472616374732f626c6f622f6d61737465722f70726f6a656374732f736d617274636865662f76322f636f6e7472616374732f536d61727443686566496e697469616c697a61626c652e736f6c0a202d204d617374657243686566203a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d736d6172742d636f6e7472616374732f626c6f622f6d61737465722f70726f6a656374732f6661726d732d706f6f6c732f636f6e7472616374732f4d6173746572436865662e736f6c0a2a2f0a0a0a66756e6374696f6e2049455243323028746f6b656e4164647265737329207b0a2020202072657475726e206164647265737328746f6b656e41646472657373292e676574436f6e747261637428293b0a7d0a0a66756e6374696f6e20537461636b6572496e666f28737461636b6564416d6f756e742c207265776172644465627429207b0a20202020746869732e737461636b6564416d6f756e74203d20737461636b6564416d6f756e74207c7c206e756d6265722830293b0a20202020746869732e72657761726444656274203d2072657761726444656274207c7c206e756d6265722830293b0a7d0a0a0a636c61737320537461636b696e6753657276696365207b0a202020206f776e6572203d2027270a20202020737461636b6564546f6b656e203d2027273b0a20202020726577617264546f6b656e203d2027273b0a20202020746f74616c537461636b6564203d206e756d6265722830293b0a20202020616363526577617264506572537461636b546f6b656e203d206e756d6265722830293b0a20202020737461636b657273203d207b7d3b202f2f206d617070696e672861646472657373203d3e20537461636b6572496e666f290a202020206c6173744d696e74203d206e756d6265722830293b0a202020200a202020206d696e74506572696f64203d206e756d626572283630202a2031303030293b202f2f203630207365636f6e6473202831206d696e757465290a2020202072657761726473506572506572696f64203d206e756d626572282731303027202b202730303030303030303030303030303030303027293b202f2f2031303020546f6b656e20656d69747465642070657220706572696f64202873706c6974746564206f6e20616c6c20737461636b657273290a0a0a0a20202020496e697469616c697a6528737461636b6564546f6b656e2c20726577617264546f6b656e29207b7d202f2f206576656e740a2020202052657365742829207b7d202f2f206576656e740a202020204465706f7369742873656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020205769746864726177616c2873656e6465722c20616d6f756e742c20726577617264416d6f756e7429207b7d202f2f206576656e740a20202020557064617465506f6f6c2870656e64696e67526577617264732c206578747261526577617264506572537461636b546f6b656e29207b7d202f2f206576656e740a20202020557064617465537461636b657228737461636b6572416464726573732c20737461636b6572526577617264416d6f756e7429207b7d202f2f206576656e740a0a0a20202020696e697469616c697a6528737461636b6564546f6b656e2c20726577617264546f6b656e29207b0a20202020202020207265717569726528746869732e737461636b6564546f6b656e203d3d2027272c2022414c52454144595f494e495449414c495a454422293b0a2020202020202020746869732e737461636b6564546f6b656e203d20737461636b6564546f6b656e2e746f4c6f7765724361736528293b0a2020202020202020746869732e726577617264546f6b656e203d20726577617264546f6b656e2e746f4c6f7765724361736528293b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a0a2020202020202020656d69742827496e697469616c697a65272c205b737461636b6564546f6b656e2c20726577617264546f6b656e5d293b0a202020207d0a0a0a2020202072657365742829207b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e6f776e65722c20274f4e4c595f4f574e455227293b0a20202020202020207265717569726528746869732e737461636b6564546f6b656e20213d2027272c20274e4f545f494e495449414c495a454427293b0a2020202020202020746869732e737461636b657273203d207b7d3b0a2020202020202020746869732e746f74616c537461636b6564203d206e756d6265722830293b0a2020202020202020746869732e6c6173744d696e74203d206e756d6265722830293b0a2020202020202020746869732e616363526577617264506572537461636b546f6b656e203d206e756d6265722830293b0a0a2020202020202020636f6e7374205b62616c616e6365312c5d203d2049455243323028746869732e737461636b6564546f6b656e292e62616c616e63654f6628616464726573732874686973292e61646472657373293b0a20202020202020206966202862616c616e6365312e677428302929207b0a20202020202020202020202049455243323028746869732e737461636b6564546f6b656e292e7472616e7366657246726f6d28616464726573732874686973292e616464726573732c206d73672e73656e6465722c2062616c616e636531293b0a20202020202020207d0a0a2020202020202020636f6e7374205b62616c616e6365322c5d203d2049455243323028746869732e726577617264546f6b656e292e62616c616e63654f6628616464726573732874686973292e61646472657373293b0a20202020202020206966202862616c616e6365322e677428302929207b0a20202020202020202020202049455243323028746869732e726577617264546f6b656e292e7472616e7366657246726f6d28616464726573732874686973292e616464726573732c206d73672e73656e6465722c2062616c616e636532293b0a20202020202020207d0a0a2020202020202020656d697428275265736574272c205b5d293b0a202020207d0a0a202020200a2020202062616c616e63654f6628737461636b657229207b0a2020202020202020737461636b6572203d20737461636b65722e746f4c6f7765724361736528293b0a20202020202020206c657420737461636b6564416d6f756e74203d206e756d6265722830293b0a202020202020202069662028737461636b657220696e20746869732e737461636b65727329207b0a202020202020202020202020737461636b6564416d6f756e74203d20746869732e737461636b6572735b737461636b65725d2e737461636b6564416d6f756e743b0a20202020202020207d0a202020202020202072657475726e205b737461636b6564416d6f756e745d3b0a202020207d0a0a0a202020206465706f736974285f616d6f756e7429207b0a20202020202020202f2f20737461636b696e670a20202020202020207265717569726528746869732e737461636b6564546f6b656e20213d2027272c20274e4f545f494e495449414c495a454427293b0a202020202020202072657175697265285f616d6f756e742e67742830292c2027494e56414c49445f414d4f554e5427293b0a0a202020202020202069662028746869732e746f74616c537461636b65642e657128302929207b0a202020202020202020202020746869732e6c6173744d696e74203d20626c6f636b2e74696d657374616d703b0a20202020202020207d0a0a2020202020202020636f6e7374205f726577617264416d6f756e74203d20746869732e706179537461636b657252657761726473286d73672e73656e646572293b202f2f206d6973652061206a6f7572206465206c6120706f6f6c202b206f6e20656e636169737365206c6573207265776172647320706f74656e7469656c732c20706f7572207265706172746972207375722064657320636f6d7074657320c3a0207a65726f20c3a020706172746972206465206365206465706f7369740a0a202020202020202049455243323028746869732e737461636b6564546f6b656e292e7472616e7366657246726f6d286d73672e73656e6465722c20616464726573732874686973292e616464726573732c205f616d6f756e74293b0a0a2020202020202020696620282120286d73672e73656e64657220696e20746869732e737461636b6572732929207b0a202020202020202020202020746869732e737461636b6572735b6d73672e73656e6465725d203d206e657720537461636b6572496e666f3b0a20202020202020207d0a0a2020202020202020636f6e737420737461636b6572203d20746869732e737461636b6572735b6d73672e73656e6465725d3b0a2020202020202020737461636b65722e737461636b6564416d6f756e74203d20737461636b65722e737461636b6564416d6f756e742e616464285f616d6f756e74293b0a0a2020202020202020746869732e746f74616c537461636b6564203d20746869732e746f74616c537461636b65642e616464285f616d6f756e74293b0a0a2020202020202020737461636b65722e72657761726444656274203d20737461636b65722e737461636b6564416d6f756e742e6d756c28746869732e616363526577617264506572537461636b546f6b656e292e6469762831653132293b0a0a20202020202020202f2f746869732e5f6561726e28293b0a0a2020202020202020656d697428274465706f736974272c205b6d73672e73656e6465722c205f616d6f756e745d293b0a202020207d0a0a202020200a202020207769746864726177285f616d6f756e7429207b0a20202020202020202f2f20756e737461636b696e67203d3e205769746864726177207374616b656420746f6b656e7320616e6420636f6c6c6563742072657761726420746f6b656e730a20202020202020207265717569726528746869732e737461636b6564546f6b656e20213d2027272c20274e4f545f494e495449414c495a454427293b0a202020202020202072657175697265285f616d6f756e742e67742830292c20274e6f7468696e6720746f20776974686472617727293b0a2020202020202020726571756972652821205f616d6f756e742e677428746869732e746f74616c537461636b6564292c2027494e53554646494349454e545f544f54414c27293b0a202020202020202072657175697265286d73672e73656e64657220696e20746869732e737461636b6572732c2027494e56414c49445f4f574e455227293b0a2020202020202020726571756972652821205f616d6f756e742e677428746869732e737461636b6572735b6d73672e73656e6465725d2e737461636b6564416d6f756e74292c2027576974686472617720616d6f756e7420657863656564732062616c616e636527293b0a0a2020202020202020636f6e7374205f726577617264416d6f756e74203d20746869732e706179537461636b657252657761726473286d73672e73656e646572293b202f2f206d6973652061206a6f7572206465206c6120706f6f6c202b206f6e20656e636169737365206c6573207265776172647320706f74656e7469656c732c20706f7572207265706172746972207375722064657320636f6d7074657320c3a0207a65726f20c3a0207061727469722064652063652077697468647261770a20202020202020200a2020202020202020636f6e737420737461636b6572203d20746869732e737461636b6572735b6d73672e73656e6465725d3b0a2020202020202020737461636b65722e737461636b6564416d6f756e74203d20737461636b65722e737461636b6564416d6f756e742e737562285f616d6f756e74293b0a2020202020202020746869732e746f74616c537461636b6564203d20746869732e746f74616c537461636b65642e737562285f616d6f756e74293b0a0a202020202020202049455243323028746869732e737461636b6564546f6b656e292e7472616e73666572286d73672e73656e6465722c205f616d6f756e74293b0a0a2020202020202020737461636b65722e72657761726444656274203d20737461636b65722e737461636b6564416d6f756e742e6d756c28746869732e616363526577617264506572537461636b546f6b656e292e6469762831653132293b0a20202020202020200a2020202020202020656d697428275769746864726177616c272c205b6d73672e73656e6465722c205f616d6f756e742c205f726577617264416d6f756e745d293b0a202020207d0a0a200a2020202067657450656e64696e67526577617264732829207b0a20202020202020202f2f207265776172647320c3a0206d696e7465720a20202020202020206c65742070656e64696e6752657761726473203d206e756d6265722830293b0a0a202020202020202069662028746869732e746f74616c537461636b65642e677428302929207b0a202020202020202020202020636f6e7374206475726174696f6e203d20626c6f636b2e74696d657374616d702e73756228746869732e6c6173744d696e74293b202f2f206d696c6c697365636f6e647320646570756973206c65206465726e696572206d696e740a0a202020202020202020202020696620286475726174696f6e2e677428302929207b0a20202020202020202020202020202020636f6e737420706572696f6473436f756e74203d206475726174696f6e2e64697628746869732e6d696e74506572696f64293b202f2f206e62506572696f647320646570756973206c65206465726e696572206d696e740a202020202020202020202020202020200a2020202020202020202020202020202069662028706572696f6473436f756e742e677428302929207b0a202020202020202020202020202020202020202070656e64696e6752657761726473203d20706572696f6473436f756e742e6d756c28746869732e72657761726473506572506572696f64293b0a202020202020202020202020202020207d0a2020202020202020202020207d0a20202020202020207d0a0a202020202020202072657475726e205b70656e64696e67526577617264735d3b202f2f207265746f75726e65206c65206e62206465207265776172647320504f5556414e5420c38a545245206d696e74c3a9730a202020207d0a0a0a20202020757064617465506f6f6c2829207b0a2020202020202020636f6e7374205b70656e64696e67526577617264732c5d203d20746869732e67657450656e64696e675265776172647328293b0a20202020202020206966202870656e64696e67526577617264732e677428302929207b0a202020202020202020202020746869732e6c6173744d696e74203d20626c6f636b2e74696d657374616d703b0a20202020202020202020202049455243323028746869732e726577617264546f6b656e292e6d696e7428616464726573732874686973292e616464726573732c2070656e64696e6752657761726473293b202f2f206f6e206175676d656e7465206c612062616c616e636520726577617264546f6b656e2028706f7572206c652070726573656e7420636f6e7472616374290a2020202020202020202020200a202020202020202020202020636f6e7374206578747261526577617264506572537461636b546f6b656e203d2070656e64696e67526577617264732e6d756c2831653132292e64697628746869732e746f74616c537461636b6564293b0a202020202020202020202020746869732e616363526577617264506572537461636b546f6b656e203d20746869732e616363526577617264506572537461636b546f6b656e2e616464286578747261526577617264506572537461636b546f6b656e293b0a0a202020202020202020202020656d69742827557064617465506f6f6c272c205b70656e64696e67526577617264732c206578747261526577617264506572537461636b546f6b656e5d293b0a20202020202020207d0a202020207d0a0a0a20202020676574537461636b6572417661696c61626c655265776172647328737461636b65724164647265737329207b0a2020202020202020737461636b657241646472657373203d20737461636b6572416464726573732e746f4c6f7765724361736528293b0a20202020202020206c657420737461636b6572526577617264416d6f756e74203d206e756d6265722830293b0a0a2020202020202020636f6e7374205b70656e64696e67526577617264732c5d203d20746869732e67657450656e64696e675265776172647328293b0a20202020202020200a202020202020202069662028737461636b65724164647265737320696e20746869732e737461636b65727329207b0a202020202020202020202020636f6e7374206578747261526577617264506572537461636b546f6b656e203d2070656e64696e67526577617264732e6d756c2831653132292e64697628746869732e746f74616c537461636b6564293b0a202020202020202020202020636f6e737420616363526577617264506572537461636b546f6b656e203d20746869732e616363526577617264506572537461636b546f6b656e2e616464286578747261526577617264506572537461636b546f6b656e293b0a0a202020202020202020202020636f6e737420737461636b6572203d20746869732e737461636b6572735b737461636b6572416464726573735d3b0a202020202020202020202020737461636b6572526577617264416d6f756e74203d20737461636b65722e737461636b6564416d6f756e742e6d756c28616363526577617264506572537461636b546f6b656e292e6469762831653132292e73756228737461636b65722e72657761726444656274293b0a20202020202020207d0a0a202020202020202072657475726e205b737461636b6572526577617264416d6f756e745d3b0a202020207d0a0a0a20202020706179537461636b65725265776172647328737461636b65724164647265737329207b0a20202020202020202f2f20686172766573740a2020202020202020737461636b657241646472657373203d20737461636b6572416464726573732e746f4c6f7765724361736528293b0a0a2020202020202020746869732e757064617465506f6f6c28293b0a2020202020202020636f6e7374205b737461636b6572526577617264416d6f756e742c5d203d20746869732e5f757064617465537461636b657228737461636b657241646472657373293b0a0a202020202020202072657475726e205b737461636b6572526577617264416d6f756e745d3b0a202020207d0a0a0a202020205f757064617465537461636b657228737461636b65724164647265737329207b0a20202020202020206c657420737461636b6572526577617264416d6f756e74203d206e756d6265722830293b0a20202020202020200a202020202020202069662028737461636b65724164647265737320696e20746869732e737461636b65727329207b0a202020202020202020202020636f6e737420737461636b6572203d20746869732e737461636b6572735b737461636b6572416464726573735d3b0a2020202020202020202020205b737461636b6572526577617264416d6f756e742c205d203d20746869732e676574537461636b6572417661696c61626c655265776172647328737461636b657241646472657373293b0a202020200a202020202020202020202020737461636b65722e72657761726444656274203d20737461636b65722e737461636b6564416d6f756e742e6d756c28746869732e616363526577617264506572537461636b546f6b656e292e6469762831653132293b0a202020200a20202020202020202020202069662028737461636b6572526577617264416d6f756e742e677428302929207b0a2020202020202020202020202020202049455243323028746869732e726577617264546f6b656e292e7472616e7366657228737461636b6572416464726573732c20737461636b6572526577617264416d6f756e74293b0a0a20202020202020202020202020202020656d69742827557064617465537461636b6572272c205b737461636b6572416464726573732c20737461636b6572526577617264416d6f756e745d293b0a2020202020202020202020207d0a20202020202020207d0a0a202020202020202072657475726e205b737461636b6572526577617264416d6f756e745d3b0a202020207d0a0a7d0a0a2f2f204576656e74730a537461636b696e67536572766963652e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e557064617465537461636b65722e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e557064617465537461636b65722e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e52657365742e6576656e74203d20747275653b0a0a537461636b696e67536572766963652e70726f746f747970652e4465706f7369742e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e4465706f7369742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e5769746864726177616c2e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e5769746864726177616c2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e557064617465506f6f6c2e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e557064617465506f6f6c2e696e70757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e557064617465537461636b65722e6576656e74203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e557064617465537461636b65722e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a537461636b696e67536572766963652e70726f746f747970652e696e697469616c697a652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a537461636b696e67536572766963652e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e6465706f7369742e696e70757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e77697468647261772e696e70757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e67657450656e64696e67526577617264732e76696577203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e67657450656e64696e67526577617264732e6f757470757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e676574537461636b6572417661696c61626c65526577617264732e76696577203d20747275653b0a537461636b696e67536572766963652e70726f746f747970652e676574537461636b6572417661696c61626c65526577617264732e696e70757473203d205b2761646472657373275d3b0a537461636b696e67536572766963652e70726f746f747970652e676574537461636b6572417661696c61626c65526577617264732e6f757470757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e706179537461636b6572526577617264732e696e70757473203d205b2761646472657373275d3b0a537461636b696e67536572766963652e70726f746f747970652e706179537461636b6572526577617264732e6f757470757473203d205b2775696e74323536275d3b0a0a537461636b696e67536572766963652e70726f746f747970652e5f757064617465537461636b65722e696e7465726e616c203d20747275653b0a0a0a72657475726e20537461636b696e67536572766963653b0a0a00000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "3", to: "0x", value: "0", chainId: "3615", origin: "1651572518038", v: "1c61", r: "ab608e48519c177551b5c95bfa8957457ce09228fa08b7b0b9f969c50ca1cf99", s: "998d2eebee6018906c1a9e12337283ee402ad71d6be3d65c6ec8efc5de0d335" }, hash: "0x484d9c454cc34fc752903e492ce5c573118ba68824594a6fe0eabedd938361f6", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572522237", status: 1, gasUsed: "24473" }, 0x064db09f8ba8a213241f621442f62b9e1a69154aed2455cd68bd217f9a1e907b: { data: { blockHash: "0x00b78e2d71f5430ea94beb2f6001c4ed6a97e6916a8a000fe57accad16de9ee9", blockNumber: "5", transactionIndex: 0, hash: "0x064db09f8ba8a213241f621442f62b9e1a69154aed2455cd68bd217f9a1e907b", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "4", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572528074", v: "", r: "", s: "" }, hash: "0x064db09f8ba8a213241f621442f62b9e1a69154aed2455cd68bd217f9a1e907b", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572534826", status: 1, gasUsed: "3043" }, 0x86764232071da39aafa015bef38b5a40f390b01afa54744d0e3965cca608b472: { data: { blockHash: "0x00b78e2d71f5430ea94beb2f6001c4ed6a97e6916a8a000fe57accad16de9ee9", blockNumber: "5", transactionIndex: 1, hash: "0x86764232071da39aafa015bef38b5a40f390b01afa54744d0e3965cca608b472", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1154700", gasPrice: "1000000000", data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000002f200000000000000000000000000000000000000000000000000000000000002eb20a2f2a0a4e616d653a20594f204e46540a56657273696f6e3a20302e302e330a2a2f0a0a0a66756e6374696f6e2049455243373231526563656976657228746f6b656e4164647265737329207b0a2020202072657475726e206164647265737328746f6b656e41646472657373292e676574436f6e747261637428293b0a7d0a0a0a636c617373204552433732315265636569766572207b0a202020206f6e4552433732315265636569766564286f70657261746f722c2066726f6d2c20746f6b656e49642c206461746129207b0a2020202020202020636f6e73742073656c6563746f72203d202730786138666363343137273b202f2f206f6e45524337323152656365697665640a202020202020202072657475726e205b73656c6563746f725d3b0a202020207d3b0a7d0a0a45524337323152656365697665722e70726f746f747970652e6f6e45524337323152656365697665642e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536272c20276279746573275d3b0a45524337323152656365697665722e70726f746f747970652e6f6e45524337323152656365697665642e6f757470757473203d205b27627974657334275d3b0a0a0a0a636c6173732045524337323120657874656e6473204552433732315265636569766572207b2020200a202020206e616d65203d2027273b0a2020202073796d626f6c203d2027273b0a20202020646563696d616c73203d206e756d6265722830293b0a0a202020206f776e657273203d207b7d3b202f2f206d617070696e672875696e74323536203d3e2061646472657373290a2020202062616c616e636573203d207b7d3b202f2f206d617070696e672861646472657373203d3e2075696e74323536290a20202020746f6b656e417070726f76616c73203d207b7d3b202f2f206d617070696e672875696e74323536203d3e2061646472657373290a202020206f70657261746f72417070726f76616c73203d207b7d3b202f2f206d617070696e672861646472657373203d3e206d617070696e672861646472657373203d3e20626f6f6c29290a0a0a202020204d696e74286d696e7465722c20746f2c20746f6b656e496429207b7d202f2f206576656e740a202020204275726e2829207b6f776e65722c206275726e65722c20746f6b656e49647d202f2f206576656e740a202020205472616e736665722866726f6d2c20746f2c20746f6b656e496429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c20617070726f7665642c20746f6b656e496429207b7d202f2f206576656e740a20202020417070726f76616c466f72416c6c286f776e65722c206f70657261746f722c20617070726f76656429207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f72286e616d652c2073796d626f6c29207b0a2020202020202020737570657228293b0a2020202020202020746869732e6e616d65203d206e616d653b0a2020202020202020746869732e73796d626f6c203d2073796d626f6c3b0a202020207d0a0a202020200a20202020737570706f727473496e7465726661636528696e74657266616365496429207b0a20202020202020202f2f636f6e737420726573756c74203d20696e746572666163654964203d3d20273078383061633538636427207c7c20696e746572666163654964203d3d20273078356235653133396627207c7c2073757065722e737570706f727473496e7465726661636528696e746572666163654964293b0a2020202020202020636f6e737420726573756c74203d2028696e746572666163654964203d3d20273078383061633538636427207c7c20696e746572666163654964203d3d20273078356235653133396627293b0a202020202020202072657475726e205b726573756c745d3b0a202020207d0a0a0a2020202062616c616e63654f66286f776e65722920207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a202020202020202072657175697265286f776e657220213d20616464726573732830292e616464726573732c20224552433732313a2061646472657373207a65726f206973206e6f7420612076616c6964206f776e657222293b0a2020202020202020696620286f776e657220696e20746869732e62616c616e63657329207b0a20202020202020202020202072657475726e205b746869732e62616c616e6365735b6f776e65725d5d3b0a20202020202020207d0a202020202020202072657475726e205b4e756d6265722830295d3b0a202020207d0a0a0a202020206f776e65724f6628746f6b656e496429207b0a20202020202020207265717569726528746f6b656e496420696e20746869732e6f776e6572732c20224552433732313a206f776e657220717565727920666f72206e6f6e6578697374656e7420746f6b656e22293b0a2020202020202020636f6e7374206f776e6572203d20746869732e6f776e6572735b746f6b656e49645d3b0a202020202020202072657475726e205b6f776e65725d3b0a202020207d0a0a0a20202020746f6b656e55524928746f6b656e496429207b0a20202020202020207265717569726528746869732e5f65786973747328746f6b656e4964295b305d2c20224552433732314d657461646174613a2055524920717565727920666f72206e6f6e6578697374656e7420746f6b656e22293b0a0a2020202020202020636f6e73742062617365555249203d20746869732e6261736555524928295b305d3b0a2020202020202020636f6e7374205f746f6b656e557269203d20626173655552492e6c656e677468203e2030203f202862617365555249202b20746f6b656e49642e746f537472696e67282929203a2022223b0a202020202020202072657475726e205b5f746f6b656e5572695d3b0a202020207d0a0a0a202020205f626173655552492829207b0a202020202020202072657475726e205b22225d3b0a202020207d0a0a0a20202020617070726f766528746f2c20746f6b656e496429207b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528746869732e5f65786973747328746f6b656e4964295b305d2c20224552433732313a20617070726f76616c206e6f6e6578697374656e7420746f6b656e22293b0a2020202020202020636f6e7374206f776e6572203d20746869732e6f776e65724f6628746f6b656e4964295b305d3b0a20202020202020207265717569726528746f20213d206f776e65722c20224552433732313a20617070726f76616c20746f2063757272656e74206f776e657222293b0a0a202020202020202072657175697265280a2020202020202020202020206d73672e73656e646572203d3d206f776e6572207c7c20746869732e6973417070726f766564466f72416c6c286f776e65722c206d73672e73656e646572295b305d2c0a202020202020202020202020224552433732313a20617070726f76652063616c6c6572206973206e6f74206f776e6572206e6f7220617070726f76656420666f7220616c6c220a2020202020202020293b0a0a2020202020202020746869732e5f617070726f766528746f2c20746f6b656e4964293b0a202020207d0a0a0a20202020676574417070726f76656428746f6b656e496429207b0a20202020202020207265717569726528746869732e5f65786973747328746f6b656e4964295b305d2c20224552433732313a20617070726f76656420717565727920666f72206e6f6e6578697374656e7420746f6b656e22293b0a0a202020202020202069662028746f6b656e496420696e20746869732e746f6b656e417070726f76616c7329207b0a20202020202020202020202072657475726e205b20746869732e746f6b656e417070726f76616c735b746f6b656e49645d205d3b0a20202020202020207d0a0a202020202020202072657475726e205b206e756d626572283029205d3b0a202020207d0a0a0a20202020736574417070726f76616c466f72416c6c286f70657261746f722c20617070726f76656429207b0a20202020202020206f70657261746f72203d206f70657261746f722e746f4c6f7765724361736528293b0a2020202020202020746869732e5f736574417070726f76616c466f72416c6c286d73672e73656e6465722c206f70657261746f722c20617070726f766564293b0a202020207d0a0a0a202020206973417070726f766564466f72416c6c286f776e65722c206f70657261746f7229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020206f70657261746f72203d206f70657261746f722e746f4c6f7765724361736528293b0a2020202020202020696620286f776e657220696e20746869732e6f70657261746f72417070726f76616c73202626206f70657261746f7220696e20746869732e6f70657261746f72417070726f76616c735b6f776e65725d29207b0a20202020202020202020202072657475726e205b20746869732e6f70657261746f72417070726f76616c735b6f776e65725d5b6f70657261746f725d205d3b0a20202020202020207d0a202020202020202072657475726e205b66616c73655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2866726f6d2c20746f2c20746f6b656e496429207b0a202020202020202066726f6d203d2066726f6d2e746f4c6f7765724361736528293b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528746869732e5f6973417070726f7665644f724f776e6572286d73672e73656e6465722c20746f6b656e4964295b305d2c20224552433732313a207472616e736665722063616c6c6572206973206e6f74206f776e6572206e6f7220617070726f76656422293b0a2020202020202020746869732e5f7472616e736665722866726f6d2c20746f2c20746f6b656e4964293b0a202020207d0a0a0a20202020736166655472616e7366657246726f6d2866726f6d2c20746f2c20746f6b656e49642c205f6461746129207b0a202020202020202066726f6d203d2066726f6d2e746f4c6f7765724361736528293b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528746869732e5f6973417070726f7665644f724f776e6572286d73672e73656e6465722c20746f6b656e4964295b305d2c20224552433732313a207472616e736665722063616c6c6572206973206e6f74206f776e6572206e6f7220617070726f76656422293b0a2020202020202020746869732e5f736166655472616e736665722866726f6d2c20746f2c20746f6b656e49642c205f64617461293b0a202020207d0a0a0a202020205f736166655472616e736665722866726f6d2c20746f2c20746f6b656e49642c205f6461746129207b0a202020202020202066726f6d203d2066726f6d2e746f4c6f7765724361736528293b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a2020202020202020746869732e5f7472616e736665722866726f6d2c20746f2c20746f6b656e4964293b0a20202020202020207265717569726528746869732e5f636865636b4f6e45524337323152656365697665642866726f6d2c20746f2c20746f6b656e49642c205f64617461295b305d2c20224552433732313a207472616e7366657220746f206e6f6e20455243373231526563656976657220696d706c656d656e74657222293b0a202020207d0a0a0a202020205f65786973747328746f6b656e496429207b0a202020202020202072657475726e205b20746f6b656e496420696e20746869732e6f776e657273205d3b0a202020207d0a0a0a202020205f6973417070726f7665644f724f776e6572287370656e6465722c20746f6b656e49642920207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202072657175697265285f65786973747328746f6b656e4964292c20224552433732313a206f70657261746f7220717565727920666f72206e6f6e6578697374656e7420746f6b656e22293b0a2020202020202020636f6e7374206f776e6572203d20746869732e6f776e65724f6628746f6b656e4964295b305d3b0a2020202020202020636f6e737420726573756c74203d20287370656e646572203d3d206f776e6572207c7c20746869732e6973417070726f766564466f72416c6c286f776e65722c207370656e646572295b305d207c7c20746869732e676574417070726f76656428746f6b656e4964295b305d203d3d207370656e646572293b0a202020202020202072657475726e205b726573756c745d3b0a202020207d0a0a0a202020205f736166654d696e7428746f2c20746f6b656e49642c205f6461746129207b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a2020202020202020746869732e5f6d696e7428746f2c20746f6b656e4964293b0a202020202020202072657175697265280a202020202020202020202020746869732e5f636865636b4f6e455243373231526563656976656428616464726573732830292e616464726573732c20746f2c20746f6b656e49642c205f64617461295b305d2c0a202020202020202020202020224552433732313a207472616e7366657220746f206e6f6e20455243373231526563656976657220696d706c656d656e746572220a2020202020202020293b0a202020207d0a0a0a202020205f6d696e7428746f2c20746f6b656e496429207b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528746f20213d20616464726573732830292e616464726573732c20224552433732313a206d696e7420746f20746865207a65726f206164647265737322293b0a2020202020202020726571756972652821746869732e5f65786973747328746f6b656e4964295b305d2c20224552433732313a20746f6b656e20616c7265616479206d696e74656422293b0a0a2020202020202020746869732e5f6265666f7265546f6b656e5472616e7366657228616464726573732830292e616464726573732c20746f2c20746f6b656e4964293b0a0a2020202020202020746869732e62616c616e6365735b746f5d203d20746869732e62616c616e6365735b746f5d207c7c206e756d6265722830293b0a2020202020202020746869732e62616c616e6365735b746f5d203d20746869732e62616c616e6365735b746f5d2e6164642831293b0a2020202020202020746869732e6f776e6572735b746f6b656e49645d203d20746f3b0a0a2020202020202020656d697428274d696e74272c205b616464726573732830292e616464726573732c20746f2c20746f6b656e49645d293b0a0a2020202020202020746869732e5f6166746572546f6b656e5472616e7366657228616464726573732830292e616464726573732c20746f2c20746f6b656e4964293b0a202020207d0a0a0a202020205f6275726e28746f6b656e496429207b0a2020202020202020636f6e7374206f776e6572203d20746869732e6f776e65724f6628746f6b656e4964295b305d3b0a0a2020202020202020746869732e5f6265666f7265546f6b656e5472616e73666572286f776e65722c20616464726573732830292e616464726573732c20746f6b656e4964293b0a0a20202020202020202f2f20436c65617220617070726f76616c730a2020202020202020746869732e5f617070726f766528616464726573732830292e616464726573732c20746f6b656e4964293b0a0a2020202020202020746869732e62616c616e6365735b6f776e65725d203d20746869732e62616c616e6365735b6f776e65725d2e7375622831293b0a202020202020202064656c65746520746869732e6f776e6572735b746f6b656e49645d3b0a0a2020202020202020656d697428274275726e272c205b6f776e65722c20616464726573732830292e616464726573732c20746f6b656e49645d293b0a0a20202020202020205f6166746572546f6b656e5472616e73666572286f776e65722c20616464726573732830292e616464726573732c20746f6b656e4964293b0a202020207d0a0a0a202020205f7472616e736665722866726f6d2c20746f2c20746f6b656e496429207b0a202020202020202066726f6d203d2066726f6d2e746f4c6f7765724361736528293b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528746869732e6f776e65724f6628746f6b656e4964295b305d203d3d2066726f6d2c20224552433732313a207472616e736665722066726f6d20696e636f7272656374206f776e657222293b0a20202020202020207265717569726528746f20213d20616464726573732830292e616464726573732c20224552433732313a207472616e7366657220746f20746865207a65726f206164647265737322293b0a0a2020202020202020746869732e5f6265666f7265546f6b656e5472616e736665722866726f6d2c20746f2c20746f6b656e4964293b0a0a20202020202020202f2f20436c65617220617070726f76616c732066726f6d207468652070726576696f7573206f776e65720a2020202020202020746869732e5f617070726f766528616464726573732830292e616464726573732c20746f6b656e4964293b0a0a2020202020202020746869732e62616c616e6365735b66726f6d5d203d20746869732e62616c616e6365735b66726f6d5d2e7375622831293b0a2020202020202020746869732e62616c616e6365735b746f5d203d20746869732e62616c616e6365735b746f5d207c7c206e756d6265722830293b0a2020202020202020746869732e62616c616e6365735b746f5d203d20746869732e62616c616e6365735b746f5d2e6164642831293b0a2020202020202020746869732e6f776e6572735b746f6b656e49645d203d20746f3b0a0a2020202020202020656d697428275472616e73666572272c205b66726f6d2c20746f2c20746f6b656e49645d293b0a0a2020202020202020746869732e5f6166746572546f6b656e5472616e736665722866726f6d2c20746f2c20746f6b656e4964293b0a202020207d0a0a0a202020205f617070726f766528617070726f7665642c20746f6b656e496429207b0a2020202020202020617070726f766564203d20617070726f7665642e746f4c6f7765724361736528293b0a2020202020202020746869732e746f6b656e417070726f76616c735b746f6b656e49645d203d20617070726f7665643b0a2020202020202020656d69742827417070726f76616c272c205b746869732e6f776e65724f6628746f6b656e4964295b305d2c20617070726f7665642c20746f6b656e49645d293b0a202020207d0a0a0a202020205f736574417070726f76616c466f72416c6c286f776e65722c206f70657261746f722c20617070726f76656429207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020206f70657261746f72203d206f70657261746f722e746f4c6f7765724361736528293b0a202020202020202072657175697265286f776e657220213d206f70657261746f722c20224552433732313a20617070726f766520746f2063616c6c657222293b0a2020202020202020696620282120286f776e657220696e20746869732e6f70657261746f72417070726f76616c732929207b0a202020202020202020202020746869732e6f70657261746f72417070726f76616c735b6f776e65725d203d207b7d3b0a20202020202020207d0a2020202020202020746869732e6f70657261746f72417070726f76616c735b6f776e65725d5b6f70657261746f725d203d20617070726f7665643b0a2020202020202020656d69742827417070726f76616c466f72416c6c272c205b6f776e65722c206f70657261746f722c20617070726f7665645d293b0a202020207d0a0a0a202020205f636865636b4f6e45524337323152656365697665642866726f6d2c20746f2c20746f6b656e49642c205f6461746129207b0a202020202020202066726f6d203d2066726f6d2e746f4c6f7765724361736528293b0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a2020202020202020696620286164647265737328746f292e6973436f6e7472616374282929207b0a202020202020202020202020747279207b0a20202020202020202020202020202020636f6e73742072657476616c203d2049455243373231526563656976657228746f292e6f6e4552433732315265636569766564286d73672e73656e6465722c2066726f6d2c20746f6b656e49642c205f64617461295b305d3b0a20202020202020202020202020202020636f6e73742073656c6563746f72203d202730786138666363343137273b202f2f206f6e45524337323152656365697665640a2020202020202020202020202020202072657475726e205b72657476616c203d3d2073656c6563746f725d3b0a0a2020202020202020202020207d20636174636820286529207b0a20202020202020202020202020202020636f6e737420726561736f6e203d20652e6d6573736167653b0a2020202020202020202020202020202069662028726561736f6e2e6c656e677468203d3d203029207b0a202020202020202020202020202020202020202072657665727428224552433732313a207472616e7366657220746f206e6f6e20455243373231526563656976657220696d706c656d656e74657222293b0a0a202020202020202020202020202020207d20656c7365207b0a202020202020202020202020202020202020202072657665727428726561736f6e293b0a202020202020202020202020202020207d0a2020202020202020202020207d0a0a20202020202020207d20656c7365207b0a20202020202020202020202072657475726e205b747275655d3b0a20202020202020207d0a202020207d0a0a0a202020205f6265666f7265546f6b656e5472616e736665722866726f6d2c20746f2c20746f6b656e496429207b0a202020207d0a0a202020205f6166746572546f6b656e5472616e736665722866726f6d2c20746f2c20746f6b656e496429207b0a202020207d0a202020200a7d0a0a2f2f204576656e74730a4552433732312e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433732312e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433732312e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433732312e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433732312e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e417070726f76616c466f72416c6c2e6576656e74203d20747275653b0a4552433732312e70726f746f747970652e417070726f76616c466f72416c6c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433732312e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433732312e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433732312e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433732312e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433732312e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433732312e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433732312e70726f746f747970652e6f776e65724f662e76696577203d20747275653b0a4552433732312e70726f746f747970652e6f776e65724f662e696e70757473203d205b2775696e74323536275d3b0a4552433732312e70726f746f747970652e6f776e65724f662e6f757470757473203d205b2761646472657373275d3b0a0a4552433732312e70726f746f747970652e746f6b656e5552492e76696577203d20747275653b0a4552433732312e70726f746f747970652e746f6b656e5552492e696e70757473203d205b2775696e74323536275d3b0a4552433732312e70726f746f747970652e746f6b656e5552492e6f757470757473203d205b27737472696e67275d3b0a0a4552433732312e70726f746f747970652e5f626173655552492e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e676574417070726f7665642e76696577203d20747275653b0a4552433732312e70726f746f747970652e676574417070726f7665642e6f757470757473203d205b2761646472657373275d3b0a0a4552433732312e70726f746f747970652e736574417070726f76616c466f72416c6c2e696e70757473203d205b2761646472657373272c2027626f6f6c275d3b0a0a4552433732312e70726f746f747970652e6973417070726f766564466f72416c6c2e76696577203d20747275653b0a4552433732312e70726f746f747970652e6973417070726f766564466f72416c6c2e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a0a4552433732312e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a0a4552433732312e70726f746f747970652e736166655472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536272c20276279746573275d3b0a0a4552433732312e70726f746f747970652e5f736166655472616e736665722e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f6578697374732e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f6973417070726f7665644f724f776e65722e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f736166654d696e742e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f7472616e736665722e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f617070726f76652e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f736574417070726f76616c466f72416c6c2e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f636865636b4f6e45524337323152656365697665642e70726976617465203d20747275653b0a0a4552433732312e70726f746f747970652e5f6265666f7265546f6b656e5472616e736665722e696e7465726e616c203d20747275653b0a0a4552433732312e70726f746f747970652e5f6166746572546f6b656e5472616e736665722e696e7465726e616c203d20747275653b0a0a0a0a0a0a636c61737320596f4e465420657874656e647320455243373231207b0a202020206e616d65203d2027594f204e4654273b0a2020202073796d626f6c203d2027596f4e4654273b0a0a202020205f746f6b656e496473203d20303b0a202020205f4d41585f535550504c59203d2031303b0a0a202020206d696e747072696365203d206e756d62657228273127202b202730303030303030303030303030303030303027293b202f2f20312065746865720a0a0a202020205f626173655552492829207b0a2020202020202020636f6e73742062617365555249203d2027687474703a2f2f6c6f63616c686f73743a383534352f636f6c6c656374696f6e732f27202b20616464726573732874686973292e61646472657373202b20272f273b0a202020202020202072657475726e205b2062617365555249205d3b0a202020207d0a0a20202020746f6b656e55524928746f6b656e496429207b0a20202020202020207265717569726528746869732e5f65786973747328746f6b656e4964295b305d2c20224552433732314d657461646174613a2055524920717565727920666f72206e6f6e6578697374656e7420746f6b656e22293b0a0a2020202020202020636f6e73742062617365555249203d20746869732e6261736555524928295b305d3b0a2020202020202020636f6e7374205f746f6b656e557269203d20626173655552492e6c656e677468203e2030203f202862617365555249202b20746f6b656e49642e746f537472696e672829202b20272e6a736f6e2729203a2022223b0a202020202020202072657475726e205b5f746f6b656e5572695d3b0a202020207d0a0a202020206d696e7428726563656976657229207b0a20202020202020207265717569726528746869732e5f746f6b656e496473203c20746869732e5f4d41585f535550504c592c20274d617820737570706c79207265616368656427293b0a202020202020202072657175697265286d73672e76616c75652e657128746869732e6d696e747072696365292c2027496e76616c696420707269636527293b0a2020202020202020746869732e5f746f6b656e4964732b2b3b0a0a2020202020202020636f6e7374206e65774e6674546f6b656e4964203d20746869732e5f746f6b656e4964733b0a2020202020202020746869732e5f736166654d696e742872656365697665722c206e65774e6674546f6b656e49642c202727293b0a0a202020202020202072657475726e205b6e65774e6674546f6b656e49645d3b0a202020207d0a7d0a0a2f2f204d6574686f64730a596f4e46542e70726f746f747970652e746f6b656e5552492e696e70757473203d205b2775696e74323536275d3b0a596f4e46542e70726f746f747970652e746f6b656e5552492e6f757470757473203d205b27737472696e67275d3b0a596f4e46542e70726f746f747970652e746f6b656e5552492e76696577203d20747275653b0a0a596f4e46542e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373275d3b0a596f4e46542e70726f746f747970652e6d696e742e6f757470757473203d205b2775696e74323536275d3b0a596f4e46542e70726f746f747970652e6d696e742e6d6f64696669657273203d205b276f6e6c794f776e6572275d3b0a596f4e46542e70726f746f747970652e6d696e742e70617961626c65203d20747275653b0a0a0a72657475726e20596f4e46543b0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "4", to: "0x", value: "0", chainId: "3615", origin: "1651572527414", v: "1c61", r: "2fc3087e36c40e64e6c573d59067566a81b8901ae977aefb08004b58e0f65c6a", s: "8b960c42292215ee3c0bea851b0a5c797b3653c5960c3522d68d4a788c80abf" }, hash: "0x86764232071da39aafa015bef38b5a40f390b01afa54744d0e3965cca608b472", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572534826", status: 1, gasUsed: "32626" }, 0xdd7fdeab583b5a983e7327d948a4aed7c53e5bb868bdb2d2db2dd9218bd39b90: { data: { blockHash: "0x0097f4ce4ac7dd73246b361656d4f11539bfe8a6ae68479b06a1aefe52729dea", blockNumber: "6", transactionIndex: 0, hash: "0xdd7fdeab583b5a983e7327d948a4aed7c53e5bb868bdb2d2db2dd9218bd39b90", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "5", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572540008", v: "", r: "", s: "" }, hash: "0xdd7fdeab583b5a983e7327d948a4aed7c53e5bb868bdb2d2db2dd9218bd39b90", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572545092", status: 1, gasUsed: "2385" }, 0x2d9987bffd02b12a95167d157f4d345b1954b62f043e308b892e916e0b217308: { data: { blockHash: "0x0097f4ce4ac7dd73246b361656d4f11539bfe8a6ae68479b06a1aefe52729dea", blockNumber: "6", transactionIndex: 1, hash: "0x2d9987bffd02b12a95167d157f4d345b1954b62f043e308b892e916e0b217308", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "989450", gasPrice: "1000000000", data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000224000000000000000000000000000000000000000000000000000000000000021c92f2a0a4e616d653a20486f7420506f7461746f650a56657273696f6e3a20302e302e330a2a2f0a0a0a636c61737320486f74506f7461746f65207b0a202020206e616d65203d2027484f5420506f7461746f65273b0a2020202073796d626f6c203d2027504f5441544f45273b0a0a20202020646563696d616c73203d20303b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020696e69745072696365203d206e756d62657228273127202b202730303030303030303030303030303030303027293b202f2f20312065746865720a2020202061646d696e203d2027273b0a202020200a20202020506f7461746f6554797065203d207b0a20202020202020206964783a206e756d6265722830292c0a20202020202020206d696e7465723a2027272c0a20202020202020206f776e65723a2027272c0a2020202020202020646562746f723a2027272c0a202020202020202070726963653a206e756d6265722830292c0a2020202020202020706172656e744964783a206e756d626572282d31292c0a2020202020202020696e6974446174653a206e756d6265722830292c0a20202020202020207472616e73666572733a206e756d6265722830292c0a202020202020202063756d756c617469766550726963653a206e756d6265722830292c0a202020207d3b0a0a20202020706f7461746f6573203d205b5d3b202f2f206172726179206f6620506f7461746f650a202020207061727469636970616e7473203d207b7d3b202f2f206d617070696e67202861646472657373207061727469636970616e74203d3e206d617070696e672875696e7432353620706f7461746f65496478203d3e20737472696e6720726f6c6529290a0a0a20202020496e697469616c697a65286d696e7465724164647265737329207b7d202f2f206576656e740a202020204d696e7428706f7461746f654964782c206d696e74657229207b7d202f2f206576656e740a202020205472616e7366657228706f7461746f654964782c2073656e6465722c20746f29207b7d202f2f206576656e740a20202020537465616c28706f7461746f654964782c20737465616c657229207b7d202f2f206576656e740a202020205769746864726177616c28616d6f756e7429207b7d202f2f206576656e740a0a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e61646d696e203d206d73672e73656e6465723b0a202020207d0a0a20202020696e697469616c697a652829207b0a20202020202020202f2f72657175697265286d73672e73656e646572203d3d20746869732e61646d696e2c20224f4e4c595f41444d494e22293b0a20202020202020207265717569726528746869732e706f7461746f65732e6c656e677468203d3d20302c20224f4e4c595f4f4e434522293b0a2020202020202020746869732e5f6d696e74286d73672e73656e6465722c202d31293b0a0a2020202020202020656d69742827496e697469616c697a65272c205b6d73672e73656e6465725d293b0a202020207d0a0a0a20202020706f7461746f6573436f756e742829207b0a202020202020202072657475726e205b746869732e706f7461746f65732e6c656e6774685d3b0a202020207d0a0a202020205f6d696e74286d696e7465722c20706172656e7449647829207b0a20202020202020206d696e746572203d206d696e7465722e746f4c6f7765724361736528293b0a2020202020202020636f6e737420706f7461746f65496478203d20746869732e706f7461746f65732e6c656e6774683b0a0a2020202020202020636f6e737420706f7461746f65203d207b0a2020202020202020202020206964783a20706f7461746f654964782c0a2020202020202020202020206d696e7465722c0a2020202020202020202020206f776e65723a20616464726573732874686973292e616464726573732c0a202020202020202020202020646562746f723a206d696e7465722c0a20202020202020202020202070726963653a20746869732e696e697450726963652c0a202020202020202020202020706172656e744964782c0a202020202020202020202020696e6974446174653a206e756d62657228626c6f636b2e74696d657374616d70292c0a2020202020202020202020207472616e73666572733a206e756d6265722830292c0a20202020202020202020202063756d756c617469766550726963653a206e756d6265722830292c0a20202020202020207d3b0a2020202020202020746869732e706f7461746f65732e7075736828706f7461746f65293b0a2020202020202020746869732e5f736574486f6c64696e67286d696e7465722c20706f7461746f654964782c20276d696e7465722b646562746f7227293b0a2020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e6164642831293b0a0a2020202020202020656d697428274d696e74272c205b706f7461746f654964782c206d696e7465725d293b0a202020207d0a0a202020205f6e65787450726963652863757272656e74507269636529207b0a2020202020202020636f6e73742072657761726450657263656e74203d206e756d62657228313530293b202f2f20313530250a2020202020202020636f6e7374207072696365203d2063757272656e7450726963652e6d756c2872657761726450657263656e74292e64697628313030293b0a202020202020202072657475726e205b70726963655d3b0a202020207d0a0a20202020707269636528706f7461746f6549647829207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a2020202020202020636f6e7374207072696365203d20706f7461746f652e70726963653b0a202020202020202072657475726e205b70726963655d3b0a202020207d0a0a202020206e657874507269636528706f7461746f6549647829207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a2020202020202020636f6e7374207072696365203d20746869732e5f6e657874507269636528706f7461746f652e7072696365295b305d3b0a202020202020202072657475726e205b70726963655d3b0a202020207d0a0a202020206f776e657228706f7461746f6549647829207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a2020202020202020636f6e7374206f776e6572203d20706f7461746f652e6f776e65723b0a202020202020202072657475726e205b6f776e65725d3b0a202020207d0a0a20202020646562746f7228706f7461746f6549647829207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a2020202020202020636f6e737420646562746f72203d20706f7461746f652e646562746f723b0a202020202020202072657475726e205b646562746f725d3b0a202020207d0a0a202020207472616e7366657228706f7461746f654964782c20746f29207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465722e746f4c6f7765724361736528293b0a20202020202020207265717569726528706f7461746f652e646562746f72203d3d2073656e6465722c20224f4e4c595f444542544f5222293b0a0a202020202020202072657175697265286164647265737328746869732920213d20746f2c2022494e56414c49445f54415247455422293b0a202020202020202072657175697265286164647265737328302920213d20746f2c2022494e56414c49445f54415247455422293b0a0a2020202020202020746f203d20746f2e746f4c6f7765724361736528293b0a20202020202020207265717569726528706f7461746f652e6f776e657220213d20746f2c20224e4f5f4241434b545241434b5f544f5f4f574e455222293b0a20202020202020207265717569726528706f7461746f652e6d696e74657220213d20746f2c20224e4f5f4241434b545241434b5f544f5f4d494e54455222293b0a20202020202020200a2020202020202020636f6e7374207072696365203d20706f7461746f652e70726963653b0a2020202020202020726571756972652870726963652e6571286d73672e76616c7565292c2022494e56414c49445f504f5441544f455f505249434522293b0a0a2020202020202020636f6e737420747265617375726546656573203d206d73672e76616c75652e6d756c2831292e6469762831303030293b202f2f20302e3125206665657320666f72207468652074726561737572650a2020202020202020636f6e7374206d696e74657246656573203d206d73672e76616c75652e6d756c2839292e6469762831303030293b2020202f2f20302e3925206665657320666f7220746865206d696e7465720a20202020202020206164647265737328706f7461746f652e6d696e746572292e7472616e73666572286d696e74657246656573293b0a20202020202020206164647265737328706f7461746f652e6f776e6572292e7472616e73666572286d73672e76616c75652e73756228747265617375726546656573292e737562286d696e7465724665657329293b202f2f20726570617920282b626f6e75732d6665657329207468652070726576696f7573206f776e65720a0a202020202020202069662028706f7461746f652e6f776e6572203d3d20706f7461746f652e6d696e74657229207b0a202020202020202020202020746869732e5f736574486f6c64696e6728706f7461746f652e6f776e65722c20706f7461746f654964782c20276d696e7465722b7265746972656427293b0a20202020202020207d20656c73652069662028706f7461746f652e6f776e657220213d20616464726573732874686973292e6164647265737329207b0a202020202020202020202020746869732e5f736574486f6c64696e6728706f7461746f652e6f776e65722c20706f7461746f654964782c20277265746972656427293b0a20202020202020207d0a0a20202020202020206966202873656e646572203d3d20706f7461746f652e6d696e74657229207b0a202020202020202020202020746869732e5f736574486f6c64696e672873656e6465722c20706f7461746f654964782c20276d696e7465722b6f776e657227293b0a20202020202020207d20656c7365207b0a202020202020202020202020746869732e5f736574486f6c64696e672873656e6465722c20706f7461746f654964782c20276f776e657227293b0a20202020202020207d0a0a202020202020202069662028746f203d3d20706f7461746f652e6d696e74657229207b0a202020202020202020202020746869732e5f736574486f6c64696e6728746f2c20706f7461746f654964782c20276d696e7465722b646562746f7227293b0a20202020202020207d20656c7365207b0a202020202020202020202020746869732e5f736574486f6c64696e6728746f2c20706f7461746f654964782c2027646562746f7227293b0a20202020202020207d0a0a2020202020202020706f7461746f652e6f776e6572203d2073656e6465723b202f2f206e6577206f776e657220286f6c6420646562746f72290a2020202020202020706f7461746f652e646562746f72203d20746f3b202f2f206e657720646562746f720a2020202020202020706f7461746f652e7472616e7366657273203d20706f7461746f652e7472616e73666572732e6164642831293b0a0a20202020202020206966202873656e64657220213d20746f29207b0a202020202020202020202020706f7461746f652e63756d756c61746976655072696365203d20706f7461746f652e63756d756c617469766550726963652e616464286d73672e76616c7565293b0a20202020202020207d0a0a2020202020202020636f6e7374206e65775072696365203d20746869732e5f6e6578745072696365287072696365295b305d3b0a2020202020202020706f7461746f652e7072696365203d206e657750726963653b202f2f206e65772070726963650a0a2020202020202020656d697428275472616e73666572272c205b706f7461746f654964782c2073656e6465722c20746f5d293b0a20202020202020200a2020202020202020746869732e5f6d696e74286d73672e73656e6465722c20706f7461746f652e696478293b0a202020207d0a0a20202020686f6c64696e6773287061727469636970616e744164647265737329207b0a20202020202020207061727469636970616e7441646472657373203d207061727469636970616e74416464726573732e746f4c6f7765724361736528293b0a202020202020202072657175697265287061727469636970616e744164647265737320696e20746869732e7061727469636970616e74732c2022494e56414c49445f5041525449434950414e5422293b0a0a2020202020202020636f6e737420726573756c74203d204f626a6563742e656e747269657328746869732e7061727469636970616e74735b7061727469636970616e74416464726573735d292e6d61702828656e747279203d3e207b0a202020202020202020202020636f6e7374205b706f7461746f654964782c20726f6c655d203d20656e7472793b0a20202020202020202020202072657475726e20706f7461746f65496478202b20273a27202b20726f6c653b0a20202020202020207d29292e6a6f696e28272027293b0a202020202020202072657475726e205b726573756c745d3b0a202020207d0a0a202020205f736574486f6c64696e67287061727469636970616e74416464726573732c20706f7461746f654964782c20726f6c6529207b0a20202020202020207061727469636970616e7441646472657373203d207061727469636970616e74416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620282120287061727469636970616e744164647265737320696e20746869732e7061727469636970616e74732929207b0a202020202020202020202020746869732e7061727469636970616e74735b7061727469636970616e74416464726573735d203d207b7d3b0a20202020202020207d0a2020202020202020746869732e7061727469636970616e74735b7061727469636970616e74416464726573735d5b706f7461746f654964785d203d20726f6c653b0a202020207d0a0a20202020737465616c28706f7461746f6549647829207b0a20202020202020207265717569726528706f7461746f6549647820696e20746869732e706f7461746f65732c2022494e56414c49445f504f5441544f455f494422293b0a2020202020202020636f6e737420706f7461746f65203d20746869732e706f7461746f65735b706f7461746f654964785d3b0a0a2020202020202020636f6e737420737465616c6572203d206d73672e73656e6465722e746f4c6f7765724361736528293b0a20202020202020207265717569726528706f7461746f652e6f776e657220213d20737465616c65722c2022494e56414c49445f535445414c45525f4f574e455222293b0a20202020202020207265717569726528706f7461746f652e646562746f7220213d20737465616c65722c2022494e56414c49445f535445414c45525f444542544f5222293b0a0a2020202020202020636f6e7374205f737465616c5072696365203d20746869732e6e657874507269636528706f7461746f65496478295b305d3b0a202020202020202072657175697265285f737465616c50726963652e6571286d73672e76616c7565292c2022494e56414c49445f504f5441544f455f535445414c5f505249434522293b0a0a2020202020202020706f7461746f652e6f776e6572203d20737465616c65723b202f2f20737465616c6572206973206e6f7720746865206f776e6572203d3e2068652077696c6c206e6f742070617920746865207472616e7366657220287472616e7366657220746f2068696d73656c66290a2020202020202020706f7461746f652e646562746f72203d20737465616c65723b0a2020202020202020706f7461746f652e7072696365203d205f737465616c50726963653b0a0a2020202020202020706f7461746f652e63756d756c61746976655072696365203d20706f7461746f652e63756d756c617469766550726963652e616464286d73672e76616c7565293b0a0a2020202020202020746869732e5f736574486f6c64696e6728737465616c65722c20706f7461746f654964782c2027737465616c657227293b0a0a2020202020202020656d69742827537465616c272c205b706f7461746f654964782c20737465616c65725d293b0a202020207d0a0a20202020776974686472617728616d6f756e7429207b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e61646d696e2c20224f4e4c595f41444d494e22293b0a20202020202020206164647265737328746869732e61646d696e292e7472616e7366657228616d6f756e74293b0a2020202020202020656d697428275769746864726177616c272c205b616d6f756e745d293b0a202020207d0a0a7d0a0a2f2f204576656e74730a486f74506f7461746f652e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a486f74506f7461746f652e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564275d3b0a0a486f74506f7461746f652e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a486f74506f7461746f652e70726f746f747970652e4d696e742e696e70757473203d205b2775696e7432353620696e6465786564272c20276164647265737320696e6465786564275d3b0a0a486f74506f7461746f652e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a486f74506f7461746f652e70726f746f747970652e5472616e736665722e696e70757473203d205b2775696e7432353620696e6465786564272c20276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a486f74506f7461746f652e70726f746f747970652e537465616c2e6576656e74203d20747275653b0a486f74506f7461746f652e70726f746f747970652e537465616c2e696e70757473203d205b2775696e7432353620696e6465786564272c20276164647265737320696e6465786564275d3b0a0a486f74506f7461746f652e70726f746f747970652e5769746864726177616c2e6576656e74203d20747275653b0a486f74506f7461746f652e70726f746f747970652e5769746864726177616c2e696e70757473203d205b2775696e74323536275d3b0a0a0a2f2f204d6574686f64730a486f74506f7461746f652e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a486f74506f7461746f652e70726f746f747970652e5f6e65787450726963652e696e7465726e616c203d20747275653b0a0a486f74506f7461746f652e70726f746f747970652e7472616e736665722e70617961626c65203d20747275653b0a486f74506f7461746f652e70726f746f747970652e7472616e736665722e696e70757473203d205b2775696e74323536272c202761646472657373275d3b0a0a486f74506f7461746f652e70726f746f747970652e70726963652e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e70726963652e696e70757473203d205b2775696e74323536275d3b0a486f74506f7461746f652e70726f746f747970652e70726963652e6f757470757473203d205b2775696e74323536275d3b0a0a486f74506f7461746f652e70726f746f747970652e706f7461746f6573436f756e742e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e706f7461746f6573436f756e742e6f757470757473203d205b2775696e74323536275d3b0a0a486f74506f7461746f652e70726f746f747970652e6e65787450726963652e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e6e65787450726963652e696e70757473203d205b2775696e74323536275d3b0a486f74506f7461746f652e70726f746f747970652e6e65787450726963652e6f757470757473203d205b2775696e74323536275d3b0a0a486f74506f7461746f652e70726f746f747970652e6f776e65722e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e6f776e65722e696e70757473203d205b2775696e74323536275d3b0a486f74506f7461746f652e70726f746f747970652e6f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a486f74506f7461746f652e70726f746f747970652e646562746f722e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e646562746f722e696e70757473203d205b2775696e74323536275d3b0a486f74506f7461746f652e70726f746f747970652e646562746f722e6f757470757473203d205b2761646472657373275d3b0a0a486f74506f7461746f652e70726f746f747970652e686f6c64696e67732e76696577203d20747275653b0a486f74506f7461746f652e70726f746f747970652e686f6c64696e67732e696e70757473203d205b2761646472657373275d3b0a486f74506f7461746f652e70726f746f747970652e686f6c64696e67732e6f757470757473203d205b27737472696e67275d3b0a0a486f74506f7461746f652e70726f746f747970652e5f736574486f6c64696e672e696e7465726e616c203d20747275653b0a0a486f74506f7461746f652e70726f746f747970652e737465616c2e70617961626c65203d20747275653b0a486f74506f7461746f652e70726f746f747970652e737465616c2e696e70757473203d205b2775696e74323536275d3b0a0a486f74506f7461746f652e70726f746f747970652e77697468647261772e696e70757473203d205b2775696e74323536275d3b0a0a72657475726e20486f74506f7461746f653b0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "5", to: "0x", value: "0", chainId: "3615", origin: "1651572536772", v: "1c62", r: "bd32c1efb50f46056411c4565ff213b9662d532f7f29409aa616ed9410647639", s: "221598b4931dfb612a38c235dcaddc2e1c8167d69db03932bcdbdb96b6bc1ae3" }, hash: "0x2d9987bffd02b12a95167d157f4d345b1954b62f043e308b892e916e0b217308", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572545092", status: 1, gasUsed: "21116" }, 0x8242a23917239a9fdc16f71f3272d08a0e6e6ebd4ec9696f13aa8f2a3bb42aeb: { data: { blockHash: "0x0002e11e37101c4d7386f6c36294514c2308fe8434e6c413dd9c843b2a0bd52e", blockNumber: "7", transactionIndex: 0, hash: "0x8242a23917239a9fdc16f71f3272d08a0e6e6ebd4ec9696f13aa8f2a3bb42aeb", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "6", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572550882", v: "", r: "", s: "" }, hash: "0x8242a23917239a9fdc16f71f3272d08a0e6e6ebd4ec9696f13aa8f2a3bb42aeb", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572551733", status: 1, gasUsed: "3108" }, 0xcef76138cfb3883fae36032061551ff0dcff8785795478380912e44318fe40bd: { data: { blockHash: "0x0002e11e37101c4d7386f6c36294514c2308fe8434e6c413dd9c843b2a0bd52e", blockNumber: "7", transactionIndex: 1, hash: "0xcef76138cfb3883fae36032061551ff0dcff8785795478380912e44318fe40bd", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1135950", gasPrice: "1000000000", data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000002da00000000000000000000000000000000000000000000000000000000000002d3d2f2a0a4e616d653a20446f75626c654f72517569747320546f6b656e0a56657273696f6e3a20302e302e330a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a0a0a636c61737320446f75626c654f725175697473546f6b656e20657874656e6473204552433230546f6b656e207b0a202020206e616d65203d2027446f75626c65204f72205175697473273b0a2020202073796d626f6c203d2027444f51273b0a0a20202020646f75626c654f7251756974732829207b0a2020202020202020636f6e7374207573657241646472657373203d206d73672e73656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a20202020202020206c65742077696e203d2066616c73653b0a0a202020202020202069662028757365724164647265737320696e20746f6b656e42616c616e63657320262620746f6b656e42616c616e6365735b75736572416464726573735d2e677428302929207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b75736572416464726573735d3b0a202020202020202020202020636f6e73742072616e64203d204d6174682e72616e646f6d2829202a203130303b0a20202020202020202020202077696e203d20284d6174682e726f756e642872616e642920252032203d3d2030293b0a0a2020202020202020202020206966202877696e29207b0a20202020202020202020202020202020746869732e6d696e742875736572416464726573732c2062616c616e6365293b0a20202020202020202020202020202020656d6974282757696e272c205b75736572416464726573732c2062616c616e63655d293b0a0a2020202020202020202020207d20656c7365207b0a20202020202020202020202020202020746869732e6275726e2875736572416464726573732c2062616c616e6365293b0a20202020202020202020202020202020656d697428274c6f6f7365272c205b75736572416464726573732c2062616c616e63655d293b0a2020202020202020202020207d0a0a20202020202020207d20656c7365207b0a202020202020202020202020636f6e7374206d696e74416d6f756e74203d206e756d62657228273130303030303030303030303030303030303027293b202f2f203120444f510a202020202020202020202020746869732e6d696e742875736572416464726573732c206d696e74416d6f756e74293b0a20202020202020202020202077696e203d20747275653b0a202020202020202020202020656d6974282746726565546f6b656e272c205b75736572416464726573732c206d696e74416d6f756e745d293b0a20202020202020207d0a0a202020202020202072657475726e205b77696e5d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020207468726f77206e6577204572726f722827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020207468726f77206e6577204572726f722827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a2020202020202020636f6e7374206275726e416d6f756e74203d20616d6f756e742e6469762832293b0a2020202020202020746869732e6275726e2873656e6465722c206275726e416d6f756e74293b0a0a2020202020202020636f6e737420726563697069656e74416d6f756e74203d20616d6f756e742e737562286275726e416d6f756e74293b0a2020202020202020746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20726563697069656e74416d6f756e74293b0a2020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20726563697069656e74416d6f756e74290a20202020202020200a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20726563697069656e74416d6f756e745d293b0a0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429202f2a2065787465726e616c2072657475726e732028626f6f6c29202a2f207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b202f2f2073656e646572206573742063656c756920646f6e74206c657320746f6b656e7320766f6e7420657472652064c3a970656e73c3a97320706172206d73672e73656e6465720a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020207468726f77206e6577204572726f722827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020207468726f77206e6577204572726f722827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020207468726f77206e6577204572726f722827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020636f6e7374206275726e416d6f756e74203d206e756d6265722830293b202f2f616d6f756e742e6469762832293b0a20202020202020202f2f746869732e6275726e2873656e6465722c206275726e416d6f756e74293b0a0a2020202020202020636f6e737420726563697069656e74416d6f756e74203d20616d6f756e742e737562286275726e416d6f756e74293b0a2020202020202020746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20726563697069656e74416d6f756e74293b0a2020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20726563697069656e74416d6f756e74290a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20726563697069656e74416d6f756e745d293b0a0a202020202020202072657475726e205b747275655d3b0a202020207d0a7d0a0a2f2f204d6574686f64730a446f75626c654f725175697473546f6b656e2e70726f746f747970652e646f75626c654f7251756974732e6f757470757473203d205b27626f6f6c275d3b0a0a446f75626c654f725175697473546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a446f75626c654f725175697473546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a446f75626c654f725175697473546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a446f75626c654f725175697473546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a0a72657475726e20446f75626c654f725175697473546f6b656e3b00000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "6", to: "0x", value: "0", chainId: "3615", origin: "1651572550141", v: "1c61", r: "a980b72389ae3d2144af00f47acbe772ae53fb51f1acbcd068760d57b663cf41", s: "775a54a26bf9f47097959d46d772a5bddbbc9310ef6c8cd4ea160e026de5ec9e" }, hash: "0xcef76138cfb3883fae36032061551ff0dcff8785795478380912e44318fe40bd", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572551733", status: 1, gasUsed: "30925" }, 0xfc4ea62204e6014c299da06a9f495eb1188f09b5c0e0832ee9bcb2b279573445: { data: { blockHash: "0x00abd0559e99a60cc3895a255124003fd783f2a7f5859b15aad137437ea7c79a", blockNumber: "8", transactionIndex: 0, hash: "0xfc4ea62204e6014c299da06a9f495eb1188f09b5c0e0832ee9bcb2b279573445", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "7", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572556926", v: "", r: "", s: "" }, hash: "0xfc4ea62204e6014c299da06a9f495eb1188f09b5c0e0832ee9bcb2b279573445", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572557849", status: 1, gasUsed: "5720" }, 0xfe576f241ed53b32a1748eb53e93838c2e60c2ffc866a993482590a854d9152e: { data: { blockHash: "0x00abd0559e99a60cc3895a255124003fd783f2a7f5859b15aad137437ea7c79a", blockNumber: "8", transactionIndex: 1, hash: "0xfe576f241ed53b32a1748eb53e93838c2e60c2ffc866a993482590a854d9152e", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1750900", gasPrice: "1000000000", data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000005d462f2a0a4e616d653a204c69717569646974795061697220286578616d706c653a20574b41524d412d594f290a56657273696f6e3a20302e302e310a536f757263653a200a202d2050616e63616b65506169723a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d737761702d636f72652f626c6f622f6d61737465722f636f6e7472616374732f50616e63616b65506169722e736f6c0a202d2050616e63616b6545524332303a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d737761702d636f72652f626c6f622f6d61737465722f636f6e7472616374732f50616e63616b6545524332302e736f6c0a202d2050616e63616b65526f757465723a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d737761702d7065726970686572792f626c6f622f6d61737465722f636f6e7472616374732f50616e63616b65526f757465722e736f6c0a202d2050616e63616b65466163746f72793a2068747470733a2f2f6769746875622e636f6d2f70616e63616b65737761702f70616e63616b652d737761702d636f72652f626c6f622f6d61737465722f636f6e7472616374732f50616e63616b65466163746f72792e736f6c0a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a0a0a0a66756e6374696f6e2049455243323028746f6b656e4164647265737329207b0a2020202072657475726e206164647265737328746f6b656e41646472657373292e676574436f6e747261637428293b0a7d0a0a0a636c617373204c69717569646974795061697220657874656e6473204552433230546f6b656e207b0a202020206e616d65203d20274c697175696469747950616972204c5073273b0a2020202073796d626f6c203d20274c6971756964697479506169722d4c50273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d6265722830293b0a202020207265736572766530203d206e756d6265722830293b0a202020207265736572766531203d206e756d6265722830293b0a20202020746f6b656e30203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a20202020746f6b656e31203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020200a20202020496e697469616c697a6528746f6b656e302c20746f6b656e3129207b7d202f2f206576656e740a202020204164644c697175696469747928616d6f756e74302c20616d6f756e743129207b7d202f2f206576656e740a2020202052656d6f76654c697175696469747928616d6f756e74302c20616d6f756e743129207b7d202f2f206576656e740a202020205472616e736665722866726f6d2c20746f2c2076616c756529207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e74302c20616d6f756e743129207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e74302c20616d6f756e74312c20746f29207b7d202f2f206576656e740a2020202053796e632872657365727665302c20726573657276653129207b7d202f2f206576656e740a20202020537761702873656e6465722c20616d6f756e7430496e2c20616d6f756e7431496e2c20616d6f756e74304f75742c20616d6f756e74314f75742c20746f29207b7d202f2f206576656e740a0a202020200a20202020636f6e7374727563746f722829207b0a2020202020202020737570657228293b0a2020202020202020746869732e666163746f7279203d206d73672e73656e6465723b0a202020207d0a0a0a20202020696e697469616c697a6528746f6b656e302c20746f6b656e3129207b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e666163746f72792c202750616e63616b653a20464f5242494444454e27293b0a2020202020202020746869732e746f6b656e30203d20746f6b656e302e746f4c6f7765724361736528293b0a2020202020202020746869732e746f6b656e31203d20746f6b656e312e746f4c6f7765724361736528293b0a0a2020202020202020656d69742827496e697469616c697a65272c205b746f6b656e302c20746f6b656e315d293b0a202020207d0a0a2020202067657452657365727665732829207b0a202020202020202072657475726e205b0a202020202020202020202020746869732e72657365727665302c0a202020202020202020202020746869732e72657365727665312c0a20202020202020205d3b0a202020207d0a0a202020200a202020205f6164644c697175696469747928616d6f756e7441446573697265642c20616d6f756e74424465736972656429207b0a2020202020202020636f6e7374205b72657365727665412c2072657365727665425d203d20746869732e676574526573657276657328293b0a20202020202020206c657420616d6f756e74412c20616d6f756e74423b0a202020200a20202020202020206966202872657365727665412e65712830292026262072657365727665422e657128302929207b0a2020202020202020202020205b616d6f756e74412c20616d6f756e74425d203d205b616d6f756e7441446573697265642c20616d6f756e7442446573697265645d3b0a0a20202020202020207d20656c7365207b0a202020202020202020202020636f6e737420616d6f756e74424f7074696d616c203d20746869732e5f71756f746528616d6f756e7441446573697265642c2072657365727665412c207265736572766542295b305d3b0a2020202020202020202020202f2f20544f444f3a20612072657465737465722f7265766f69720a2020202020202020202020206966202874727565207c7c202120616d6f756e74424f7074696d616c2e677428616d6f756e7442446573697265642929207b0a202020202020202020202020202020202f2f726571756972652821616d6f756e74424f7074696d616c2e6c7428616d6f756e74424d696e292c202750616e63616b65526f757465723a20494e53554646494349454e545f425f414d4f554e5427293b0a202020202020202020202020202020205b616d6f756e74412c20616d6f756e74425d203d205b616d6f756e7441446573697265642c20616d6f756e74424f7074696d616c5d3b0a0a2020202020202020202020207d20656c7365207b0a20202020202020202020202020202020636f6e737420616d6f756e74414f7074696d616c203d20746869732e5f71756f746528616d6f756e7442446573697265642c2072657365727665422c207265736572766541295b305d3b0a202020202020202020202020202020206173736572742821616d6f756e74414f7074696d616c2e677428616d6f756e74414465736972656429293b0a202020202020202020202020202020202f2f726571756972652821616d6f756e74414f7074696d616c2e6c7428616d6f756e74414d696e292c202750616e63616b65526f757465723a20494e53554646494349454e545f415f414d4f554e5427293b0a202020202020202020202020202020205b616d6f756e74412c20616d6f756e74425d203d205b616d6f756e74414f7074696d616c2c20616d6f756e7442446573697265645d3b0a2020202020202020202020207d0a20202020202020207d0a202020202020202072657475726e205b616d6f756e74412c20616d6f756e74425d3b0a202020207d0a0a0a202020205f71756f746528616d6f756e74412c2072657365727665422c20726573657276654129207b0a20202020202020207265717569726528616d6f756e74412e67742830292c202750616e63616b654c6962726172793a20494e53554646494349454e545f414d4f554e5427293b0a2020202020202020726571756972652872657365727665412e67742830292026262072657365727665422e67742830292c202750616e63616b654c6962726172793a20494e53554646494349454e545f4c495155494449545927293b0a2020202020202020636f6e737420616d6f756e7442203d20616d6f756e74412e6d756c287265736572766542292e646976287265736572766541293b0a202020202020202072657475726e205b616d6f756e74425d3b0a202020207d0a0a0a202020206164644c697175696469747928616d6f756e7441446573697265642c20616d6f756e74424465736972656429207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a2020202020202020636f6e7374205b616d6f756e74412c20616d6f756e74425d203d20746869732e5f6164644c697175696469747928616d6f756e7441446573697265642c20616d6f756e744244657369726564293b0a0a2020202020202020636f6e737420746f6b656e41203d20746869732e746f6b656e303b0a2020202020202020636f6e737420746f6b656e42203d20746869732e746f6b656e313b0a202020202020202049455243323028746f6b656e41292e7472616e7366657246726f6d286d73672e73656e6465722c20616464726573732874686973292e616464726573732c20616d6f756e7441293b0a202020202020202049455243323028746f6b656e42292e7472616e7366657246726f6d286d73672e73656e6465722c20616464726573732874686973292e616464726573732c20616d6f756e7442293b0a20202020202020200a2020202020202020636f6e7374206c6971756964697479203d20746869732e6d696e74286d73672e73656e646572295b305d3b0a20202020202020200a2020202020202020656d697428274164644c6971756964697479272c205b616d6f756e74412c20616d6f756e74425d293b0a0a202020202020202072657475726e205b616d6f756e74412c20616d6f756e74422c206c69717569646974795d3b0a202020207d0a0a0a202020206d696e7428746f29207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a2020202020202020636f6e7374205b5f72657365727665302c205f72657365727665315d203d20746869732e676574526573657276657328293b0a2020202020202020636f6e73742062616c616e636530203d2049455243323028746869732e746f6b656e30292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a2020202020202020636f6e73742062616c616e636531203d2049455243323028746869732e746f6b656e31292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a2020202020202020636f6e737420616d6f756e7430203d2062616c616e6365302e737562285f7265736572766530293b0a2020202020202020636f6e737420616d6f756e7431203d2062616c616e6365312e737562285f7265736572766531293b0a2020202020202020636f6e7374204d494e494d554d5f4c4951554944495459203d2031302a2a333b0a0a202020202020202066756e6374696f6e205f6d696e28612c206229207b0a20202020202020202020202072657475726e20612e6774286229203f2062203a20613b0a20202020202020207d0a0a202020202020202066756e6374696f6e2073717274426967496e742876616c756529207b0a2020202020202020202020206966202876616c7565203c20306e29207b0a202020202020202020202020202020207468726f77202773717561726520726f6f74206f66206e65676174697665206e756d62657273206973206e6f7420737570706f72746564270a2020202020202020202020207d0a20202020202020200a2020202020202020202020206966202876616c7565203c20326e29207b0a2020202020202020202020202020202072657475726e2076616c75653b0a2020202020202020202020207d0a20202020202020200a20202020202020202020202066756e6374696f6e205f69746572286e2c20783029207b0a20202020202020202020202020202020636f6e7374207831203d2028286e202f20783029202b20783029203e3e20316e3b0a20202020202020202020202020202020696620287830203d3d3d207831207c7c207830203d3d3d20287831202d20316e2929207b0a202020202020202020202020202020202020202072657475726e2078303b0a202020202020202020202020202020207d0a2020202020202020202020202020202072657475726e205f69746572286e2c207831293b0a2020202020202020202020207d0a20202020202020200a20202020202020202020202072657475726e205f697465722876616c75652c20316e293b0a20202020202020207d0a20202020202020200a202020202020202066756e6374696f6e205f73717274286129207b0a20202020202020202020202072657475726e206e756d6265722873717274426967496e7428612e746f426967496e742829292e746f537472696e672829293b0a20202020202020207d0a0a20202020202020206c6574206c69717569646974793b0a20202020202020202f2f636f6e7374206665654f6e203d205f6d696e74466565285f72657365727665302c205f7265736572766531293b0a0a2020202020202020636f6e7374205f746f74616c537570706c79203d20746869732e746f74616c537570706c793b0a2020202020202020696620285f746f74616c537570706c79203d3d203029207b0a2020202020202020202020206c6971756964697479203d205f7371727428616d6f756e74302e6d756c28616d6f756e743129292e737562284d494e494d554d5f4c4951554944495459293b0a202020202020202020202020746869732e5f6d696e7428616464726573732830292e616464726573732c204d494e494d554d5f4c4951554944495459293b202f2f207065726d616e656e746c79206c6f636b20746865206669727374204d494e494d554d5f4c495155494449545920746f6b656e730a0a20202020202020207d20656c7365207b0a2020202020202020202020206c6971756964697479203d205f6d696e28616d6f756e74302e6d756c285f746f74616c537570706c79292e646976285f7265736572766530292c20616d6f756e74312e6d756c285f746f74616c537570706c79292e646976285f726573657276653129293b0a20202020202020207d0a202020202020202072657175697265286c69717569646974792e67742830292c202750616e63616b653a20494e53554646494349454e545f4c49515549444954595f4d494e54454427293b0a2020202020202020746869732e5f6d696e7428746f2c206c6971756964697479293b0a0a2020202020202020746869732e5f7570646174652862616c616e6365302c2062616c616e6365312c205f72657365727665302c205f7265736572766531293b0a20202020202020200a20202020202020202f2f696620286665654f6e29206b4c617374203d2075696e74287265736572766530292e6d756c287265736572766531293b202f2f20726573657276653020616e64207265736572766531206172652075702d746f2d646174650a20202020202020202f2f656d697428274d696e74272c205b6d73672e73656e6465722c20616d6f756e74302c20616d6f756e74315d293b0a0a202020202020202072657475726e205b0a202020202020202020202020616d6f756e74302c0a20202020202020205d3b0a202020207d0a0a0a2020202072656d6f76654c6971756964697479286c697175696469747929207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a2020202020202020746869732e7472616e7366657246726f6d286d73672e73656e6465722c20616464726573732874686973292e616464726573732c206c6971756964697479293b202f2f2073656e64206c697175696469747920746f20706169720a0a0a2020202020202020636f6e737420746f6b656e30203d20746869732e746f6b656e303b0a0a2020202020202020636f6e7374205b616d6f756e74412c20616d6f756e74425d203d20746869732e6275726e28746f293b0a0a20202020202020202f2f72657175697265282120616d6f756e74412e6c7428616d6f756e74414d696e292c202750616e63616b65526f757465723a20494e53554646494349454e545f415f414d4f554e5427293b0a20202020202020202f2f72657175697265282120616d6f756e74422e6c7428616d6f756e74424d696e292c202750616e63616b65526f757465723a20494e53554646494349454e545f425f414d4f554e5427293b0a20202020202020207265717569726528616d6f756e74412e67742830292c202750616e63616b65526f757465723a20494e53554646494349454e545f415f414d4f554e5427293b0a20202020202020207265717569726528616d6f756e74422e67742830292c202750616e63616b65526f757465723a20494e53554646494349454e545f425f414d4f554e5427293b0a20202020202020200a2020202020202020656d6974282752656d6f76654c6971756964697479272c205b616d6f756e74412c20616d6f756e74425d293b0a0a202020202020202072657475726e205b616d6f756e74412c20616d6f756e74425d3b0a202020207d0a0a0a202020206275726e28746f29207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a2020202020202020636f6e7374205b5f72657365727665302c205f72657365727665315d203d20746869732e676574526573657276657328293b0a2020202020202020636f6e7374205f746f6b656e30203d20746869732e746f6b656e303b0a2020202020202020636f6e7374205f746f6b656e31203d20746869732e746f6b656e313b0a2020202020202020636f6e73742062616c616e636530203d20494552433230285f746f6b656e30292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a2020202020202020636f6e73742062616c616e636531203d20494552433230285f746f6b656e31292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a2020202020202020636f6e7374206c6971756964697479203d20746869732e62616c616e63654f665b616464726573732874686973292e616464726573735d3b0a0a20202020202020202f2f636f6e7374206665654f6e203d20746869732e5f6d696e74466565285f72657365727665302c205f7265736572766531293b0a2020202020202020636f6e7374205f746f74616c537570706c79203d20746869732e746f74616c537570706c793b0a20202020202020200a2020202020202020636f6e737420616d6f756e7430203d206c69717569646974792e6d756c2862616c616e636530292e646976285f746f74616c537570706c79293b202f2f207573696e672062616c616e63657320656e73757265732070726f2d7261746120646973747269627574696f6e0a2020202020202020636f6e737420616d6f756e7431203d206c69717569646974792e6d756c2862616c616e636531292e646976285f746f74616c537570706c79293b202f2f207573696e672062616c616e63657320656e73757265732070726f2d7261746120646973747269627574696f6e0a20202020202020207265717569726528616d6f756e74302e677428302920262620616d6f756e74312e67742830292c202750616e63616b653a20494e53554646494349454e545f4c49515549444954595f4255524e454427293b0a0a2020202020202020746869732e5f6275726e28616464726573732874686973292e616464726573732c206c6971756964697479293b0a2020202020202020494552433230285f746f6b656e30292e7472616e7366657228746f2c20616d6f756e7430293b0a2020202020202020494552433230285f746f6b656e31292e7472616e7366657228746f2c20616d6f756e7431293b0a202020202020202062616c616e636530203d20494552433230285f746f6b656e30292e62616c616e63654f6628616464726573732874686973292e61646472657373293b0a202020202020202062616c616e636531203d20494552433230285f746f6b656e31292e62616c616e63654f6628616464726573732874686973292e61646472657373293b0a0a2020202020202020746869732e5f7570646174652862616c616e6365302c2062616c616e6365312c205f72657365727665302c205f7265736572766531293b0a0a20202020202020202f2f696620286665654f6e29206b4c617374203d2075696e74287265736572766530292e6d756c287265736572766531293b202f2f20726573657276653020616e64207265736572766531206172652075702d746f2d646174650a20202020202020202f2f656d697428274275726e272c205b6d73672e73656e6465722c20616d6f756e74302c20616d6f756e74312c20746f5d293b0a0a202020202020202072657475726e205b0a202020202020202020202020616d6f756e74302c0a202020202020202020202020616d6f756e74312c0a20202020202020205d0a202020207d0a0a0a202020205f6d696e7428746f2c2076616c756529207b0a2020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e6164642876616c7565293b0a202020202020202069662028212028746f20696e20746869732e62616c616e6365732929207b0a202020202020202020202020746869732e62616c616e6365735b746f5d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746869732e62616c616e6365735b746f5d203d20746869732e62616c616e6365735b746f5d2e6164642876616c7565293b0a2020202020202020656d697428275472616e73666572272c205b616464726573732830292e616464726573732c20746f2c2076616c75655d293b0a202020207d0a0a202020205f6275726e2866726f6d2c2076616c756529207b0a2020202020202020726571756972652866726f6d20696e20746869732e62616c616e636573202626202120746869732e62616c616e6365735b66726f6d5d2e6c7428746869732e62616c616e6365735b66726f6d5d29293b0a2020202020202020746869732e62616c616e6365735b66726f6d5d203d20746869732e62616c616e6365735b66726f6d5d2e7375622876616c7565293b0a2020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e7375622876616c7565293b0a2020202020202020656d697428275472616e73666572272c205b66726f6d2c20616464726573732830292e616464726573732c2076616c75655d293b0a202020207d0a202020200a202020205f7570646174652862616c616e6365302c2062616c616e63653129207b0a2020202020202020746869732e7265736572766530203d2062616c616e6365303b0a2020202020202020746869732e7265736572766531203d2062616c616e6365313b0a2020202020202020656d6974282753796e63272c205b746869732e72657365727665302c20746869732e72657365727665315d293b0a202020207d0a0a0a20202020737761704578616374546f6b656e73466f72546f6b656e7328736f75726365546f6b656e2c20616d6f756e74496e2c20616d6f756e744f75744d696e29207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a2020202020202020736f75726365546f6b656e203d20736f75726365546f6b656e2e746f4c6f7765724361736528293b0a2020202020202020636f6e737420746f203d206d73672e73656e6465723b0a2020202020202020636f6e737420746172676574546f6b656e203d2028736f75726365546f6b656e203d3d20746869732e746f6b656e3029203f20746869732e746f6b656e31203a20746869732e746f6b656e303b0a20202020202020200a2020202020202020636f6e737420616d6f756e7473203d20746869732e5f676574416d6f756e74734f757428616d6f756e74496e2c20736f75726365546f6b656e295b305d3b0a202020202020202072657175697265282120616d6f756e74735b616d6f756e74732e6c656e677468202d20315d2e6c7428616d6f756e744f75744d696e292c202750616e63616b65526f757465723a20494e53554646494349454e545f4f55545055545f414d4f554e5427293b0a0a202020202020202049455243323028736f75726365546f6b656e292e7472616e7366657246726f6d286d73672e73656e6465722c20616464726573732874686973292e616464726573732c20616d6f756e74735b305d293b0a0a2020202020202020746869732e5f7377617028616d6f756e74732c20736f75726365546f6b656e2c20746f293b0a0a202020202020202072657475726e205b616d6f756e74735d3b0a202020207d0a0a202020205f676574416d6f756e74734f757428616d6f756e74496e2c20736f75726365546f6b656e29207b0a2020202020202020636f6e7374205b5f72657365727665302c205f72657365727665315d203d20746869732e676574526573657276657328293b0a2020202020202020636f6e7374205b72657365727665496e2c20726573657276654f75745d203d2028736f75726365546f6b656e203d3d20746869732e746f6b656e3029203f205b5f72657365727665302c205f72657365727665315d203a205b5f72657365727665312c205f72657365727665305d3b0a0a2020202020202020636f6e737420616d6f756e7473203d205b5d3b0a2020202020202020616d6f756e74732e7075736828616d6f756e74496e293b0a20202020202020200a2020202020202020636f6e7374205f616d6f756e74203d20746869732e5f676574416d6f756e744f757428616d6f756e74735b305d2c2072657365727665496e2c20726573657276654f7574295b305d3b0a2020202020202020616d6f756e74732e70757368285f616d6f756e74293b0a202020202020202072657475726e205b616d6f756e74735d3b0a202020207d0a0a202020205f676574416d6f756e744f757428616d6f756e74496e2c2072657365727665496e2c20726573657276654f757429207b0a20202020202020207265717569726528616d6f756e74496e2e67742830292c202750616e63616b654c6962726172793a20494e53554646494349454e545f494e5055545f414d4f554e5427293b0a2020202020202020726571756972652872657365727665496e2e677428302920262620726573657276654f75742e67742830292c202750616e63616b654c6962726172793a20494e53554646494349454e545f4c495155494449545927293b0a0a2020202020202020636f6e737420616d6f756e74496e57697468466565203d20616d6f756e74496e2e6d756c28393938293b0a2020202020202020636f6e7374206e756d657261746f72203d20616d6f756e74496e576974684665652e6d756c28726573657276654f7574293b0a2020202020202020636f6e73742064656e6f6d696e61746f72203d2072657365727665496e2e6d756c2831303030292e61646428616d6f756e74496e57697468466565293b0a2020202020202020636f6e737420616d6f756e744f7574203d206e756d657261746f722e6469762864656e6f6d696e61746f72293b0a202020202020202072657475726e205b616d6f756e744f75745d3b0a202020207d0a0a0a202020207377617028616d6f756e74304f75742c20616d6f756e74314f75742c20746f2c20646174612920207b0a20202020202020207265717569726528746869732e746f6b656e3120213d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030272c20224e4f545f494e495449414c495a454422293b0a20202020202020207265717569726528616d6f756e74304f75742e6774283029207c7c20616d6f756e74314f75742e67742830292c202750616e63616b653a20494e53554646494349454e545f4f55545055545f414d4f554e5427293b0a2020202020202020636f6e7374205b5f72657365727665302c205f72657365727665312c5d203d20746869732e676574526573657276657328293b0a20202020202020207265717569726528616d6f756e74304f75742e6c74285f72657365727665302920262620616d6f756e74314f75742e6c74285f7265736572766531292c202750616e63616b653a20494e53554646494349454e545f4c495155494449545927293b0a0a20202020202020206c65742062616c616e6365303b0a20202020202020206c65742062616c616e6365313b0a20202020202020207b202f2f2073636f706520666f72205f746f6b656e7b302c317d2c2061766f69647320737461636b20746f6f2064656570206572726f72730a202020202020202020202020636f6e7374205f746f6b656e30203d20746869732e746f6b656e303b0a202020202020202020202020636f6e7374205f746f6b656e31203d20746869732e746f6b656e313b0a2020202020202020202020207265717569726528746f20213d205f746f6b656e3020262620746f20213d205f746f6b656e312c202750616e63616b653a20494e56414c49445f544f27293b0a0a20202020202020202020202069662028616d6f756e74304f75742e67742830292920494552433230285f746f6b656e30292e7472616e7366657228746f2c20616d6f756e74304f7574293b0a20202020202020202020202069662028616d6f756e74314f75742e67742830292920494552433230285f746f6b656e31292e7472616e7366657228746f2c20616d6f756e74314f7574293b0a0a202020202020202020202020696620286461746120262620646174612e6c656e677468203e2030292049455243323028746f292e70616e63616b6543616c6c286d73672e73656e6465722c20616d6f756e74304f75742c20616d6f756e74314f75742c2064617461293b0a0a20202020202020202020202062616c616e636530203d20494552433230285f746f6b656e30292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a20202020202020202020202062616c616e636531203d20494552433230285f746f6b656e31292e62616c616e63654f6628616464726573732874686973292e61646472657373295b305d3b0a20202020202020207d0a0a2020202020202020636f6e737420616d6f756e7430496e203d202862616c616e6365302e6774285f72657365727665302e73756228616d6f756e74304f7574292929203f2062616c616e6365302e73756228285f72657365727665302e73756228616d6f756e74304f7574292929203a206e756d6265722830293b0a2020202020202020636f6e737420616d6f756e7431496e203d202862616c616e6365312e6774285f72657365727665312e73756228616d6f756e74314f7574292929203f2062616c616e6365312e73756228285f72657365727665312e73756228616d6f756e74314f7574292929203a206e756d6265722830293b0a20202020202020207265717569726528616d6f756e7430496e2e6774283029207c7c20616d6f756e7431496e2e67742830292c202750616e63616b653a20494e53554646494349454e545f494e5055545f414d4f554e5427293b0a0a20202020202020207b202f2f2073636f706520666f7220726573657276657b302c317d41646a75737465642c2061766f69647320737461636b20746f6f2064656570206572726f72730a202020202020202020202020636f6e73742062616c616e63653041646a7573746564203d2062616c616e6365302e6d756c2831303030292e73756228616d6f756e7430496e2e6d756c283229293b0a202020202020202020202020636f6e73742062616c616e63653141646a7573746564203d2062616c616e6365312e6d756c2831303030292e73756228616d6f756e7431496e2e6d756c283229293b0a20202020202020202020202072657175697265282162616c616e63653041646a75737465642e6d756c2862616c616e63653141646a7573746564292e6c74285f72657365727665302e6d756c285f7265736572766531292e6d756c2828313030302a2a32292e746f537472696e67282929292c202750616e63616b653a204b27293b0a20202020202020207d0a0a2020202020202020746869732e5f7570646174652862616c616e6365302c2062616c616e6365312c205f72657365727665302c205f7265736572766531293b0a20202020202020200a2020202020202020656d6974282753776170272c205b6d73672e73656e6465722c20616d6f756e7430496e2c20616d6f756e7431496e2c20616d6f756e74304f75742c20616d6f756e74314f75742c20746f5d293b0a202020207d0a0a0a202020205f7377617028616d6f756e74732c20736f75726365546f6b656e2c205f746f29207b0a2020202020202020636f6e737420746172676574546f6b656e203d2028736f75726365546f6b656e203d3d20746869732e746f6b656e3029203f20746869732e746f6b656e31203a20746869732e746f6b656e303b0a2020202020202020636f6e73742070617468203d205b736f75726365546f6b656e2c20746172676574546f6b656e5d3b0a2020202020202020666f7220286c657420693d303b2069203c20706174682e6c656e677468202d20313b20692b2b29207b0a202020202020202020202020636f6e7374205b696e7075742c206f75747075745d203d205b20706174685b695d2c20706174685b69202b20315d205d3b0a2020202020202020202020202f2f636f6e7374205b746f6b656e302c5d203d2050616e63616b654c6962726172792e736f7274546f6b656e7328696e7075742c206f7574707574293b0a202020202020202020202020636f6e737420746f6b656e30203d2028696e707574203e206f757470757429203f206f7574707574203a20696e7075743b0a2020202020202020202020200a202020202020202020202020636f6e737420616d6f756e744f7574203d20616d6f756e74735b69202b20315d3b0a0a202020202020202020202020636f6e7374205b616d6f756e74304f75742c20616d6f756e74314f75745d203d2028696e707574203d3d20746f6b656e3029203f205b6e756d6265722830292c20616d6f756e744f75745d203a205b616d6f756e744f75742c206e756d6265722830295d3b0a0a202020202020202020202020636f6e737420746f203d202869203c20706174682e6c656e677468202d203229203f20616464726573732874686973292e61646472657373203a205f746f3b0a0a202020202020202020202020746869732e7377617028616d6f756e74304f75742c20616d6f756e74314f75742c20746f2c202727293b0a20202020202020207d0a202020207d0a0a7d0a0a2f2f204576656e74730a4c6971756964697479506169722e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a4c6971756964697479506169722e70726f746f747970652e4164644c69717569646974792e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e4164644c69717569646974792e696e70757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e52656d6f76654c69717569646974792e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e52656d6f76654c69717569646974792e696e70757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536272c20276164647265737320696e6465786564275d3b0a0a4c6971756964697479506169722e70726f746f747970652e53796e632e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e53796e632e696e70757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e537761702e6576656e74203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e537761702e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202775696e74323536272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4c6971756964697479506169722e70726f746f747970652e696e697469616c697a652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a0a4c6971756964697479506169722e70726f746f747970652e67657452657365727665732e76696577203d20747275653b0a4c6971756964697479506169722e70726f746f747970652e67657452657365727665732e6f757470757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e5f6164644c69717569646974792e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e5f71756f74652e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e6164644c69717569646974792e696e70757473203d205b2775696e74323536272c202775696e74323536275d3b0a4c6971756964697479506169722e70726f746f747970652e6164644c69717569646974792e6f757470757473203d205b2775696e74323536272c202775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373275d3b0a4c6971756964697479506169722e70726f746f747970652e6d696e742e6f757470757473203d205b2775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e72656d6f76654c69717569646974792e696e70757473203d205b2775696e74323536275d3b0a4c6971756964697479506169722e70726f746f747970652e72656d6f76654c69717569646974792e6f757470757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373275d3b0a4c6971756964697479506169722e70726f746f747970652e6275726e2e6f757470757473203d205b2775696e74323536272c202775696e74323536275d3b0a0a4c6971756964697479506169722e70726f746f747970652e737761704578616374546f6b656e73466f72546f6b656e732e696e70757473203d205b2761646472657373272c202775696e74323536272c202775696e74323536275d3b0a4c6971756964697479506169722e70726f746f747970652e737761704578616374546f6b656e73466f72546f6b656e732e6f757470757473203d205b2775696e745b5d275d3b0a0a4c6971756964697479506169722e70726f746f747970652e5f676574416d6f756e74734f75742e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e5f676574416d6f756e744f75742e696e7465726e616c203d20747275653b0a0a4c6971756964697479506169722e70726f746f747970652e737761702e696e70757473203d205b2775696e74323536272c202775696e74323536272c202761646472657373272c20276279746573275d3b0a0a4c6971756964697479506169722e70726f746f747970652e5f737761702e696e7465726e616c203d20747275653b0a0a0a0a72657475726e204c6971756964697479506169723b0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "7", to: "0x", value: "0", chainId: "3615", origin: "1651572555562", v: "1c62", r: "4799de081734b7104ce37c499c427ead6a2e9133cc370ed5e2600e31f665acd1", s: "188d92578b709d60685699c65685d49a2758c0ca042bdb5b8ab5bdcc05cc594e" }, hash: "0xfe576f241ed53b32a1748eb53e93838c2e60c2ffc866a993482590a854d9152e", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572557849", status: 1, gasUsed: "130808" }, 0xd27067832f83e7888f4fa5193f443f880650af6aa95d1f67ba0f48d22619d302: { data: { blockHash: "0x007d12ef737a792e65de5c88ad295881851a53ffb193f3157f157bb13747118d", blockNumber: "9", transactionIndex: 0, hash: "0xd27067832f83e7888f4fa5193f443f880650af6aa95d1f67ba0f48d22619d302", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "8", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572563042", v: "", r: "", s: "" }, hash: "0xd27067832f83e7888f4fa5193f443f880650af6aa95d1f67ba0f48d22619d302", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572564252", status: 1, gasUsed: "2419" }, 0xd242c8ffbe99682086ac93dfbdb2f83ce794ef8af244cc33bf59368aaf85b603: { data: { blockHash: "0x007d12ef737a792e65de5c88ad295881851a53ffb193f3157f157bb13747118d", blockNumber: "9", transactionIndex: 1, hash: "0xd242c8ffbe99682086ac93dfbdb2f83ce794ef8af244cc33bf59368aaf85b603", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "1015000", gasPrice: "1000000000", data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000244000000000000000000000000000000000000000000000000000000000000023c82f2a0a4e616d653a2050726573616c6520546f6b656e0a56657273696f6e3a20302e302e330a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a0a0a636c6173732050726573616c65546f6b656e20657874656e6473204552433230546f6b656e207b0a202020206e616d65203d202750726573616c6520546f6b656e273b0a2020202073796d626f6c203d2027505245273b0a0a2020202073616c655365727669636541646472657373203d2027273b0a202020200a0a20202020496e697469616c697a652873616c65536572766963654164647265737329207b7d202f2f206576656e740a0a0a20202020696e697469616c697a652873616c65536572766963654164647265737329207b0a2020202020202020746869732e73616c655365727669636541646472657373203d2073616c6553657276696365416464726573732e746f4c6f7765724361736528293b0a0a2020202020202020656d69742827496e697469616c697a65272c205b73616c6553657276696365416464726573735d293b0a202020207d0a0a0a202020206d696e7428726563697069656e742c20616d6f756e74546f4d696e7429207b0a20202020202020207265717569726528746869732e73616c65536572766963654164647265737320213d2027272c20224e4f545f494e495449414c495a454422293b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e73616c6553657276696365416464726573732c20224f4e4c595f535441434b494e475f5345525649434522293b0a2020202020202020746869732e5f6d696e7428726563697069656e742c20616d6f756e74546f4d696e74293b0a202020207d0a0a7d0a0a2f2f204576656e74730a50726573616c65546f6b656e2e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a50726573616c65546f6b656e2e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a50726573616c65546f6b656e2e70726f746f747970652e696e697469616c697a652e696e70757473203d205b2761646472657373275d3b0a0a50726573616c65546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a0a0a0a72657475726e2050726573616c65546f6b656e3b0a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "8", to: "0x", value: "0", chainId: "3615", origin: "1651572560908", v: "1c62", r: "d14cfec2be03194c3f5a22a8c52034fca6a038ec7274d758214020b2d3e079d0", s: "4eeed5c1be8fe784e7601029f7300369eb5458317718434504e1d115c704d533" }, hash: "0xd242c8ffbe99682086ac93dfbdb2f83ce794ef8af244cc33bf59368aaf85b603", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572564252", status: 1, gasUsed: "22850" }, 0xdb8d72297b2c4ec5299b905c528f01396a1b01a40e07a1096ed6490d4f09f341: { data: { blockHash: "0x00995e427a45c5fef2068e47722cab6cfedf79fa867f67a4183b56a24d67a629", blockNumber: "10", transactionIndex: 0, hash: "0xdb8d72297b2c4ec5299b905c528f01396a1b01a40e07a1096ed6490d4f09f341", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "9", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572569412", v: "", r: "", s: "" }, hash: "0xdb8d72297b2c4ec5299b905c528f01396a1b01a40e07a1096ed6490d4f09f341", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572572819", status: 1, gasUsed: "2371" }, 0x15a7d8bf0c3ef7b0d877bd57e0410f1d73fe4bc4e3425a606f3dab8b44370584: { data: { blockHash: "0x00995e427a45c5fef2068e47722cab6cfedf79fa867f67a4183b56a24d67a629", blockNumber: "10", transactionIndex: 1, hash: "0x15a7d8bf0c3ef7b0d877bd57e0410f1d73fe4bc4e3425a606f3dab8b44370584", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "773200", gasPrice: "1000000000", data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000116000000000000000000000000000000000000000000000000000000000000010e42f2a0a4e616d653a2053616c65536572766963650a56657273696f6e3a20302e302e330a2a2f0a0a0a66756e6374696f6e2049455243323028746f6b656e4164647265737329207b0a2020202072657475726e206164647265737328746f6b656e41646472657373292e676574436f6e747261637428293b0a7d0a0a0a636c6173732053616c6553657276696365207b2020200a202020206e616d65203d202750726573616c652053657276696365273b0a202020200a20202020746f6b656e41646472657373203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a202020206d6178537570706c79203d206e756d62657228273027293b0a2020202070726573616c65446174655374617274203d206e756d6265722830293b0a2020202070726573616c654461746553746f70203d206e756d6265722830293b0a2020202070726573616c655072696365203d206e756d6265722830293b0a2020202070726573616c654d6178537570706c79203d206e756d6265722830293b0a2020202070726573616c655768696c74654c6973744d65726b6c65526f6f74203d206e756d62657228273027293b0a202020207075626c69635072696365203d206e756d62657228273027293b0a0a0a20202020496e697469616c697a6528746f6b656e416464726573732c206d6178537570706c792c2070726573616c654461746553746172742c2070726573616c654461746553746f702c2070726573616c6550726963652c2070726573616c654d6178537570706c792c2070726573616c655768696c74654c6973744d65726b6c65526f6f742c207075626c6963507269636529207b7d202f2f206576656e740a2020202042757950726573616c652873656e6465722c2076616c75652c20616d6f756e744f757429207b7d202f2f206576656e740a202020204275792873656e6465722c2076616c75652c20616d6f756e744f757429207b7d202f2f206576656e740a20202020576974686472617753616c65732873656e6465722c20616d6f756e7429207b7d202f2f206576656e740a0a0a20202020696e697469616c697a6528746f6b656e416464726573732c206d6178537570706c792c2070726573616c654461746553746172742c2070726573616c654461746553746f702c2070726573616c6550726963652c2070726573616c654d6178537570706c792c2070726573616c655768696c74654c6973744d65726b6c65526f6f742c207075626c6963507269636529207b0a2020202020202020726571756972652821206d6178537570706c792e6c742870726573616c654d6178537570706c79292c2220494e56414c49445f535550504c5922293b0a2020202020202020746869732e746f6b656e41646472657373203d20746f6b656e416464726573733b0a2020202020202020746869732e6d6178537570706c79203d206d6178537570706c793b0a2020202020202020746869732e70726573616c65446174655374617274203d2070726573616c654461746553746172743b0a2020202020202020746869732e70726573616c654461746553746f70203d2070726573616c654461746553746f703b0a2020202020202020746869732e70726573616c655072696365203d2070726573616c6550726963653b0a2020202020202020746869732e70726573616c654d6178537570706c79203d2070726573616c654d6178537570706c793b0a2020202020202020746869732e70726573616c655768696c74654c6973744d65726b6c65526f6f74203d2070726573616c655768696c74654c6973744d65726b6c65526f6f743b0a2020202020202020746869732e7075626c69635072696365203d207075626c696350726963653b0a0a2020202020202020656d69742827496e697469616c697a65272c205b746f6b656e416464726573732c206d6178537570706c792c2070726573616c654461746553746172742c2070726573616c654461746553746f702c2070726573616c6550726963652c2070726573616c654d6178537570706c792c2070726573616c655768696c74654c6973744d65726b6c65526f6f742c207075626c696350726963655d293b0a202020207d0a0a0a2020202062757950726573616c652829207b0a20202020202020207265717569726528746869732e746f6b656e4164647265737320213d2027272c20224e4f545f494e495449414c495a454422293b0a202020202020202072657175697265282120626c6f636b2e74696d657374616d702e6c7428746869732e70726573616c65446174655374617274292c202250524553414c455f4e4f545f5354415254454422293b0a202020202020202072657175697265282120626c6f636b2e74696d657374616d702e677428746869732e70726573616c654461746553746f70292c202250524553414c455f46494e495348454422293b0a20202020202020200a2020202020202020636f6e7374205b616d6f756e744f75742c5d203d20746869732e5f676574416d6f756e744f7574286d73672e76616c75652c20746869732e70726573616c655072696365293b0a20202020202020207265717569726528616d6f756e744f75742e67742830292c2022494e56414c49445f414d4f554e545f4f555422293b0a0a2020202020202020636f6e737420746f74616c537570706c79203d20746f6b656e4f75742e746f74616c537570706c793b0a202020202020202072657175697265282120746f74616c537570706c792e61646428616d6f756e744f7574292e677428746869732e70726573616c654d6178537570706c79292c202750524553414c455f535550504c595f455843454544454427293b0a202020202020202072657175697265282120746f74616c537570706c792e61646428616d6f756e744f7574292e677428746869732e6d6178537570706c79292c2027544f54414c5f535550504c595f455843454544454427293b0a0a2020202020202020746869732e70726573616c654d6178537570706c79203d20746869732e70726573616c654d6178537570706c792e61646428616d6f756e744f7574293b0a202020202020202049455243323028746869732e746f6b656e41646472657373292e6d696e74286d73672e73656e6465722c20616d6f756e744f7574293b0a0a2020202020202020656d6974282742757950726573616c65272c205b6d73672e73656e6465722c206d73672e76616c75652c20616d6f756e744f75745d293b0a202020207d0a0a0a202020206275792829207b0a20202020202020207265717569726528746869732e746f6b656e4164647265737320213d2027272c20224e4f545f494e495449414c495a454422293b0a20202020202020207265717569726528626c6f636b2e74696d657374616d702e677428746869732e70726573616c654461746553746f70292c202250524553414c455f4e4f545f46494e495348454422293b0a20202020202020200a2020202020202020636f6e7374205b616d6f756e744f75742c5d203d20746869732e5f676574416d6f756e744f7574286d73672e76616c75652c20746869732e7075626c69635072696365293b0a20202020202020207265717569726528616d6f756e744f75742e67742830292c2022494e56414c49445f414d4f554e545f4f555422293b0a0a2020202020202020636f6e737420746f74616c537570706c79203d20746f6b656e4f75742e746f74616c537570706c793b0a202020202020202072657175697265282120746f74616c537570706c792e61646428616d6f756e744f7574292e677428746869732e6d6178537570706c79292c2027544f54414c5f535550504c595f455843454544454427293b0a0a202020202020202049455243323028746869732e746f6b656e41646472657373292e6d696e74286d73672e73656e6465722c20616d6f756e744f7574293b0a0a2020202020202020656d69742827427579272c205b6d73672e73656e6465722c206d73672e76616c75652c20616d6f756e744f75745d293b0a202020207d0a0a0a202020205f676574416d6f756e744f757428616d6f756e74496e2c20707269636529207b0a2020202020202020636f6e737420746f6b656e4f7574203d2049455243323028746869732e746f6b656e41646472657373293b0a2020202020202020636f6e737420646563696d616c734f7574203d20746f6b656e4f75742e646563696d616c733b0a2020202020202020636f6e737420616d6f756e744f7574203d20616d6f756e74496e2e646976287072696365292e6d756c283130202a2a20646563696d616c734f7574293b0a202020202020202072657475726e205b616d6f756e744f75745d3b0a202020207d0a0a0a20202020776974686472617753616c657328616d6f756e7429207b0a202020202020202072657175697265286d73672e73656e646572203d3d20746869732e6f776e65722c20274f4e4c595f4f574e455227293b0a202020202020202072657175697265282120616d6f756e742e677428616464726573732874686973292e62616c616e636529293b0a2020202020202020616464726573732874686973292e7472616e73666572286d73672e73656e6465722c20616d6f756e74293b0a0a2020202020202020656d69742827576974686472617753616c6573272c205b6d73672e73656e6465722c20616d6f756e745d293b0a202020207d0a0a7d0a0a2f2f204576656e74730a53616c65536572766963652e70726f746f747970652e496e697469616c697a652e6576656e74203d20747275653b0a53616c65536572766963652e70726f746f747970652e496e697469616c697a652e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202761646472657373272c202775696e74323536275d3b0a0a53616c65536572766963652e70726f746f747970652e42757950726573616c652e6576656e74203d20747275653b0a53616c65536572766963652e70726f746f747970652e42757950726573616c652e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536275d0a0a53616c65536572766963652e70726f746f747970652e4275792e6576656e74203d20747275653b0a53616c65536572766963652e70726f746f747970652e4275792e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536272c202775696e74323536275d0a0a53616c65536572766963652e70726f746f747970652e576974686472617753616c65732e6576656e74203d20747275653b0a53616c65536572766963652e70726f746f747970652e576974686472617753616c65732e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d0a0a2f2f204d6574686f64730a53616c65536572766963652e70726f746f747970652e696e697469616c697a652e696e70757473203d205b2761646472657373272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202775696e74323536272c202761646472657373272c202775696e74323536275d3b0a0a53616c65536572766963652e70726f746f747970652e62757950726573616c652e70617961626c65203d20747275653b0a0a53616c65536572766963652e70726f746f747970652e776974686472617753616c65732e696e70757473203d205b2775696e74323536275d3b0a0a0a72657475726e2053616c65536572766963653b0a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "9", to: "0x", value: "0", chainId: "3615", origin: "1651572566256", v: "1c61", r: "59929cee33cb5db34421e6445095e373f112098a28fa78e51411c6d9c5e0877a", s: "676b05cfcdcbcdc8f5ac9f2da3eb8834e505bd338f505afc9246ed44e9c09a93" }, hash: "0x15a7d8bf0c3ef7b0d877bd57e0410f1d73fe4bc4e3425a606f3dab8b44370584", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572572819", status: 1, gasUsed: "15563" }, 0x41b01d5cf0ac57ead682903fef63b69ac9dbfb9cddf90ad94423ecac16fd6a44: { data: { blockHash: "0x00a5f0a942a24b2965f4c5c46e76099952b74c10e6ec7160cecad9d9290f80cb", blockNumber: "11", transactionIndex: 0, hash: "0x41b01d5cf0ac57ead682903fef63b69ac9dbfb9cddf90ad94423ecac16fd6a44", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "10", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651572577821", v: "", r: "", s: "" }, hash: "0x41b01d5cf0ac57ead682903fef63b69ac9dbfb9cddf90ad94423ecac16fd6a44", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572579441", status: 1, gasUsed: "2427" }, 0xd35ada50d67fe104087551ffcc9b05db08902ced472e9d26982fce6f59b66cf1: { data: { blockHash: "0x00a5f0a942a24b2965f4c5c46e76099952b74c10e6ec7160cecad9d9290f80cb", blockNumber: "11", transactionIndex: 1, hash: "0xd35ada50d67fe104087551ffcc9b05db08902ced472e9d26982fce6f59b66cf1", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "987250", gasPrice: "1000000000", data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000219d2f2a0a4e616d653a20424153494320546f6b656e0a56657273696f6e3a20302e302e330a2a2f0a0a0a636c617373204f776e61626c65207b0a202020206f776e6572203d2027307830303030303030303030303030303030303030303030303030303030303030303030303030303030273b0a0a202020204f776e6572736869705472616e73666572726564286f6c644f776e65722c206e65774f776e657229207b7d202f2f206576656e740a202020200a0a20202020636f6e7374727563746f722829207b0a2020202020202020746869732e6f776e6572203d206d73672e73656e6465723b0a202020207d0a0a202020206765744f776e65722829207b0a202020202020202072657475726e205b746869732e6f776e65725d3b0a202020207d0a202020200a202020207472616e736665724f776e657273686970286e65774f776e657229207b0a202020202020202072657175697265286e65774f776e657220213d20616464726573732830292c20224f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737322293b0a2020202020202020746869732e5f7472616e736665724f776e657273686970286e65774f776e6572293b0a202020207d0a0a202020205f7472616e736665724f776e657273686970286e65774f776e657229207b0a20202020202020206e65774f776e6572203d206e65774f776e65722e746f4c6f7765724361736528293b0a2020202020202020636f6e7374206f6c644f776e6572203d20746869732e6f776e65723b0a2020202020202020746869732e6f776e6572203d206e65774f776e65723b0a2020202020202020656d697428274f776e6572736869705472616e73666572726564272c205b6f6c644f776e65722c206e65774f776e65725d293b0a202020207d0a0a202020205f6f6e6c794f776e6572286e65787429207b0a202020202020202072657175697265286d73672e73656e6465722e746f4c6f776572436173652829203d3d20746869732e6f776e65722c20276f6e6c79206f776e65722063616e2065786563757465207468697327293b0a20202020202020206e65787428293b0a202020207d0a7d0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e6576656e74203d20747275653b0a4f776e61626c652e70726f746f747970652e4f776e6572736869705472616e736665727265642e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564275d3b0a0a2f2f204d6574686f64730a4f776e61626c652e70726f746f747970652e6765744f776e65722e76696577203d20747275653b0a4f776e61626c652e70726f746f747970652e6765744f776e65722e6f757470757473203d205b2761646472657373275d3b0a0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e696e70757473203d205b2761646472657373275d3b0a4f776e61626c652e70726f746f747970652e7472616e736665724f776e6572736869702e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4f776e61626c652e70726f746f747970652e5f7472616e736665724f776e6572736869702e696e7465726e616c203d20747275653b0a0a4f776e61626c652e70726f746f747970652e5f6f6e6c794f776e65722e696e7465726e616c203d20747275653b0a0a0a0a0a636c617373204552433230546f6b656e20657874656e6473204f776e61626c65207b0a202020206e616d65203d2027455243323020546f6b656e273b0a2020202073796d626f6c203d20274552433230273b0a20202020646563696d616c73203d2031383b0a20202020746f74616c537570706c79203d206e756d62657228273027293b0a0a20202020616c6c6f776564203d207b7d3b0a2020202062616c616e636573203d207b7d3b0a0a20202020737570706f72746564496e7465726661636573203d207b0a20202020202020202730783336333732623037273a20747275652c202f2f2045524332300a20202020202020202730783036666464653033273a20747275652c202f2f204552433230206e616d650a20202020202020202730783935643839623431273a20747275652c202f2f2045524332302073796d626f6c0a20202020202020202730783331336365353637273a20747275652c202f2f20455243323020646563696d616c730a202020207d3b0a0a0a202020205472616e736665722873656e6465722c20726563697069656e742c20616d6f756e7429207b7d202f2f206576656e740a20202020417070726f76616c286f776e65722c207370656e6465722c20616d6f756e7429207b7d202f2f206576656e740a202020204d696e74286d696e7465722c20616d6f756e7429207b7d202f2f206576656e740a202020204275726e286275726e65722c20616d6f756e7429207b7d202f2f206576656e740a0a0a2020202062616c616e63654f6628686f6c6465724164647265737329207b0a2020202020202020686f6c64657241646472657373203d20686f6c646572416464726573732e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028686f6c6465724164647265737320696e20746f6b656e42616c616e63657329207b0a202020202020202020202020636f6e73742062616c616e6365203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d3b0a20202020202020202020202072657475726e205b62616c616e63655d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a0a202020207472616e7366657228726563697069656e742c20616d6f756e7429207b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a2020202020202020636f6e73742073656e646572203d206d73672e73656e6465723b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a20202020202020206966202821202873656e64657220696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a202020207472616e7366657246726f6d2873656e6465722c20726563697069656e742c20616d6f756e7429207b0a202020202020202073656e646572203d2073656e6465722e746f4c6f7765724361736528293b0a2020202020202020726563697069656e74203d20726563697069656e742e746f4c6f7765724361736528293b0a20202020202020200a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a0a2020202020202020726571756972652873656e64657220696e20746f6b656e42616c616e6365732c20274d495353494e475f544f4b454e5f42414c414e434527293b0a202020202020202069662028746f6b656e42616c616e6365735b73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a20202020202020200a2020202020202020726571756972652873656e646572203d3d206d73672e73656e646572207c7c202873656e64657220696e20746f6b656e416c6c6f77616e636573202626206d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365735b73656e6465725d292c20224d495353494e475f414c4c4f57414e434522293b0a20202020202020206966202873656e64657220213d206d73672e73656e64657220262620746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e6c7428616d6f756e742929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f414c4c4f57414e434527293b0a20202020202020207d0a0a2020202020202020746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d203d20746f6b656e416c6c6f77616e6365735b73656e6465725d5b6d73672e73656e6465725d2e73756228616d6f756e74293b0a0a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292873656e6465722c20616d6f756e742929207b0a202020202020202020202020746869732e5f696e6372656d656e745573657242616c616e63652e62696e6428746869732928726563697069656e742c20616d6f756e74290a20202020202020207d0a20202020202020200a0a2020202020202020656d697428275472616e73666572272c205b73656e6465722c20726563697069656e742c20616d6f756e745d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a0a20202020616c6c6f77616e6365286f776e65722c207370656e64657229207b0a20202020202020206f776e6572203d206f776e65722e746f4c6f7765724361736528293b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620286f776e657220696e20746f6b656e416c6c6f77616e636573202626207370656e64657220696e20746f6b656e416c6c6f77616e6365735b6f776e65725d29207b0a20202020202020202020202072657475726e205b746f6b656e416c6c6f77616e6365735b6f776e65725d5b7370656e6465725d5d3b0a20202020202020207d0a202020202020202072657475726e205b6e756d6265722830295d3b0a202020207d0a202020200a20202020617070726f7665287370656e6465722c2076616c756529207b0a20202020202020207370656e646572203d207370656e6465722e746f4c6f7765724361736528293b0a202020202020202076617220746f6b656e416c6c6f77616e636573203d20746869732e616c6c6f776564207c7c207b7d3b0a2020202020202020696620282120286d73672e73656e64657220696e20746f6b656e416c6c6f77616e6365732929207b0a202020202020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d203d207b7d3b0a20202020202020207d0a2020202020202020746f6b656e416c6c6f77616e6365735b6d73672e73656e6465725d5b7370656e6465725d203d2076616c75653b0a2020202020202020656d69742827417070726f76616c272c205b6d73672e73656e6465722c207370656e6465722c2076616c75655d293b0a202020202020202072657475726e205b747275655d3b0a202020207d0a0a202020200a202020206d696e742875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6d696e742875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6d696e742875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206d696e7420746f20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f696e6372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e61646428616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274d696e74272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a202020200a202020206275726e2875736572416464726573732c20616d6f756e7429207b0a2020202020202020746869732e5f6275726e2875736572416464726573732c20616d6f756e74293b0a202020207d0a0a202020205f6275726e2875736572416464726573732c20616d6f756e7429207b0a20202020202020207573657241646472657373203d2075736572416464726573732e746f4c6f7765724361736528293b0a2020202020202020696620287573657241646472657373203d3d20616464726573732830292e6164647265737329207b0a2020202020202020202020205f7468726f77282745524332303a206275726e2066726f6d20746865207a65726f206164647265737327293b0a20202020202020207d0a20202020202020200a202020202020202069662028746869732e5f64656372656d656e745573657242616c616e63652e62696e642874686973292875736572416464726573732c20616d6f756e742929207b0a202020202020202020202020746869732e746f74616c537570706c79203d20746869732e746f74616c537570706c792e73756228616d6f756e74293b0a20202020202020207d0a0a2020202020202020656d697428274275726e272c205b75736572416464726573732c20616d6f756e745d293b0a202020207d0a0a0a20202020737570706f727473496e7465726661636528696e74657266616365494429207b0a2020202020202020636f6e7374205f696e7465726661636573203d20746869732e737570706f72746564496e7465726661636573207c7c207b7d3b0a202020202020202072657475726e205b0a2020202020202020202020202828696e74657266616365494420696e205f696e7465726661636573290a202020202020202020202020202020203f205f696e74657266616365735b696e7465726661636549445d0a202020202020202020202020202020203a2066616c7365290a20202020202020205d3b0a202020207d0a0a0a202020205f64656372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a202020202020202069662028616d6f756e742e677428746f6b656e42616c616e6365735b686f6c646572416464726573735d2929207b0a2020202020202020202020205f7468726f772827494e53554646494349454e545f544f4b454e5f42414c414e434527293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e73756228616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a0a202020205f696e6372656d656e745573657242616c616e636528686f6c646572416464726573732c20616d6f756e7429207b0a202020202020202076617220746f6b656e42616c616e636573203d20746869732e62616c616e636573207c7c207b7d3b0a0a202020202020202069662028212028686f6c6465724164647265737320696e20746f6b656e42616c616e6365732929207b0a202020202020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d206e756d6265722830293b0a20202020202020207d0a2020202020202020746f6b656e42616c616e6365735b686f6c646572416464726573735d203d20746f6b656e42616c616e6365735b686f6c646572416464726573735d2e61646428616d6f756e74293b0a202020202020202072657475726e20747275653b0a202020207d0a0a7d0a0a2f2f204576656e74730a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e5472616e736665722e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e417070726f76616c2e696e70757473203d205b276164647265737320696e6465786564272c20276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4d696e742e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4d696e742e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e4275726e2e6576656e74203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e4275726e2e696e70757473203d205b276164647265737320696e6465786564272c202775696e74323536275d3b0a0a2f2f204d6574686f64730a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e696e70757473203d205b2761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e62616c616e63654f662e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e736665722e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e696e70757473203d205b2761646472657373272c202761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e7472616e7366657246726f6d2e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e696e70757473203d205b2761646472657373272c202761646472657373275d3b0a4552433230546f6b656e2e70726f746f747970652e616c6c6f77616e63652e6f757470757473203d205b2775696e74323536275d3b0a0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e617070726f76652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e6d696e742e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6d696e742e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6d696e742e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e6275726e2e696e70757473203d205b2761646472657373272c202775696e74323536275d3b0a4552433230546f6b656e2e70726f746f747970652e6275726e2e6d6f64696669657273203d205b275f6f6e6c794f776e6572275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f6275726e2e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e76696577203d20747275653b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e696e70757473203d205b27627974657334275d3b0a4552433230546f6b656e2e70726f746f747970652e737570706f727473496e746572666163652e6f757470757473203d205b27626f6f6c275d3b0a0a4552433230546f6b656e2e70726f746f747970652e5f64656372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a4552433230546f6b656e2e70726f746f747970652e5f696e6372656d656e745573657242616c616e63652e696e7465726e616c203d20747275653b0a0a0a0a636c617373204261736963546f6b656e20657874656e6473204552433230546f6b656e207b2020200a202020206e616d65203d2027424153494320546f6b656e273b0a2020202073796d626f6c203d20274241534943273b0a0a20202020636f6e7374727563746f722829207b0a2020202020202020737570657228293b0a2020202020202020746869732e746f74616c537570706c79203d206e756d6265722827343230303030303027202b202730303030303030303030303030303030303027293b202f2f20343220303030203030302042415349430a2020202020202020746869732e62616c616e6365735b6d73672e73656e6465725d203d20746869732e746f74616c537570706c793b0a202020207d0a0a7d0a0a0a72657475726e204261736963546f6b656e3b0a00000000000000000000000000000000000000000000000000000000000000000000025b5d000000000000000000000000000000000000000000000000000000000000", nonce: "10", to: "0x", value: "0", chainId: "3615", origin: "1651572575597", v: "1c62", r: "9f401c8da69afce6b0386fe070ad3f5590f4a9145f392e4fc9157a1efdda51fd", s: "7893ea5244c46eef14522140de92b977b08999aea6e49de661916914afcd55b7" }, hash: "0xd35ada50d67fe104087551ffcc9b05db08902ced472e9d26982fce6f59b66cf1", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651572579441", status: 1, gasUsed: "118396" }, 0x1ccfa75127b63b28546df83229b654b3b3e9e6b1f19a39b66b8d4360cec9075a: { data: { blockHash: "0x0057534b6cfdb8717c383af981bbb658d7b6ddd2dc8ad354d8d1707bc2f387a8", blockNumber: "12", transactionIndex: 0, hash: "0x1ccfa75127b63b28546df83229b654b3b3e9e6b1f19a39b66b8d4360cec9075a", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "11", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573244565", v: "", r: "", s: "" }, hash: "0x1ccfa75127b63b28546df83229b654b3b3e9e6b1f19a39b66b8d4360cec9075a", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573244923", status: 1, gasUsed: "10220" }, 0x769cdbd9cf5e7f437df3a7f1b8020b66933d562fcec2b04982a6db364b7e4368: { data: { blockHash: "0x0057534b6cfdb8717c383af981bbb658d7b6ddd2dc8ad354d8d1707bc2f387a8", blockNumber: "12", transactionIndex: 1, hash: "0x769cdbd9cf5e7f437df3a7f1b8020b66933d562fcec2b04982a6db364b7e4368", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "1009225", gasPrice: "1000000000", data: "0xc4d66de8000000000000000000000000c3349025e73b4746d79a30ade1200e5dd2d8f754", nonce: "0", to: "0x92c8b8729d850d99c34c50ab2846ec36169e0024", value: "0", chainId: "3615", origin: "1651573244284", v: "1c62", r: "14746962d66ef43c71ca800f8781b85332e41d4278834ecb0447a97ecef31bbb", s: "7a99c76ff5cfbcaf9bb2a747be7ee0fa98f3c331fffc07b34b660d72e24f6764" }, hash: "0x769cdbd9cf5e7f437df3a7f1b8020b66933d562fcec2b04982a6db364b7e4368", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573244923", status: 1, gasUsed: "37027" }, 0xbbd9bc8af925117d2b6780e7f27663864e9210fc13a138dfac247df2fdc49a1a: { data: { blockHash: "0x00414a114990d56e26f1dcfe2a7408290afeab03d0c1745a9a62c50c5b6ef4b7", blockNumber: "13", transactionIndex: 0, hash: "0xbbd9bc8af925117d2b6780e7f27663864e9210fc13a138dfac247df2fdc49a1a", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "12", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573274928", v: "", r: "", s: "" }, hash: "0xbbd9bc8af925117d2b6780e7f27663864e9210fc13a138dfac247df2fdc49a1a", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573276517", status: 1, gasUsed: "1091" }, 0xc26da3a9c97df9c900dad01068900bbc283f53e191e45aa27e753452f5de4513: { data: { blockHash: "0x00414a114990d56e26f1dcfe2a7408290afeab03d0c1745a9a62c50c5b6ef4b7", blockNumber: "13", transactionIndex: 1, hash: "0xc26da3a9c97df9c900dad01068900bbc283f53e191e45aa27e753452f5de4513", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "1042350", gasPrice: "1000000000", data: "0x485cc9550000000000000000000000002347d36cf0d94b98bab6d6d41c6f94d7214830ad00000000000000000000000092c8b8729d850d99c34c50ab2846ec36169e0024", nonce: "1", to: "0xc3349025e73b4746d79a30ade1200e5dd2d8f754", value: "0", chainId: "3615", origin: "1651573273725", v: "1c61", r: "223c71fe6602674c5a71940b55e166e57249ea32a210f6ee5ac16e3079b27497", s: "6ab19456fcd771a02e3843972f403b89812d9aa3c9f68f4f9157086c1f05d064" }, hash: "0xc26da3a9c97df9c900dad01068900bbc283f53e191e45aa27e753452f5de4513", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573276517", status: 1, gasUsed: "3653" }, 0xaa8218d823df7dc15a2d178e93c01452b8b70dc35a35ce671a7ac576bfc40908: { data: { blockHash: "0x005a49b15afe4e0debb58472946ae60874f2dc624b1c04c3917ea8ee3a311894", blockNumber: "14", transactionIndex: 0, hash: "0xaa8218d823df7dc15a2d178e93c01452b8b70dc35a35ce671a7ac576bfc40908", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "13", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573336527", v: "", r: "", s: "" }, hash: "0xaa8218d823df7dc15a2d178e93c01452b8b70dc35a35ce671a7ac576bfc40908", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573347208", status: 1, gasUsed: "1637" }, 0x30cba6a6848c69fe29d79a6eb059accf512d6021704f3b40f9108382a0f20090: { data: { blockHash: "0x005a49b15afe4e0debb58472946ae60874f2dc624b1c04c3917ea8ee3a311894", blockNumber: "14", transactionIndex: 1, hash: "0x30cba6a6848c69fe29d79a6eb059accf512d6021704f3b40f9108382a0f20090", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "1093175", gasPrice: "1000000000", data: "0x8129fc1c", nonce: "2", to: "0x73f7dafa086d58aedd8146f40dd8a86fcf778b8f", value: "0", chainId: "3615", origin: "1651573333287", v: "1c62", r: "caa8c427b5298000eda99e650f74972b18d074c4a977e47358d42e261b6a505", s: "7c5786fec02c279877cb51f84f101158b09871fa49e613b60c1fe6bae7bd36d2" }, hash: "0x30cba6a6848c69fe29d79a6eb059accf512d6021704f3b40f9108382a0f20090", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573347208", status: 1, gasUsed: "16006" }, 0x342ed5070a8a271f266c3ee9c999953009ee99be3a877c78c80cbe299d0e339b: { data: { blockHash: "0x00e582a804ccdb50fd7c37f92b368113b0c133e26ca6cc2b77dce3b84ec75dc2", blockNumber: "15", transactionIndex: 0, hash: "0x342ed5070a8a271f266c3ee9c999953009ee99be3a877c78c80cbe299d0e339b", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "14", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573372214", v: "", r: "", s: "" }, hash: "0x342ed5070a8a271f266c3ee9c999953009ee99be3a877c78c80cbe299d0e339b", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573373097", status: 1, gasUsed: "3034" }, 0xb32a63ebe1e1c60d471eeeedae9c86a0ff211736e7ccb84078c51da86d0dd2d8: { data: { blockHash: "0x00e582a804ccdb50fd7c37f92b368113b0c133e26ca6cc2b77dce3b84ec75dc2", blockNumber: "15", transactionIndex: 1, hash: "0xb32a63ebe1e1c60d471eeeedae9c86a0ff211736e7ccb84078c51da86d0dd2d8", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "997000", gasPrice: "1000000000", data: "0xc4d66de80000000000000000000000000c3b429fa6cd44a96fcea3cc3fc085d1ddee8158", nonce: "3", to: "0x76c2315b2e96876d0d3db8f036f74fee3b581785", value: "0", chainId: "3615", origin: "1651573367816", v: "1c62", r: "3cae497511bc5d4f7a5fc99adc9f75e965b1680be4d28078c4cce1ebee8f88fb", s: "7dc97ce0d35f7e751c8540223f121326a5af3a2756cac03b4c3361e2b2da27bf" }, hash: "0xb32a63ebe1e1c60d471eeeedae9c86a0ff211736e7ccb84078c51da86d0dd2d8", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573373097", status: 1, gasUsed: "15632" }, 0x4e28d78f2bd6c539618307eb60e57a4afba941eb4d75606ac5fc37747de5e9ff: { data: { blockHash: "0x0082e934a641129a5c4797b95d0bdd2425f9cf81467df3f9a3b5c8e079fabc51", blockNumber: "16", transactionIndex: 0, hash: "0x4e28d78f2bd6c539618307eb60e57a4afba941eb4d75606ac5fc37747de5e9ff", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "15", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573733167", v: "", r: "", s: "" }, hash: "0x4e28d78f2bd6c539618307eb60e57a4afba941eb4d75606ac5fc37747de5e9ff", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573739426", status: 1, gasUsed: "19697" }, 0x194cae77eda01be54da0d49a51846deb0b138797565b73b012900c782acd2daa: { data: { blockHash: "0x0082e934a641129a5c4797b95d0bdd2425f9cf81467df3f9a3b5c8e079fabc51", blockNumber: "16", transactionIndex: 1, hash: "0x194cae77eda01be54da0d49a51846deb0b138797565b73b012900c782acd2daa", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "2223850", gasPrice: "1000000000", data: "0x485cc9550000000000000000000000002347d36cf0d94b98bab6d6d41c6f94d7214830ad00000000000000000000000092c8b8729d850d99c34c50ab2846ec36169e0024", nonce: "4", to: "0xe58b9e60afe16e5707376e84372fa1dfbebf71f0", value: "0", chainId: "3615", origin: "1651573731459", v: "1c61", r: "e90cbfbd0561483a0fb4836b3d4a20428b17f42e9e9e6ef71636e2baf5923407", s: "983e71c3c7ef66aad95f9b19afcf458f6553cfa9588e51a6cb716d1bbd07f21" }, hash: "0x194cae77eda01be54da0d49a51846deb0b138797565b73b012900c782acd2daa", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573739426", status: 0, gasUsed: "10899" }, 0xd47ddc32fa6151aa2cf9c1a8e416128bfbf6f29333acc1311a822c4de4dc0b8e: { data: { blockHash: "0x00ce0b46cfcc43e1f94282a4add1dafe308d0b612049cbd65e3f57eba677831f", blockNumber: "17", transactionIndex: 0, hash: "0xd47ddc32fa6151aa2cf9c1a8e416128bfbf6f29333acc1311a822c4de4dc0b8e", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "16", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651573774435", v: "", r: "", s: "" }, hash: "0xd47ddc32fa6151aa2cf9c1a8e416128bfbf6f29333acc1311a822c4de4dc0b8e", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573779861", status: 1, gasUsed: "3942" }, 0x2294b753e054ffa64adcafc660939c2f9d40009cde3175a78eb57c6985399b65: { data: { blockHash: "0x00ce0b46cfcc43e1f94282a4add1dafe308d0b612049cbd65e3f57eba677831f", blockNumber: "17", transactionIndex: 1, hash: "0x2294b753e054ffa64adcafc660939c2f9d40009cde3175a78eb57c6985399b65", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "2223850", gasPrice: "1000000000", data: "0x485cc9550000000000000000000000002347d36cf0d94b98bab6d6d41c6f94d7214830ad00000000000000000000000092c8b8729d850d99c34c50ab2846ec36169e0024", nonce: "11", to: "0xe58b9e60afe16e5707376e84372fa1dfbebf71f0", value: "0", chainId: "3615", origin: "1651573773098", v: "1c61", r: "6aed032c0326f470058228edb502c7232d6f6c3d409ab4317789e75a03dcfc2e", s: "9fbf54b42a0b6e840feb40775d48313fa1bd4ff4079d5cea00bcb8b913d28c6" }, hash: "0x2294b753e054ffa64adcafc660939c2f9d40009cde3175a78eb57c6985399b65", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651573779861", status: 1, gasUsed: "19070" }, 0xb67035d14aa288f02f17022e4c0a4cfc7174c405b14958c3d8025a30c37db03c: { data: { blockHash: "0x00194ae3567ca5cb5c82eba81fbc2ef50b1031af4cfd42da84fe7bfc2f78f109", blockNumber: "18", transactionIndex: 0, hash: "0xb67035d14aa288f02f17022e4c0a4cfc7174c405b14958c3d8025a30c37db03c", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "17", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651574069918", v: "", r: "", s: "" }, hash: "0xb67035d14aa288f02f17022e4c0a4cfc7174c405b14958c3d8025a30c37db03c", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574073240", status: 1, gasUsed: "30545" }, 0xf5ff4d07877270f43c650fc44c0b7f3746af8bc96b81c331232a8e546eddd118: { data: { blockHash: "0x00194ae3567ca5cb5c82eba81fbc2ef50b1031af4cfd42da84fe7bfc2f78f109", blockNumber: "18", transactionIndex: 1, hash: "0xf5ff4d07877270f43c650fc44c0b7f3746af8bc96b81c331232a8e546eddd118", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "709300", gasPrice: "1000000000", data: "0xac9b697d00000000000000000000000076c2315b2e96876d0d3db8f036f74fee3b58178500000000000000000000000000000000000000000000152d02c7e14af680000000000000000000000000000000000000000000000000000000000180897a8ddc0000000000000000000000000000000000000000000000000000018089e8ee8a0000000000000000000000000000000000000000000000000429d069189e000000000000000000000000000000000000000000000000032d26d12e980b60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000", nonce: "5", to: "0x0c3b429fa6cd44a96fcea3cc3fc085d1ddee8158", value: "0", chainId: "3615", origin: "1651574066662", v: "1c62", r: "1ac7e1f679fdcbd748015a87ff3b8cc632816d44dc483dbd7c14c931d98f60a1", s: "39d9888efd78891aaed5aeef22927ee818208f1ad9c6c046ffcf3aba98f30998" }, hash: "0xf5ff4d07877270f43c650fc44c0b7f3746af8bc96b81c331232a8e546eddd118", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574073240", status: 1, gasUsed: "38720" }, 0x9b2847c8ad869e5c7088bf9a0085d59a8c2c33254018d8f0d2f3c7ac3d432920: { data: { blockHash: "0x0064d5547aa2ffd14e4859e5f7ddd42920ac3f7c3b30fd37df1a124c8ebed045", blockNumber: "19", transactionIndex: 0, hash: "0x9b2847c8ad869e5c7088bf9a0085d59a8c2c33254018d8f0d2f3c7ac3d432920", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "18", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651574148254", v: "", r: "", s: "" }, hash: "0x9b2847c8ad869e5c7088bf9a0085d59a8c2c33254018d8f0d2f3c7ac3d432920", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574150882", status: 1, gasUsed: "2533" }, 0x3f7a6a28c7cf9385da34dc150f359bbc079ac93b8730f3decfcaedd997097d87: { data: { blockHash: "0x0064d5547aa2ffd14e4859e5f7ddd42920ac3f7c3b30fd37df1a124c8ebed045", blockNumber: "19", transactionIndex: 1, hash: "0x3f7a6a28c7cf9385da34dc150f359bbc079ac93b8730f3decfcaedd997097d87", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "756800", gasPrice: "1000000000", data: "0x9fcdec61", nonce: "6", to: "0x0c3b429fa6cd44a96fcea3cc3fc085d1ddee8158", value: "500000000000000000000", chainId: "3615", origin: "1651574147126", v: "1c62", r: "ac4c77e38c392a0e4a6a77d199c5c540cbe08ea1b4f4c6d8bfdc9b46890caf35", s: "8a63e6112e1fcef19f49ce9ebb5be6a55da282e7e5b3fc5c7ca1e32e7579025" }, hash: "0x3f7a6a28c7cf9385da34dc150f359bbc079ac93b8730f3decfcaedd997097d87", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574150882", status: 0, gasUsed: "24231" }, 0x2abee744990aac1acae099789c5faef85dfaeee92db840996c1739dc1de001b4: { data: { blockHash: "0x006f9e87c3f7e9810f5cd88f8b256d3a72f1e428c4589aaebbf55a39f705fe67", blockNumber: "20", transactionIndex: 0, hash: "0x2abee744990aac1acae099789c5faef85dfaeee92db840996c1739dc1de001b4", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "19", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1651574490946", v: "", r: "", s: "" }, hash: "0x2abee744990aac1acae099789c5faef85dfaeee92db840996c1739dc1de001b4", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574491426", status: 1, gasUsed: "4760" }, 0x60334c2fbb9a0da329f6503562b6d3abb0f589f735490b6cb548804d357d9f32: { data: { blockHash: "0x006f9e87c3f7e9810f5cd88f8b256d3a72f1e428c4589aaebbf55a39f705fe67", blockNumber: "20", transactionIndex: 1, hash: "0x60334c2fbb9a0da329f6503562b6d3abb0f589f735490b6cb548804d357d9f32", from: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", gasLimit: "756800", gasPrice: "1000000000", data: "0x9fcdec61", nonce: "7", to: "0x0c3b429fa6cd44a96fcea3cc3fc085d1ddee8158", value: "500000000000000000000", chainId: "3615", origin: "1651574488415", v: "1c62", r: "beeead632fec2c3f28bdd652990b3c1906381fb9554c0be4a5b4ffefc8609e56", s: "2cffe1f1903020fec2d55b28138b7a0c27c6318aff7c3b027c09b252ebccb0dc" }, hash: "0x60334c2fbb9a0da329f6503562b6d3abb0f589f735490b6cb548804d357d9f32", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1651574491426", status: 0, gasUsed: "26482" }, 0x41af027d89d15b18be4560bcbe6ac3b1ce537076e699b17359cec57781b340fa: { data: { blockHash: "0x000690dbd86540f6288a531c7c62b84ab7df7ee3fd2f47898d7d46ece93cafb2", blockNumber: "21", transactionIndex: 0, hash: "0x41af027d89d15b18be4560bcbe6ac3b1ce537076e699b17359cec57781b340fa", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "20", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1690714320713", v: "", r: "", s: "" }, hash: "0x41af027d89d15b18be4560bcbe6ac3b1ce537076e699b17359cec57781b340fa", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1690714322068", status: 1, gasUsed: "10894" }, 0xb3abc261f15cb3e4270615cc4c5f5a9bfab9ffbf6772c6361092d95732329b13: { data: { blockHash: "0x000690dbd86540f6288a531c7c62b84ab7df7ee3fd2f47898d7d46ece93cafb2", blockNumber: "21", transactionIndex: 1, hash: "0xb3abc261f15cb3e4270615cc4c5f5a9bfab9ffbf6772c6361092d95732329b13", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000690bf6cfce1affb8c0e72fe863bd0a2ab06374cc", nonce: "12", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1690714315984", v: "1c62", r: "b96813a3bb66cae8a9e2a1adfef6d0c0e897caa0bf17a4a2792314ad2e8516dc", s: "741345b60cec7bda4cc8d1a79b0b1eac7583772900910baf825c696e1b09dc92" }, hash: "0xb3abc261f15cb3e4270615cc4c5f5a9bfab9ffbf6772c6361092d95732329b13", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1690714322068", status: 0, gasUsed: "12980" }, 0x1790ba27fa43a3b8b107a762a4601833623ccd5855c3624e2aff68130b8776e6: { data: { blockHash: "0x00e5251a9e96c0ac07d69d28521ce9df5e149bf0b7c398c665cadbe55915e269", blockNumber: "22", transactionIndex: 0, hash: "0x1790ba27fa43a3b8b107a762a4601833623ccd5855c3624e2aff68130b8776e6", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "21", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136409591", v: "", r: "", s: "" }, hash: "0x1790ba27fa43a3b8b107a762a4601833623ccd5855c3624e2aff68130b8776e6", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136410432", status: 1, gasUsed: "1798" }, 0x08838c57c86c750b35a3c2318e39f2d9685225d4080c2c38d24b7ef5add02d9b: { data: { blockHash: "0x00e5251a9e96c0ac07d69d28521ce9df5e149bf0b7c398c665cadbe55915e269", blockNumber: "22", transactionIndex: 1, hash: "0x08838c57c86c750b35a3c2318e39f2d9685225d4080c2c38d24b7ef5add02d9b", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "13", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136406721", v: "1c62", r: "62b5516c4d8df50d01b7f5f7592d48a5f26fe4719fd67d120c715ed4b901ff0c", s: "3cbaeefaf1ecf7c18931e12e3ab2baf54a8d5322f19c9ada47d4cc6b5288e4c1" }, hash: "0x08838c57c86c750b35a3c2318e39f2d9685225d4080c2c38d24b7ef5add02d9b", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136410432", status: 0, gasUsed: "6259" }, 0x0fb59c7e470b8e22ead1eed3b19fb9d59258a7c1dbe2bff2fcbaeea6361d46c0: { data: { blockHash: "0x0067ca596f5e893ca6e0eef72f493d1978c209dfe986a7a92910c8686f9b966f", blockNumber: "23", transactionIndex: 0, hash: "0x0fb59c7e470b8e22ead1eed3b19fb9d59258a7c1dbe2bff2fcbaeea6361d46c0", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "22", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136415433", v: "", r: "", s: "" }, hash: "0x0fb59c7e470b8e22ead1eed3b19fb9d59258a7c1dbe2bff2fcbaeea6361d46c0", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136420516", status: 1, gasUsed: "681" }, 0xe51decb9e38a2bb8630f947fcd3e45e61bcdeae36f8e92130c7f06d8d344ed9a: { data: { blockHash: "0x0067ca596f5e893ca6e0eef72f493d1978c209dfe986a7a92910c8686f9b966f", blockNumber: "23", transactionIndex: 1, hash: "0xe51decb9e38a2bb8630f947fcd3e45e61bcdeae36f8e92130c7f06d8d344ed9a", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "14", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136411229", v: "1c61", r: "c919f19797471643d595821753236b6e021082f4931f53d0b7b3eb50029fe0f5", s: "696d64d921b93a7f839a42ebd7e8d54f5834397ea2b690eb253c224803b8bfb" }, hash: "0xe51decb9e38a2bb8630f947fcd3e45e61bcdeae36f8e92130c7f06d8d344ed9a", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136420516", status: 0, gasUsed: "1229" }, 0xf01070f8909570ad2744f059270511d7ba0d38535b119cc2af507fa194168e4d: { data: { blockHash: "0x00910ce3374fbac9cd36c3b3c0455e57453539f71c4c17ab99dd868fc23f7fab", blockNumber: "24", transactionIndex: 0, hash: "0xf01070f8909570ad2744f059270511d7ba0d38535b119cc2af507fa194168e4d", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "23", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136425518", v: "", r: "", s: "" }, hash: "0xf01070f8909570ad2744f059270511d7ba0d38535b119cc2af507fa194168e4d", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136426450", status: 1, gasUsed: "503" }, 0xf5a2934425fc3114cd64b383f3adfd9813f0c1b4bb2147a9bc37cb6bd6a72b2a: { data: { blockHash: "0x00910ce3374fbac9cd36c3b3c0455e57453539f71c4c17ab99dd868fc23f7fab", blockNumber: "24", transactionIndex: 1, hash: "0xf5a2934425fc3114cd64b383f3adfd9813f0c1b4bb2147a9bc37cb6bd6a72b2a", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "15", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136420786", v: "1c61", r: "13cc3d97fbd971ded5ee668bc443ff5f0578a28d72997da72064f89a89e6763c", s: "7920b01a54414cda22674d309bfe424a339493204555a4291c983d2e7215ad3" }, hash: "0xf5a2934425fc3114cd64b383f3adfd9813f0c1b4bb2147a9bc37cb6bd6a72b2a", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136426450", status: 0, gasUsed: "1444" }, 0xf1d86d75ab2c63629371c58c70dfb9b013da1585cf6664e9c25bead876f29fe6: { data: { blockHash: "0x00eb6d9c7468838daa19609330108bf2a43393bec98e8b8924d53cc7b89e40f9", blockNumber: "25", transactionIndex: 0, hash: "0xf1d86d75ab2c63629371c58c70dfb9b013da1585cf6664e9c25bead876f29fe6", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "24", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136431451", v: "", r: "", s: "" }, hash: "0xf1d86d75ab2c63629371c58c70dfb9b013da1585cf6664e9c25bead876f29fe6", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136443883", status: 1, gasUsed: "1112" }, 0xe04941629d6246a16a45de1a024aaf9cc18110ef6c2e1dc4f38cbea4a81c2dd2: { data: { blockHash: "0x00eb6d9c7468838daa19609330108bf2a43393bec98e8b8924d53cc7b89e40f9", blockNumber: "25", transactionIndex: 1, hash: "0xe04941629d6246a16a45de1a024aaf9cc18110ef6c2e1dc4f38cbea4a81c2dd2", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "16", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136429238", v: "1c62", r: "57923fab44764235e922bc52f340e8e72cf11bbdf34835e45ceb49e60b4ca6b3", s: "73b1acabe3613f87811c32b0cad98a938787aa8c4b9e323f4b4bfe00b39317b0" }, hash: "0xe04941629d6246a16a45de1a024aaf9cc18110ef6c2e1dc4f38cbea4a81c2dd2", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136443883", status: 0, gasUsed: "5952" }, 0xf528ac61d82abd653b1f6e157b8bca69c7fcb386977dbe7cf47e396c9834db5b: { data: { blockHash: "0x00f4ba73a9f760b6a0b4c862034535e3db461c803270f75f4aa4232797643d50", blockNumber: "26", transactionIndex: 0, hash: "0xf528ac61d82abd653b1f6e157b8bca69c7fcb386977dbe7cf47e396c9834db5b", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "25", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136448885", v: "", r: "", s: "" }, hash: "0xf528ac61d82abd653b1f6e157b8bca69c7fcb386977dbe7cf47e396c9834db5b", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136455225", status: 1, gasUsed: "991" }, 0x121ccb7eaf7a50218653db6da441ce98ba31ac8ea2a56211ad512f786723d799: { data: { blockHash: "0x00f4ba73a9f760b6a0b4c862034535e3db461c803270f75f4aa4232797643d50", blockNumber: "26", transactionIndex: 1, hash: "0x121ccb7eaf7a50218653db6da441ce98ba31ac8ea2a56211ad512f786723d799", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "17", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136445425", v: "1c61", r: "4087e83510a2909f2f00c9afe00a6015e21b59a9cf48ba0cd739a4af988d2fc2", s: "61e53064bc554fcdf27313fb2a76ab0f883c562b9592d625939d62b848d63f23" }, hash: "0x121ccb7eaf7a50218653db6da441ce98ba31ac8ea2a56211ad512f786723d799", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136455225", status: 0, gasUsed: "929" }, 0xef49572448544b2409e7f177f49c41723947ff4abf57e24803a2581a3afaf2a6: { data: { blockHash: "0x0004d466554881f61a680e105a7a77683f40cfd4ba1d17c1193d3b1334bebb0c", blockNumber: "27", transactionIndex: 0, hash: "0xef49572448544b2409e7f177f49c41723947ff4abf57e24803a2581a3afaf2a6", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "26", to: "0xff7cee092641a91e3a105bf0df0748bc11216973", value: "250000000000000000000", chainId: "3615", origin: "1702136669514", v: "", r: "", s: "" }, hash: "0xef49572448544b2409e7f177f49c41723947ff4abf57e24803a2581a3afaf2a6", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136670178", status: 1, gasUsed: "3015" }, 0x6c731343fb71c49d30ba2ad776743a3ac994b55559f1a7cacb6d2edacc876b43: { data: { blockHash: "0x0004d466554881f61a680e105a7a77683f40cfd4ba1d17c1193d3b1334bebb0c", blockNumber: "27", transactionIndex: 1, hash: "0x6c731343fb71c49d30ba2ad776743a3ac994b55559f1a7cacb6d2edacc876b43", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "18", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136665973", v: "1c61", r: "89464680f09640478b764b989a943c1a86db2cb786cb7ad3dfe181b41f69a166", s: "37bd73da6c796559761a516c69c3e4778aed293cab346ee1abc894329af2df54" }, hash: "0x6c731343fb71c49d30ba2ad776743a3ac994b55559f1a7cacb6d2edacc876b43", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136670178", status: 0, gasUsed: "1355" }, 0x802b4da2ec330e7f51b5523db7aa08d31b5bae1c9f25e516f94c32e83c1b20e0: { data: { blockHash: "0x0023d94e150cd9d0209c569310198178011944804b3d80e0cd7403db76c7886e", blockNumber: "28", transactionIndex: 0, hash: "0x802b4da2ec330e7f51b5523db7aa08d31b5bae1c9f25e516f94c32e83c1b20e0", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "27", to: "0xff7cee092641a91e3a105bf0df0748bc11216973", value: "250000000000000000000", chainId: "3615", origin: "1702136675333", v: "", r: "", s: "" }, hash: "0x802b4da2ec330e7f51b5523db7aa08d31b5bae1c9f25e516f94c32e83c1b20e0", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136675685", status: 1, gasUsed: "2447" }, 0x270780dd570baa0939e878e62eff1a299b708a6a2a376cce05110c1efdc4dd7d: { data: { blockHash: "0x0023d94e150cd9d0209c569310198178011944804b3d80e0cd7403db76c7886e", blockNumber: "28", transactionIndex: 1, hash: "0x270780dd570baa0939e878e62eff1a299b708a6a2a376cce05110c1efdc4dd7d", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "19", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136674101", v: "1c61", r: "d72534f6555af0b83068666852f5aa5fcb96ef0564bc30baad7ad556d3c5ac4", s: "6e96ca222952a82913ee6a6fed3c535a8b50a4147b0b46f337407a83e280c6f9" }, hash: "0x270780dd570baa0939e878e62eff1a299b708a6a2a376cce05110c1efdc4dd7d", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136675685", status: 0, gasUsed: "1192" }, 0xdc4c777af734c8539685508a03ce8d64a4d49525568287e751a7b41d003fa3b3: { data: { blockHash: "0x00dc4623b40d1208a92adaf738aa0f099c50568d2fecf77ecac15328a039dc4f", blockNumber: "29", transactionIndex: 0, hash: "0xdc4c777af734c8539685508a03ce8d64a4d49525568287e751a7b41d003fa3b3", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "28", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136680713", v: "", r: "", s: "" }, hash: "0xdc4c777af734c8539685508a03ce8d64a4d49525568287e751a7b41d003fa3b3", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136681456", status: 1, gasUsed: "2318" }, 0xf0862f9fa47e615ca5f7afd8ab3e70c109f5b7f9f86f2d686a919464a4f2149c: { data: { blockHash: "0x00dc4623b40d1208a92adaf738aa0f099c50568d2fecf77ecac15328a039dc4f", blockNumber: "29", transactionIndex: 1, hash: "0xf0862f9fa47e615ca5f7afd8ab3e70c109f5b7f9f86f2d686a919464a4f2149c", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "20", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136676590", v: "1c61", r: "2fd6bc46b6ef144782f408abf2e6c81dd50e0c66e2a7c29cb98e8d80e6f62980", s: "29a6110bd245044235f33a70ff88bae92ca3b1f0f3e1f422ae6605d06ede2dcd" }, hash: "0xf0862f9fa47e615ca5f7afd8ab3e70c109f5b7f9f86f2d686a919464a4f2149c", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136681456", status: 0, gasUsed: "475" }, 0xdba34e001f8f0e3c6b71556127a3bd50c32a1906c5b196f1aee25929ee8ef500: { data: { blockHash: "0x000c1c50d4063f616242644954f26c2bc38a523b21a9d324e3876ad56a0fd93c", blockNumber: "30", transactionIndex: 0, hash: "0xdba34e001f8f0e3c6b71556127a3bd50c32a1906c5b196f1aee25929ee8ef500", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "29", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702136841557", v: "", r: "", s: "" }, hash: "0xdba34e001f8f0e3c6b71556127a3bd50c32a1906c5b196f1aee25929ee8ef500", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136844666", status: 1, gasUsed: "4881" }, 0x67a182fd9147cc9a2160566428468b52b8880b7900f745d03074438fdbf953af: { data: { blockHash: "0x000c1c50d4063f616242644954f26c2bc38a523b21a9d324e3876ad56a0fd93c", blockNumber: "30", transactionIndex: 1, hash: "0x67a182fd9147cc9a2160566428468b52b8880b7900f745d03074438fdbf953af", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "21", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702136839457", v: "1c61", r: "4be4ddb141689445ff53784e08465d4d7f45aa9e46efcc18eabe5676e6e6b02e", s: "54eee4c931bb33f42ea849cfb7f2e574854f10313fbc265e2673c8e6a571fbcd" }, hash: "0x67a182fd9147cc9a2160566428468b52b8880b7900f745d03074438fdbf953af", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702136844666", status: 0, gasUsed: "808" }, 0x70d2bfbb497dd5336d6f5a3016807243fb9ccf9e117fb5a7a4c5f85766155220: { data: { blockHash: "0x000c646f9f2c7f3830ab77d0ed5e382099b67cacfc2eb22c72c4b51c3c5f3ed0", blockNumber: "31", transactionIndex: 0, hash: "0x70d2bfbb497dd5336d6f5a3016807243fb9ccf9e117fb5a7a4c5f85766155220", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "30", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137274143", v: "", r: "", s: "" }, hash: "0x70d2bfbb497dd5336d6f5a3016807243fb9ccf9e117fb5a7a4c5f85766155220", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137278416", status: 1, gasUsed: "1690" }, 0x64c30834336d117661c16a25ab6e47b2819a8518e3df66ac4c4e3b4b4974e42a: { data: { blockHash: "0x000c646f9f2c7f3830ab77d0ed5e382099b67cacfc2eb22c72c4b51c3c5f3ed0", blockNumber: "31", transactionIndex: 1, hash: "0x64c30834336d117661c16a25ab6e47b2819a8518e3df66ac4c4e3b4b4974e42a", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "22", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137270974", v: "1c62", r: "b235a69b65903d6326b42ec27e65bd698ac706f8366b1f7b1bf593e844f87ba7", s: "591833980021f6defb2671c36cc826ea1efab4d9c1eee28dcfe2e2f8f61752c" }, hash: "0x64c30834336d117661c16a25ab6e47b2819a8518e3df66ac4c4e3b4b4974e42a", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137278416", status: 0, gasUsed: "3578" }, 0xbe6bbc00c3c0486cf1889ab21ebeca901506b7785825fbb9b2cefe776089036f: { data: { blockHash: "0x005b608b0f73a663a5edf7b94cacedf12a990728daa35410e732d04cf5961205", blockNumber: "32", transactionIndex: 0, hash: "0xbe6bbc00c3c0486cf1889ab21ebeca901506b7785825fbb9b2cefe776089036f", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "31", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137323444", v: "", r: "", s: "" }, hash: "0xbe6bbc00c3c0486cf1889ab21ebeca901506b7785825fbb9b2cefe776089036f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137324876", status: 1, gasUsed: "6915" }, 0x2c32e99703210bdadc2bba260f770e7e8c790690c2149c9ce0255d3bc55cf0fe: { data: { blockHash: "0x005b608b0f73a663a5edf7b94cacedf12a990728daa35410e732d04cf5961205", blockNumber: "32", transactionIndex: 1, hash: "0x2c32e99703210bdadc2bba260f770e7e8c790690c2149c9ce0255d3bc55cf0fe", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "23", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137323133", v: "1c61", r: "71ef73ebcfa51ed1836f0968de9126ddabd8efe7cb226d5102b4962d3e8ccbc1", s: "6fc776b60f18d71b65c69fb95cca9a82a0894b35e43c0f7e98ee861f20acdfa" }, hash: "0x2c32e99703210bdadc2bba260f770e7e8c790690c2149c9ce0255d3bc55cf0fe", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137324876", status: 0, gasUsed: "10298" }, 0x02eebf08f69a19407314dd507477a3a341de8116a003064e3afac510e2939e11: { data: { blockHash: "0x0034c1f486ba0099f048e3c6f963fa5c2178a97eb9b472473ff8304011266d28", blockNumber: "33", transactionIndex: 0, hash: "0x02eebf08f69a19407314dd507477a3a341de8116a003064e3afac510e2939e11", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "32", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137329878", v: "", r: "", s: "" }, hash: "0x02eebf08f69a19407314dd507477a3a341de8116a003064e3afac510e2939e11", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137352344", status: 1, gasUsed: "1490" }, 0x43d2e7922f2608ad4c091c265f857962698c79c4aa6385793ec39034004579ee: { data: { blockHash: "0x0034c1f486ba0099f048e3c6f963fa5c2178a97eb9b472473ff8304011266d28", blockNumber: "33", transactionIndex: 1, hash: "0x43d2e7922f2608ad4c091c265f857962698c79c4aa6385793ec39034004579ee", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "24", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137327308", v: "1c62", r: "110e68ad3bfb1f5cab695d63eb50ec29c57f3f7df6e4cf933d130fd9a3cb2862", s: "144c3f5e3acb70c60cb576c9b695be9803bef71c9145d4fbf4fe578aa0180b86" }, hash: "0x43d2e7922f2608ad4c091c265f857962698c79c4aa6385793ec39034004579ee", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137352344", status: 0, gasUsed: "1170" }, 0x4e5bc4cca076ee80bbc868ec882161baaf85851a2dc4dfe93931d2afefa73a31: { data: { blockHash: "0x004b8380706b89f74b5126e4ab5eb60c5811587ed8aed703286756b1adfefbb6", blockNumber: "34", transactionIndex: 0, hash: "0x4e5bc4cca076ee80bbc868ec882161baaf85851a2dc4dfe93931d2afefa73a31", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "33", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137823067", v: "", r: "", s: "" }, hash: "0x4e5bc4cca076ee80bbc868ec882161baaf85851a2dc4dfe93931d2afefa73a31", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137826739", status: 1, gasUsed: "2308" }, 0x83b887180a7347a09434575e16a0d50565c8b884367a1ca154b7e9325c0934ed: { data: { blockHash: "0x004b8380706b89f74b5126e4ab5eb60c5811587ed8aed703286756b1adfefbb6", blockNumber: "34", transactionIndex: 1, hash: "0x83b887180a7347a09434575e16a0d50565c8b884367a1ca154b7e9325c0934ed", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "25", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137819484", v: "1c62", r: "4b96f61e91579b312e50c061bc1b1c2f40b4bcfca28869472572533a5c58259d", s: "44971e3fede1a485287c251c0bb4e9b764929d29d977da7701d7326ef90a2d98" }, hash: "0x83b887180a7347a09434575e16a0d50565c8b884367a1ca154b7e9325c0934ed", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137826739", status: 0, gasUsed: "3647" }, 0xb27ebcd8c98776e1b99835d69133df247b7e28211de7dd1a0d04bf42d6d9f4ed: { data: { blockHash: "0x00f81029c4e5f06c4fb592473afc90fe1f4651488ed142a63220ff8693535919", blockNumber: "35", transactionIndex: 0, hash: "0xb27ebcd8c98776e1b99835d69133df247b7e28211de7dd1a0d04bf42d6d9f4ed", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "34", to: "0xff7cee092641a91e3a105bf0df0748bc11216973", value: "250000000000000000000", chainId: "3615", origin: "1702137831931", v: "", r: "", s: "" }, hash: "0xb27ebcd8c98776e1b99835d69133df247b7e28211de7dd1a0d04bf42d6d9f4ed", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137833892", status: 1, gasUsed: "3655" }, 0x8bd2e15463e7077c51933818c91b92287dc272f79db15ebd48cf6dc84ea0bae2: { data: { blockHash: "0x00f81029c4e5f06c4fb592473afc90fe1f4651488ed142a63220ff8693535919", blockNumber: "35", transactionIndex: 1, hash: "0x8bd2e15463e7077c51933818c91b92287dc272f79db15ebd48cf6dc84ea0bae2", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "26", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137827637", v: "1c61", r: "be82a89ba20515d9ff891561181623171c5661be9a5dd73f4656e65eb684ec42", s: "440fcf40d590f1aac3f23aeb1695aa826bd5af18a96fde353838c924b8c6fdb9" }, hash: "0x8bd2e15463e7077c51933818c91b92287dc272f79db15ebd48cf6dc84ea0bae2", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137833892", status: 0, gasUsed: "1273" }, 0x9814d82d46954129753942f06a3121515183cad43afd6a9adda690ad0929e055: { data: { blockHash: "0x0053a1f89231de7c1b598893293511ac148473593f36e6059d92b27932a43dc2", blockNumber: "36", transactionIndex: 0, hash: "0x9814d82d46954129753942f06a3121515183cad43afd6a9adda690ad0929e055", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "35", to: "0xff7cee092641a91e3a105bf0df0748bc11216973", value: "250000000000000000000", chainId: "3615", origin: "1702137839061", v: "", r: "", s: "" }, hash: "0x9814d82d46954129753942f06a3121515183cad43afd6a9adda690ad0929e055", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137840582", status: 1, gasUsed: "2820" }, 0x08792b452d315a340f7f4ee093822b61c4386c80706e8e2c71d80ba12c9bdd9f: { data: { blockHash: "0x0053a1f89231de7c1b598893293511ac148473593f36e6059d92b27932a43dc2", blockNumber: "36", transactionIndex: 1, hash: "0x08792b452d315a340f7f4ee093822b61c4386c80706e8e2c71d80ba12c9bdd9f", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "27", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137835843", v: "1c62", r: "484eb98963b525931f2e121fac3199d97f81c804ae9f698594ae953f28849b54", s: "49e147bb97f3389481c5b238811968fd0e228b2af6350fa2d72b7a69a15d1e6b" }, hash: "0x08792b452d315a340f7f4ee093822b61c4386c80706e8e2c71d80ba12c9bdd9f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137840582", status: 0, gasUsed: "1171" }, 0x7d18a9d6291cc89a506368e78ec6be69019c90f278a6281b4f7bd2e20e5142ac: { data: { blockHash: "0x001cd21e7ab3f7739d453af26aa167b3a3320eb9e01b9fd4329c5433b0650148", blockNumber: "37", transactionIndex: 0, hash: "0x7d18a9d6291cc89a506368e78ec6be69019c90f278a6281b4f7bd2e20e5142ac", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "36", to: "0xff7cee092641a91e3a105bf0df0748bc11216973", value: "250000000000000000000", chainId: "3615", origin: "1702137845764", v: "", r: "", s: "" }, hash: "0x7d18a9d6291cc89a506368e78ec6be69019c90f278a6281b4f7bd2e20e5142ac", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137848089", status: 1, gasUsed: "2598" }, 0x72362ea00b8ea3be471e03529de92cc594e89e21ff907040dcb578d311c329ce: { data: { blockHash: "0x001cd21e7ab3f7739d453af26aa167b3a3320eb9e01b9fd4329c5433b0650148", blockNumber: "37", transactionIndex: 1, hash: "0x72362ea00b8ea3be471e03529de92cc594e89e21ff907040dcb578d311c329ce", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "28", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137842068", v: "1c61", r: "5e00417a963a27fd2478cd70efdf75390875c8a0f18ff1c8cf14be75439a4a22", s: "435a4794c4eafcb633b98d254a3da0746192aa36daf4e52781adb34cb7427f30" }, hash: "0x72362ea00b8ea3be471e03529de92cc594e89e21ff907040dcb578d311c329ce", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137848089", status: 0, gasUsed: "2181" }, 0x50e956c5d644e8017f5c4b58fe4a61dd95c222361da3dcc8a06516aac9a09d5e: { data: { blockHash: "0x00c79795363be9d51a2edcd5d09d3129ed597744e7e10c4f98847b8079daa87a", blockNumber: "38", transactionIndex: 0, hash: "0x50e956c5d644e8017f5c4b58fe4a61dd95c222361da3dcc8a06516aac9a09d5e", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "37", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137918147", v: "", r: "", s: "" }, hash: "0x50e956c5d644e8017f5c4b58fe4a61dd95c222361da3dcc8a06516aac9a09d5e", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137919215", status: 1, gasUsed: "8333" }, 0x5e413a2d19b593f8455e747d09b69e38a44712f9166cfe282bdbbdb5a0098e92: { data: { blockHash: "0x00c79795363be9d51a2edcd5d09d3129ed597744e7e10c4f98847b8079daa87a", blockNumber: "38", transactionIndex: 1, hash: "0x5e413a2d19b593f8455e747d09b69e38a44712f9166cfe282bdbbdb5a0098e92", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "29", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137917291", v: "1c62", r: "982519f609bc4de9ae4bb6dbf9cc5356683754bc2aeba3412e8df19f6b7504cc", s: "71397958a733be7b4e579a3ebd4b1f16cb646c246baa970d3266d08912ab1603" }, hash: "0x5e413a2d19b593f8455e747d09b69e38a44712f9166cfe282bdbbdb5a0098e92", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137919215", status: 0, gasUsed: "1055" }, 0x409351aba8e15d21ce4bc113f5c0510cb799b4842be3a95d6fa75cc27274c848: { data: { blockHash: "0x00a5825c4712f8212adcd27de1763dd8c9f2d07d59610c9503adf13e1ea3b229", blockNumber: "39", transactionIndex: 0, hash: "0x409351aba8e15d21ce4bc113f5c0510cb799b4842be3a95d6fa75cc27274c848", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "38", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702137939226", v: "", r: "", s: "" }, hash: "0x409351aba8e15d21ce4bc113f5c0510cb799b4842be3a95d6fa75cc27274c848", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137939988", status: 1, gasUsed: "642" }, 0xc4d96a5b29be167f24d2d28ee4dc3791e81eb839a0ba92ab99d1392b595ed541: { data: { blockHash: "0x00a5825c4712f8212adcd27de1763dd8c9f2d07d59610c9503adf13e1ea3b229", blockNumber: "39", transactionIndex: 1, hash: "0xc4d96a5b29be167f24d2d28ee4dc3791e81eb839a0ba92ab99d1392b595ed541", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "30", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702137936800", v: "1c62", r: "ac8ae8bd23cd288508621ca45e6eaf4de3998f9ca0812d96a356005446b88ff", s: "f38bfd469eac5104e74a9f6da955357de1a68d1b80356855d2b6f950f5e27d" }, hash: "0xc4d96a5b29be167f24d2d28ee4dc3791e81eb839a0ba92ab99d1392b595ed541", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702137939988", status: 0, gasUsed: "1085" }, 0x28fb3bc93c30e752582ca9c7fac1ee633739dc46014dea5ef338fc2f3edee979: { data: { blockHash: "0x00942bbd416f946abf34f7ae6c1aab02f4a72060eef9918c816ec2852c0196bd", blockNumber: "40", transactionIndex: 0, hash: "0x28fb3bc93c30e752582ca9c7fac1ee633739dc46014dea5ef338fc2f3edee979", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "39", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702138822530", v: "", r: "", s: "" }, hash: "0x28fb3bc93c30e752582ca9c7fac1ee633739dc46014dea5ef338fc2f3edee979", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138823536", status: 1, gasUsed: "1585" }, 0x335f64d35e5c9b5879618554417d24c76622c5c22f37a4bfff710f2810af9352: { data: { blockHash: "0x00942bbd416f946abf34f7ae6c1aab02f4a72060eef9918c816ec2852c0196bd", blockNumber: "40", transactionIndex: 1, hash: "0x335f64d35e5c9b5879618554417d24c76622c5c22f37a4bfff710f2810af9352", from: "0xff7cee092641a91e3a105bf0df0748bc11216973", gasLimit: "457925", gasPrice: "1000000000", data: "0x", nonce: "0", to: "0x0da81b5da0e5b6a4615d3499d31bb34f0e35a74e", value: "1000000000000000000", chainId: "3615", origin: "1702138818813", v: "1c61", r: "b659ea068b91f6f9d34b7ed2efbe04ecc3c31521ed48315858496c0f716e6a02", s: "3127a9f3f46a0077063bacd6141eea5af79adf8b5186c59e07e79c71950c4158" }, hash: "0x335f64d35e5c9b5879618554417d24c76622c5c22f37a4bfff710f2810af9352", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138823536", status: 1, gasUsed: "14175" }, 0x4fc374812e184d2b31bb616153c2fb177ee54f18d13c82bf2168dfdb15cfe051: { data: { blockHash: "0x007feb1b0beff5c9b2c13c1cac1f97cbfb7fb0c563b6c1027a1a8a67aed26972", blockNumber: "41", transactionIndex: 0, hash: "0x4fc374812e184d2b31bb616153c2fb177ee54f18d13c82bf2168dfdb15cfe051", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "40", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702138878566", v: "", r: "", s: "" }, hash: "0x4fc374812e184d2b31bb616153c2fb177ee54f18d13c82bf2168dfdb15cfe051", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138882707", status: 1, gasUsed: "5164" }, 0x9875bfede4e11ecf7234698a14ed497864c10f9ba1baf77b045fbca420c1bb00: { data: { blockHash: "0x007feb1b0beff5c9b2c13c1cac1f97cbfb7fb0c563b6c1027a1a8a67aed26972", blockNumber: "41", transactionIndex: 1, hash: "0x9875bfede4e11ecf7234698a14ed497864c10f9ba1baf77b045fbca420c1bb00", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "31", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702138878201", v: "1c62", r: "e67a89c4d859f94618ceece78d875fe209af2b5c6a8c8f26b36c9cad0701a9ab", s: "8d62cdf554cab1298d6ad8ac2232984bc0c3d4dc88e92a78791ebbe808de1e5" }, hash: "0x9875bfede4e11ecf7234698a14ed497864c10f9ba1baf77b045fbca420c1bb00", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138882707", status: 0, gasUsed: "8917" }, 0x1621eee19380ab670cb4301345344031738e04555ca7aa7ef474054b3a136f1f: { data: { blockHash: "0x00155f75cd57c99929a90803a293ae760e23946288afa442791de97e900c7aa2", blockNumber: "42", transactionIndex: 0, hash: "0x1621eee19380ab670cb4301345344031738e04555ca7aa7ef474054b3a136f1f", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "41", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702138887709", v: "", r: "", s: "" }, hash: "0x1621eee19380ab670cb4301345344031738e04555ca7aa7ef474054b3a136f1f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138894728", status: 1, gasUsed: "2379" }, 0x21a481eab803eba639e6dfae09491919c5cc637ae0d97c63a3c557a7fa316ded: { data: { blockHash: "0x00155f75cd57c99929a90803a293ae760e23946288afa442791de97e900c7aa2", blockNumber: "42", transactionIndex: 1, hash: "0x21a481eab803eba639e6dfae09491919c5cc637ae0d97c63a3c557a7fa316ded", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "32", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702138886315", v: "1c61", r: "8b7ff3c664f6aad558404f0d3899f69d4d4491e2f2f3a4de3d877b0fd61a5e8d", s: "313a3b0026c6b70bca114abe58e947202d9b8856d732b11d6a192d0ccd57f014" }, hash: "0x21a481eab803eba639e6dfae09491919c5cc637ae0d97c63a3c557a7fa316ded", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138894728", status: 0, gasUsed: "4717" }, 0xee1a32f18c864b23ee880c38ab0b483806167f185b121a8ff0da72e30facda10: { data: { blockHash: "0x00610d94f46b3ceb99c9846344a5405319b4a3347f900aaeee784787bb7e7f75", blockNumber: "43", transactionIndex: 0, hash: "0xee1a32f18c864b23ee880c38ab0b483806167f185b121a8ff0da72e30facda10", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "42", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702138899729", v: "", r: "", s: "" }, hash: "0xee1a32f18c864b23ee880c38ab0b483806167f185b121a8ff0da72e30facda10", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138900939", status: 1, gasUsed: "2797" }, 0x63a85145c4535cd1cb3b86206215a855e6d3102d03e87608e04019a635b84e96: { data: { blockHash: "0x00610d94f46b3ceb99c9846344a5405319b4a3347f900aaeee784787bb7e7f75", blockNumber: "43", transactionIndex: 1, hash: "0x63a85145c4535cd1cb3b86206215a855e6d3102d03e87608e04019a635b84e96", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "33", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702138897501", v: "1c61", r: "54974378d4c8428aa98c98c8bee4d688d4e6b9de3d758e1c32b5fb854acf9b1d", s: "33aff75c83bdbd6cec133aee9c7db6fda57fefe2dd341a4dc6c21ab1f05a24c2" }, hash: "0x63a85145c4535cd1cb3b86206215a855e6d3102d03e87608e04019a635b84e96", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702138900939", status: 0, gasUsed: "9530" }, 0x8dcde4817f3e75a457608b1721ee358c10b0f72adb33d119ce6ce1620a527149: { data: { blockHash: "0x00efede3eeb6c067bee5f66812a9ceb85e16fe1120945df8f06927c01c3a4a7d", blockNumber: "44", transactionIndex: 0, hash: "0x8dcde4817f3e75a457608b1721ee358c10b0f72adb33d119ce6ce1620a527149", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "43", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1702149311240", v: "", r: "", s: "" }, hash: "0x8dcde4817f3e75a457608b1721ee358c10b0f72adb33d119ce6ce1620a527149", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702149317044", status: 1, gasUsed: "1620" }, 0x515a83ca0d5a12c7f1875c48446d1f1d8eb82a42781c153072d29be86fee41d0: { data: { blockHash: "0x00efede3eeb6c067bee5f66812a9ceb85e16fe1120945df8f06927c01c3a4a7d", blockNumber: "44", transactionIndex: 1, hash: "0x515a83ca0d5a12c7f1875c48446d1f1d8eb82a42781c153072d29be86fee41d0", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000ff7cee092641a91e3a105bf0df0748bc11216973", nonce: "34", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1702149308158", v: "1c61", r: "5a1df60ceeb16afc9356813765d416a325d989923f94027277c9f6271fb6e1cc", s: "677748f9f8c415cb60323de81d94680c685864b90762523bdb62f1d5711ba99" }, hash: "0x515a83ca0d5a12c7f1875c48446d1f1d8eb82a42781c153072d29be86fee41d0", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1702149317044", status: 0, gasUsed: "3968" }, 0xbca7f856124df08bf861a13a966883f2dab746612aaca6aea0695c75101f8601: { data: { blockHash: "0x0068644374aa763da8626cb7d8da7029a35e4e76d008d4c18ebd0eed694ba844", blockNumber: "45", transactionIndex: 0, hash: "0xbca7f856124df08bf861a13a966883f2dab746612aaca6aea0695c75101f8601", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "44", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821792217", v: "", r: "", s: "" }, hash: "0xbca7f856124df08bf861a13a966883f2dab746612aaca6aea0695c75101f8601", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821795071", status: 1, gasUsed: "1777" }, 0x7c5e6c6cc0e0dcd1f9762f541913543cd006d5c3f97cba2d97f53959819053f4: { data: { blockHash: "0x0068644374aa763da8626cb7d8da7029a35e4e76d008d4c18ebd0eed694ba844", blockNumber: "45", transactionIndex: 1, hash: "0x7c5e6c6cc0e0dcd1f9762f541913543cd006d5c3f97cba2d97f53959819053f4", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "35", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821789274", v: "1c62", r: "b791155d2cf6c765b249ec4b243dd3ed24b9f10234be1c757571af00c3279859", s: "4d2047de2c77bdbe30793f6f57b2799039f3e8ff530f784173562b2e6c7944a3" }, hash: "0x7c5e6c6cc0e0dcd1f9762f541913543cd006d5c3f97cba2d97f53959819053f4", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821795071", status: 0, gasUsed: "2645" }, 0x37171e0eb861a0618ea855011041129345199a390ccd368343cba4ef8b0897b4: { data: { blockHash: "0x00a9cc29f57007beffafcc64a8fe2322b61d5b32069bd1a296f916b17ae74401", blockNumber: "46", transactionIndex: 0, hash: "0x37171e0eb861a0618ea855011041129345199a390ccd368343cba4ef8b0897b4", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "45", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821800073", v: "", r: "", s: "" }, hash: "0x37171e0eb861a0618ea855011041129345199a390ccd368343cba4ef8b0897b4", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821804879", status: 1, gasUsed: "1316" }, 0xded491ff92f07cb14728354689cb16b1e5f3bb9b338776c1e96dd6bff9215e51: { data: { blockHash: "0x00a9cc29f57007beffafcc64a8fe2322b61d5b32069bd1a296f916b17ae74401", blockNumber: "46", transactionIndex: 1, hash: "0xded491ff92f07cb14728354689cb16b1e5f3bb9b338776c1e96dd6bff9215e51", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "36", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821797574", v: "1c61", r: "c27bca8e98172e8d23ac1ec002a64796abd8c130b72c7a4ffd970a0cda53aec2", s: "58c03d8b5fb14e9dc4365af3a79a939a1a63250cb315956f8852316f0581b2d4" }, hash: "0xded491ff92f07cb14728354689cb16b1e5f3bb9b338776c1e96dd6bff9215e51", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821804879", status: 0, gasUsed: "3709" }, 0xb10d66aac9cb0f5b52fc066c8d7cd82b1481d9bd1872e1b486b996dd109214a5: { data: { blockHash: "0x00defbce09fc00e2cd955476b6a650a4a149457fbeb430eebdeb405aeba91acd", blockNumber: "47", transactionIndex: 0, hash: "0xb10d66aac9cb0f5b52fc066c8d7cd82b1481d9bd1872e1b486b996dd109214a5", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "46", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821809881", v: "", r: "", s: "" }, hash: "0xb10d66aac9cb0f5b52fc066c8d7cd82b1481d9bd1872e1b486b996dd109214a5", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821810754", status: 1, gasUsed: "2918" }, 0xacbccaadfc4e8ee297241ee3d53c50b6cca88acf446cf73223560b70b794669f: { data: { blockHash: "0x00defbce09fc00e2cd955476b6a650a4a149457fbeb430eebdeb405aeba91acd", blockNumber: "47", transactionIndex: 1, hash: "0xacbccaadfc4e8ee297241ee3d53c50b6cca88acf446cf73223560b70b794669f", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "37", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821805838", v: "1c62", r: "6be8748707dcaae60001a90ac04d768f03d70f47230e797d662732e598cec835", s: "2d407440f499dfe306c79fa963777807662317962bb23b068b6d7509fd39ff88" }, hash: "0xacbccaadfc4e8ee297241ee3d53c50b6cca88acf446cf73223560b70b794669f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821810754", status: 0, gasUsed: "1208" }, 0xb9e9938da0cda840664916d4e1f21aaea93ce71aba61751434e5df5f4c958381: { data: { blockHash: "0x00ff93d3baa9957d2625848fc15f3c1ee79edc4b88f86570bd0865aebf6814af", blockNumber: "48", transactionIndex: 0, hash: "0xb9e9938da0cda840664916d4e1f21aaea93ce71aba61751434e5df5f4c958381", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "47", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821815755", v: "", r: "", s: "" }, hash: "0xb9e9938da0cda840664916d4e1f21aaea93ce71aba61751434e5df5f4c958381", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821817042", status: 1, gasUsed: "2846" }, 0xdf8a85a40ae16d2ff22ae153a108e9c89cdc54bfb5ae0d9623a5540bc160b1e0: { data: { blockHash: "0x00ff93d3baa9957d2625848fc15f3c1ee79edc4b88f86570bd0865aebf6814af", blockNumber: "48", transactionIndex: 1, hash: "0xdf8a85a40ae16d2ff22ae153a108e9c89cdc54bfb5ae0d9623a5540bc160b1e0", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "38", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821814122", v: "1c62", r: "1ba46d83642b13c847bb62dfed2236689f5cdc15e351dd4176be2c8120d6d884", s: "4c7645b264f7a07262102d65f95a173de5c2f4692a4d37c01c3b8a0b26b81963" }, hash: "0xdf8a85a40ae16d2ff22ae153a108e9c89cdc54bfb5ae0d9623a5540bc160b1e0", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821817042", status: 0, gasUsed: "2141" }, 0x19312c73e009168e5aba240693a46fc8d708d9c85d2b1f17ce9b4964dc2c92ec: { data: { blockHash: "0x00dccc0e8c7319b1897b12de619d4d73909b2dbc0ac91456807ad7dd722704eb", blockNumber: "49", transactionIndex: 0, hash: "0x19312c73e009168e5aba240693a46fc8d708d9c85d2b1f17ce9b4964dc2c92ec", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "48", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821822045", v: "", r: "", s: "" }, hash: "0x19312c73e009168e5aba240693a46fc8d708d9c85d2b1f17ce9b4964dc2c92ec", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821823374", status: 1, gasUsed: "9610" }, 0x433de861a571f823a73baf3c8f0e2ed8b26d3b36df57a9206b958e8788ff3c54: { data: { blockHash: "0x00dccc0e8c7319b1897b12de619d4d73909b2dbc0ac91456807ad7dd722704eb", blockNumber: "49", transactionIndex: 1, hash: "0x433de861a571f823a73baf3c8f0e2ed8b26d3b36df57a9206b958e8788ff3c54", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "39", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821818382", v: "1c61", r: "6e7828fbe247518c01a9eedb6bf65dfb3341508b028ddd2fd40cae61f947c63b", s: "6e6d2d51aa847425c77939baf33ffb61a86f3e5d18b8f55fd47201fc4b199b62" }, hash: "0x433de861a571f823a73baf3c8f0e2ed8b26d3b36df57a9206b958e8788ff3c54", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821823374", status: 0, gasUsed: "8028" }, 0xa2773de16cb41a33ea4a03df59a67ea9a9efc871c694b2ea80ae666974059e1f: { data: { blockHash: "0x00c08d7840d93b44269c79b9ceab2012671e75e63365f8d4f6b08961562528ad", blockNumber: "50", transactionIndex: 0, hash: "0xa2773de16cb41a33ea4a03df59a67ea9a9efc871c694b2ea80ae666974059e1f", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "49", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1704821828375", v: "", r: "", s: "" }, hash: "0xa2773de16cb41a33ea4a03df59a67ea9a9efc871c694b2ea80ae666974059e1f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821832978", status: 1, gasUsed: "4133" }, 0x333540a1c860d3a4a7293a1379658d2e98fa7fc7c20308db62c8b42dfe851334: { data: { blockHash: "0x00c08d7840d93b44269c79b9ceab2012671e75e63365f8d4f6b08961562528ad", blockNumber: "50", transactionIndex: 1, hash: "0x333540a1c860d3a4a7293a1379658d2e98fa7fc7c20308db62c8b42dfe851334", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f90000000000000000000000001f3cb43d20f18f467a8a2be65ea69a5ac87d14be", nonce: "40", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1704821826626", v: "1c62", r: "8e9f40c58aaf86ebf0184d30bb4f541e94869687ec93dd3728266e072f8bb685", s: "68339a377174bffd0a3de5bf03084635efba13d7f102acb853b219d859df6c77" }, hash: "0x333540a1c860d3a4a7293a1379658d2e98fa7fc7c20308db62c8b42dfe851334", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1704821832978", status: 0, gasUsed: "6354" }, 0x453116693a6f1962ed295942db3ed0a62d62bdf1b5a3ee68adafb1ec747a08c1: { data: { blockHash: "0x0062e79229a78e3061f1e300ec47ba9d93468f72e79787288f3a2a6a35d88056", blockNumber: "51", transactionIndex: 0, hash: "0x453116693a6f1962ed295942db3ed0a62d62bdf1b5a3ee68adafb1ec747a08c1", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "50", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1720875217431", v: "", r: "", s: "" }, hash: "0x453116693a6f1962ed295942db3ed0a62d62bdf1b5a3ee68adafb1ec747a08c1", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875223408", status: 1, gasUsed: "11220" }, 0x56aa930a78d7b4dd3ddeea9b692e825100fd4a5076b82131f79d7f8bbf4a990e: { data: { blockHash: "0x0062e79229a78e3061f1e300ec47ba9d93468f72e79787288f3a2a6a35d88056", blockNumber: "51", transactionIndex: 1, hash: "0x56aa930a78d7b4dd3ddeea9b692e825100fd4a5076b82131f79d7f8bbf4a990e", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f900000000000000000000000074f79e07013553b8b111b3a0645cc4b46389ca82", nonce: "41", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1720875215734", v: "1c61", r: "76c5ad4b56fdf8040224d8f7305639699b6ede1807f0afe4b4ff85dcda9dcfa2", s: "46f9d5fa27eed74ba36a8fbb9395af788c74939ce46d53894b045319ba0834e3" }, hash: "0x56aa930a78d7b4dd3ddeea9b692e825100fd4a5076b82131f79d7f8bbf4a990e", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875223408", status: 0, gasUsed: "2154" }, 0x1255e6263f5b245aa0b1450b253c2d98c9d0ba358f75fb09f897dc82181441fa: { data: { blockHash: "0x0082aecf4ac3e72c58f2fe3723577a59322d8b90296c42bd35386c39f774e10c", blockNumber: "52", transactionIndex: 0, hash: "0x1255e6263f5b245aa0b1450b253c2d98c9d0ba358f75fb09f897dc82181441fa", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "51", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1720875253416", v: "", r: "", s: "" }, hash: "0x1255e6263f5b245aa0b1450b253c2d98c9d0ba358f75fb09f897dc82181441fa", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875260360", status: 1, gasUsed: "7893" }, 0x0d1568e67db0f458336483f6fb6be4e6085dc07c94390d1614d06132bbbfb381: { data: { blockHash: "0x0082aecf4ac3e72c58f2fe3723577a59322d8b90296c42bd35386c39f774e10c", blockNumber: "52", transactionIndex: 1, hash: "0x0d1568e67db0f458336483f6fb6be4e6085dc07c94390d1614d06132bbbfb381", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f900000000000000000000000074f79e07013553b8b111b3a0645cc4b46389ca82", nonce: "42", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1720875249570", v: "1c62", r: "1ec59e55d09f4be84ddf08840342b3ea3ce2d55559b503b4bf07a248e65c887d", s: "74b6af1a38a515ca76853964bc74517ddee7175e651c8fcc36d1036b08a28f36" }, hash: "0x0d1568e67db0f458336483f6fb6be4e6085dc07c94390d1614d06132bbbfb381", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875260360", status: 0, gasUsed: "2938" }, 0x92fdbe50dc544a193479fb367c4301c9940044ddbc46bbad6b4a2ffe38358f20: { data: { blockHash: "0x00e533277943cfad3d3b7dde4f9e00e009eba9a8e7a38bbea85c57d61bae2934", blockNumber: "53", transactionIndex: 0, hash: "0x92fdbe50dc544a193479fb367c4301c9940044ddbc46bbad6b4a2ffe38358f20", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "52", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1720875280370", v: "", r: "", s: "" }, hash: "0x92fdbe50dc544a193479fb367c4301c9940044ddbc46bbad6b4a2ffe38358f20", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875291372", status: 1, gasUsed: "6571" }, 0xb1dfea4931e97dcbc74d04df5fcb99565afc904719197134ddb07843f1932998: { data: { blockHash: "0x00e533277943cfad3d3b7dde4f9e00e009eba9a8e7a38bbea85c57d61bae2934", blockNumber: "53", transactionIndex: 1, hash: "0xb1dfea4931e97dcbc74d04df5fcb99565afc904719197134ddb07843f1932998", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f900000000000000000000000074f79e07013553b8b111b3a0645cc4b46389ca82", nonce: "43", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1720875279533", v: "1c61", r: "d8bf8944e3283cd9efe3fb057cb86a8680e3a5e536f9867f8679d42a9f9dfd7e", s: "7439c93a38f7e5712eaa4451d17a8358de1869d766fd5ba3579c876fa7955028" }, hash: "0xb1dfea4931e97dcbc74d04df5fcb99565afc904719197134ddb07843f1932998", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1720875291372", status: 0, gasUsed: "2802" }, 0xd1b76201c6a544c0c36b34b3877db8e574b418696308f1c8557ce430d75cbe38: { data: { blockHash: "0x001bfb3e0d14ef264819249e476628b8b7063ae1615ac452413be50a92e78700", blockNumber: "54", transactionIndex: 0, hash: "0xd1b76201c6a544c0c36b34b3877db8e574b418696308f1c8557ce430d75cbe38", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "53", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1724298449838", v: "", r: "", s: "" }, hash: "0xd1b76201c6a544c0c36b34b3877db8e574b418696308f1c8557ce430d75cbe38", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1724298460542", status: 1, gasUsed: "11122" }, 0x79bdb61267021b32912944437760757c5a31e92b30a9142222bce04bd8a00fbc: { data: { blockHash: "0x001bfb3e0d14ef264819249e476628b8b7063ae1615ac452413be50a92e78700", blockNumber: "54", transactionIndex: 1, hash: "0x79bdb61267021b32912944437760757c5a31e92b30a9142222bce04bd8a00fbc", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000e37f7f523e8b72fb06c4d8ccf9631a74a8fcd414", nonce: "44", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1724298444919", v: "1c62", r: "1ff1f39908bb1e122b6b782fc99a842904e9ae5fd4e62643fc402b676c01f7b4", s: "7fae433e9d6f2e9595f0b2e446c187660faea10ed6ca8cf0e30cb98cd3b914bd" }, hash: "0x79bdb61267021b32912944437760757c5a31e92b30a9142222bce04bd8a00fbc", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1724298460542", status: 0, gasUsed: "2163" }, 0xa98b83e5bf2731669fe4b01d51a3085a0cdb151ddb7d7a2006776aef4840547f: { data: { blockHash: "0x00a6c1108bb8c09cf2419ff183ed6e1187661ab729166f3447f9fcc5f618b48b", blockNumber: "55", transactionIndex: 0, hash: "0xa98b83e5bf2731669fe4b01d51a3085a0cdb151ddb7d7a2006776aef4840547f", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "54", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1724298475544", v: "", r: "", s: "" }, hash: "0xa98b83e5bf2731669fe4b01d51a3085a0cdb151ddb7d7a2006776aef4840547f", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1724298476709", status: 1, gasUsed: "14704" }, 0x9f70ab2b8b65c8c2ee0fbe6544a649a594b7e290cf8bf58f5ac5d992283bca5e: { data: { blockHash: "0x00a6c1108bb8c09cf2419ff183ed6e1187661ab729166f3447f9fcc5f618b48b", blockNumber: "55", transactionIndex: 1, hash: "0x9f70ab2b8b65c8c2ee0fbe6544a649a594b7e290cf8bf58f5ac5d992283bca5e", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000e37f7f523e8b72fb06c4d8ccf9631a74a8fcd414", nonce: "45", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1724298474127", v: "1c61", r: "6d6237e7ea5a6818035430fbb8867bdf1080def421b3a9d6ff6bbd24d673f7d1", s: "27d98933339e8348b231028177e96d22118ca4382dd33af43aa99d21362db464" }, hash: "0x9f70ab2b8b65c8c2ee0fbe6544a649a594b7e290cf8bf58f5ac5d992283bca5e", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1724298476709", status: 0, gasUsed: "2866" }, 0x3ef76a68950d7c0251462879115f4e7c44341751239bbab737818005c129b204: { data: { blockHash: "0x00ad07432529e25c388b446a05d4b52d8cab0e6bacfd954d9cd2334e463e4cb3", blockNumber: "56", transactionIndex: 0, hash: "0x3ef76a68950d7c0251462879115f4e7c44341751239bbab737818005c129b204", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "55", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1727863123092", v: "", r: "", s: "" }, hash: "0x3ef76a68950d7c0251462879115f4e7c44341751239bbab737818005c129b204", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1727863127368", status: 1, gasUsed: "20935" }, 0x021f05bed67059020def15d6a018bce0a11ef03cb428f47cb6966b9e7e87af87: { data: { blockHash: "0x00ad07432529e25c388b446a05d4b52d8cab0e6bacfd954d9cd2334e463e4cb3", blockNumber: "56", transactionIndex: 1, hash: "0x021f05bed67059020def15d6a018bce0a11ef03cb428f47cb6966b9e7e87af87", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f9000000000000000000000000e37f7f523e8b72fb06c4d8ccf9631a74a8fcd414", nonce: "46", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1727863120371", v: "1c61", r: "5629980ac165476175a8c090ddeb467b9fc184fff8d56bbb755ae234db20a4f0", s: "7c2cdb94111471c82b651dafd7a6c91100938e6bb1fe558e1154ace3f7cc85c9" }, hash: "0x021f05bed67059020def15d6a018bce0a11ef03cb428f47cb6966b9e7e87af87", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1727863127368", status: 0, gasUsed: "2054" }, 0x18c8ad125f2f3f8977083b5147c3aa1d29b179bca46fd891c2afd5eaa1ee4958: { data: { blockHash: "0x00e6f175cd7c14b48c9635b7dbcd753f858180c0bdc5874dc5a4ca6f0e1627de", blockNumber: "57", transactionIndex: 0, hash: "0x18c8ad125f2f3f8977083b5147c3aa1d29b179bca46fd891c2afd5eaa1ee4958", from: "0x0000000000000000000000000000000000000001", gasLimit: "1000000000000000000", gasPrice: "0", data: "0x", nonce: "56", to: "0xccf8ba457dcad7ee6a0361c96846a0f79744b113", value: "250000000000000000000", chainId: "3615", origin: "1729335876068", v: "", r: "", s: "" }, hash: "0x18c8ad125f2f3f8977083b5147c3aa1d29b179bca46fd891c2afd5eaa1ee4958", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1729335876427", status: 1, gasUsed: "13508" }, 0x8c10f2618c30e1eff594ee0cb80605fa9dc9b701b124c2e670db08add42ef965: { data: { blockHash: "0x00e6f175cd7c14b48c9635b7dbcd753f858180c0bdc5874dc5a4ca6f0e1627de", blockNumber: "57", transactionIndex: 1, hash: "0x8c10f2618c30e1eff594ee0cb80605fa9dc9b701b124c2e670db08add42ef965", from: "0x131a190fa22cb867abf58193bf9e7640e9fce682", gasLimit: "25000", gasPrice: "1000000000", data: "0x418083f900000000000000000000000074f79e07013553b8b111b3a0645cc4b46389ca82", nonce: "47", to: "0xa28745ed4502f9c9eea2f3f474178d43e36347d8", value: "0", chainId: "3615", origin: "1729335874077", v: "1c62", r: "3a33e6885ba5b682e6f0de96f2108db98a391cc1a009a7be7707f670cae267f8", s: "675bc6b025e7ec2ee5f0e2baca80407f7b2fe84f87dc9bbfb981cf30a2dee77b" }, hash: "0x8c10f2618c30e1eff594ee0cb80605fa9dc9b701b124c2e670db08add42ef965", load: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';

if (! fs.existsSync(transactionFile)) {
console.warn(utils.getDate(), "[BLOCKCHAIN]", 'IMPORT Transaction not found', this.hash);
throw new Error('IMPORT_TRANSACTION_NOT_FOUND_DATABASE_CORRUPTED');
}

const transactionJson = fs.readFileSync(transactionFile);
transactionData = JSON.parse(transactionJson, utils.bnJsonParser);

this.data = (typeof transactionData == 'TransactionData') ? transactionData : new TransactionData(transactionData);

return true;
}"
, save: "() => {
const transactionFile = config.node.dataDir + '/transactions/' + this.hash + '.json';
fs.writeFileSync(transactionFile, JSON.stringify(this.data));
return true;
}"
, getReceipt: "() => {
return blockchain.getTransactionReceiptByHash(this.hash);
}"
, getBlock: "() => {
return blockchain.getBlockByHash(this.data.blockHash);
}"
, getBlockTransationPrev: "() => {
const block = this.getBlock();
const txPrevIdx = (this.data.transactionIndex > 0) ? (this.data.transactionIndex-1) : null;
const tx = (block && txPrevIdx in block.transactions) ? block.transactions[txPrevIdx] : null;
return tx;
}"
, getBlockTransationNext: "() => {
const block = this.getBlock();
const txNextIdx = (this.data.transactionIndex < block.transactions.length - 1) ? (this.data.transactionIndex+1) : null;
const tx = (block && txNextIdx in block.transactions) ? block.transactions[txNextIdx] : null;
return tx;
}"
, getAccountTransactionIndex: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = account.data.transactions.findIndex(transactionHash => blockchain.getTransactionByHash(transactionHash).hash == this.hash);
if (txIdx === -1) {
return null;
}
return txIdx;
}"
, getAccountTransationPrev: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txPrevIdx = (txIdx > 0) ? (txIdx-1) : null;
const transactionHash = (txPrevIdx in account.data.transactions) ? account.data.transactions[txPrevIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);
}"
, getAccountTransationNext: "() => {
const account = blockchain.getAccount(this.data.from);
if (! account) {
return null;
}
const txIdx = this.getAccountTransactionIndex();
if (txIdx === null) {
return null;
}
const txNextIdx = (txIdx < account.data.transactions.length - 1) ? (txIdx+1) : null;
const transactionHash = (txNextIdx in account.data.transactions) ? account.data.transactions[txNextIdx] : null;
if (! transactionHash) {
return null;
}
return blockchain.getTransactionByHash(transactionHash);

}"
, formatRpc: "() => {
// https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/lib/index.d.ts

const to = (this.data.to && this.data.to.length == 42) ? this.data.to : null;

const _txData = {
blockHash: this.data.blockHash,
blockNumber: this.data.blockNumber.toNumber(),
transactionIndex: this.data.transactionIndex,
hash: this.data.hash,
from: this.data.from,
gasLimit: this.data.gasLimit.toHexString(),
gasPrice: this.data.gasPrice.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
to,
value: this.data.value.toHexString(),
chainId: this.data.chainId.toNumber(),
//origin: this.data.origin.toHexString(),
};

return _txData;
}"
, getHashOLD: "() => {
const _txData = {
gasPrice: this.data.gasPrice,
gasLimit: this.data.gasLimit,
from: this.data.from,
to: this.data.to,
value: this.data.value,
data: this.data.data,
nonce: this.data.nonce,
};

const strvalue = JSON.stringify(utils.orderedObject(_txData));
const transactionHash = ethers.utils.id(strvalue);

return transactionHash;
}"
, getHash: "() => {
const _txData = {
gasPrice: this.data.gasPrice.toHexString(),
gasLimit: this.data.gasLimit.toHexString(),
from: this.data.from,
to: this.data.to,
value: this.data.value.toHexString(),
data: this.data.data,
nonce: this.data.nonce.toHexString(),
v: this.data.v ? ('0x' + this.data.v) : '',
r: this.data.r ? ('0x' + this.data.r) : '',
s: this.data.s ? ('0x' + this.data.s) : '',
};

const tx = new EthereumJS.Transaction(_txData);
const hashRaw = tx.hash();

const transactionHash = ethers.utils.defaultAbiCoder.decode(["uint256"], hashRaw)[0].toHexString();

return transactionHash;
}"
, execute: "(block, transactionIndex, logIndexOffset) => {
const _tx = this;

function _rejectTransaction(reason) {
// https://openethereum.github.io/Transactions-Queue
reason = reason || 'unknown';
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] REJECTED PROCESSING TRANSACTION', _tx.data.hash, ' (Reason:', reason + ')');
const success = blockchain.removeTransactionsFromMempool([_tx.hash], true, true);
if (success) {
saveMempool();
}
throw new Error(reason); // on rejete definitivement la tx (elle ne sera pas minée ... et donc aucun frais de gas ne sera compté)
// et du coup on rejete le block
}

function _checkGas(step) {
execDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
gasUsed = toBn(Math.round(execDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
txCost = gasUsed.mul(_tx.data.gasPrice);

if (step == 'afterTransfer') {
//debugger;
}

if (status) {
gasUsedEffective = gasUsed;
txCostEffective = gasUsedEffective.mul(_tx.data.gasPrice);

if (gasUsed.gt(_tx.data.gasLimit)) {
status = 0;
error = new Error('INSUFFICIENT_GAS');

gasUsedEffective = _tx.data.gasLimit;
txCostEffective = txCostLimit;

// emit event
const debugError = {
message: 'INSUFFICIENT_GAS',
step,
gasPrice: _tx.data.gasPrice,
gasLimit: _tx.data.gasLimit,
gasUsed,
gasUsedEffective,
txCostLimit,
txCost,
txCostEffective,
execDuration,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, _tx.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

}
}


//const block = this;
const txLogs = [];
let vmStorage = {};

let gasUsed = toBn(0);
let gasUsedEffective = toBn(0);
let status = 1;
let error = null;
let createdContractAddress = null;
let amountTransfered = toBn(0);

const contractAddress = this.data.to;
let storageBackup;
let balanceBackup;
let contractExecuted = false;
let contractDeployed = false;

const execTsStart = performance.now()/1000; //process.hrtime(); //Date.now();
let execDuration = 0;

const txCostLimit = this.data.gasLimit.mul(this.data.gasPrice);
let txCost = toBn(0);
let txCostEffective = toBn(0);


const accountFrom = blockchain.getAccount(this.data.from);
if (! accountFrom) {
_rejectTransaction('TRANSACTION_REJECTED_EMPTY_ACCOUNT');
}
const accountFromBalance = accountFrom.data.balance;


if (accountFrom.address != mintAddress) {

// verif nonce
const expectedNonce = toBn(accountFrom.data.transactions.length); // le nonce attendu est egal au nombre de transactions passées de l'account
if (! this.data.nonce.eq(expectedNonce)) {
_rejectTransaction('TRANSACTION_REJECTED_INCORRECT_NONCE');
}

// verif balance pour gas + transfer
if (this.data.value && this.data.value.gt(0)) {
if (accountFromBalance.lt(this.data.value.add(txCostLimit))) {
// l'adresse n'a pas suffisamment de fond pour executer la transaction (selon gasLimit)
// TODO: faut-il rejeter la tx ? ou la laisser passer (en status=0) et lui vider sa balance (si 100% du gas est utilisé) ?
//return _rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE');

_rejectTransaction('TRANSACTION_REJECTED_INSUFFICIENT_ETH_BALANCE');
}
}
}



_checkGas('init');


if (status) {
if (this.data.value && this.data.value.gt(0) && contractAddress && contractAddress.length > 2) {
// Transfer la valeur demandée (simple ETH transaction)
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(this.data, block);
txLogs.push( ...transferLogs );

// si recipient == contract => on transfer quand meme dès maintenant => TODO: a revoir (le transferEth declenche un appel à receive ... avant meme de savoir si unne methode payable doit etre appelée. Probleme si receive provoque un revert => la fonction payable ne sera pas appelée)
// 1) si methode payable appellée, on l'execute
// 2) si methode non payable => on appelle receive juste avant
// 3) si methode inconnue => revert


if (transferResult && transferReverted) {
// reverted (after transfer)
amountTransfered = this.data.value;

status = 0;
error = new Error('TRANSFER_REVERTED');

} else if (transferResult) {
// success
amountTransfered = this.data.value;

} else {
// transfer failed
status = 0;
error = new Error('TRANSFER_FAILED');
}

}

_checkGas('afterTransfer');
}

const gasUsedBeforeContractExecution = gasUsed;

if (status && this.data.data && this.data.data != '0x') {
// Traitement du champ data de la transaction (smart contract call)

if (!contractAddress || contractAddress == '0x') {
// contract creation

contractDeployed = true;

let contractExecResult;
try {
contractExecResult = this.transactionDeployContract(block);
const creationLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (creationLogs.length) {
txLogs.push( ...creationLogs );
}

if (! contractExecResult.result) {
// Deploy Failed
status = 0;
error = new Error('CONTRACT_CREATION_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_CREATION_FAILED',
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);

} else {
// Deploy Success
createdContractAddress = contractExecResult.result.toLowerCase();

if (this.data.value.gt(0)) {
// Transfer tx value to contract
const _txData = {
...this.data,
to: createdContractAddress,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(_txData, block);
txLogs.push( ...transferLogs );

if (transferResult) {
amountTransfered = this.data.value;

} else {
status = 0;
error = new Error('CONTRACT_CREATION_TRANSFER_FAILED');
}
}

// TODO: emit event CONTRACT_CREATION_SUCCESS ?
}



} catch (e) {
//console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Cannot execute transaction');
//throw e;
status = 0;
error = new Error('CONTRACT_CREATION_ERROR');

console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] CONTRACT_CREATION_ERROR with message', e.message)

// emit event
const debugError = {
message: 'CONTRACT_CREATION_ERROR',
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

_checkGas('afterContractCreation');

} else {
// contract execution

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
status = 0;
error = new Error('CONTRACT_ACCOUNT_NOT_FOUND');

// emit event
const debugError = {
message: 'CONTRACT_ACCOUNT_NOT_FOUND',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

if (status) {
storageBackup = JSON.stringify(contractAccount.data.storage);
balanceBackup = contractAccount.data.balance;
contractExecuted = true;

try {
const contractExecResult = this.executeTransactionContract(block);
const internalLogs = contractExecResult.logs;
vmStorage = contractExecResult.storage;

if (internalLogs.length) {
txLogs.push( ...internalLogs );
}

if (! contractExecResult.result) {
status = 0;
error = new Error('CONTRACT_EXEC_FAILED');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_FAILED',
address: contractAddress,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}

} catch (e) {
status = 0;
error = new Error('CONTRACT_EXEC_ERROR');

// emit event
const debugError = {
message: 'CONTRACT_EXEC_ERROR',
address: contractAddress,
error: e.message,
};
const eventParams = [
JSON.stringify(debugError),
];
const errorLog = new TxLog(blockchain, null, this.data, 'Error', eventParams, errorAbi);
txLogs.push(errorLog);
}
}

_checkGas('afterContractCall');
}

}

const gasUsedAfterContractExecution = gasUsed;
const gasUsedInContractExecution = gasUsedAfterContractExecution.sub(gasUsedBeforeContractExecution);
const contractExecutionCost = gasUsedInContractExecution.mul(_tx.data.gasPrice);


console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] execDuration =', (Math.round(execDuration * 1000000) / 1000000), ' seconds');



if (txCostEffective.gt(0)) {
// Gas payment
//const minerAccount = blockchain.getAccount(block.data.miner, true);
//const gasPaymentResult = minerAccount.transferFrom(accountFrom.address, txCostEffective);

const txDataGasPayment = {
from: accountFrom.address,
to: block.data.miner,
value: txCostEffective,
};
const [gasPaymentResult, gasPaymentLogs, transferReverted] = blockchain.transferEth(txDataGasPayment, block, true);
txLogs.push(...gasPaymentLogs);

if (gasPaymentResult) {
// gas payment success

} else {
// gas payment failed => reject transaction (and block)
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] Gas payment failed');
_rejectTransaction('GAS_PAYMENT_FAILED');
}

if (false) {
// DEBUG
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
const gasEffectivePercent = gasUsedEffective.mul(100).div(this.data.gasLimit).toNumber();
const costPercent = txCost.mul(100).div(txCostLimit).toNumber();
const costEffectivePercent = txCostEffective.mul(100).div(txCostLimit).toNumber();
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostLimit =', ethers.utils.formatEther(txCostLimit));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] txCostEffective =', ethers.utils.formatEther(txCostEffective));
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] gas % =', Math.round(gasPercent), '% =', Math.round(gasEffectivePercent), '%');
console.log(utils.getDate(), "[BLOCKCHAIN]", '[DEBUG] cost % =', Math.round(costPercent), '%', Math.round(costEffectivePercent), '%');
}


}


if (accountFrom.address != mintAddress) {
const accountFromBalanceAtEnd = blockchain.getAccountBalance(accountFrom.address);

if (accountFromBalanceAtEnd.lt(0)){
_rejectTransaction('TRANSACTION_FAILED_INSUFFICIENT_ETH_BALANCE_AT_END');
}
}


if (! status) {
// Transaction Failed

// Revert transfer
if (amountTransfered.gt(0)) {
const txDataRevert = {
...this.data,
from: this.data.to,
to: this.data.from,
value: amountTransfered,
};
const [transferResult, transferLogs, transferReverted] = blockchain.transferEth(txDataRevert, block);

// TODO: gerer le revert des internal transfers
}

txLogs.forEach(log => {
log.removed = true;
});

// Revert contract storage
if (contractExecuted) {
const contractAccount = blockchain.getAccount(contractAddress);
contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
contractAccount.data.balance = balanceBackup;

} else if (amountTransfered) {
// on check aussi amountTransfered car le contrat peut avoir été exécuté via receive => TODO: process a revoir
const contractAccount = blockchain.getAccount(contractAddress);
if (contractAccount && contractAccount.isContract()) {
//contractAccount.data.storage = JSON.parse(storageBackup, utils.bnJsonParser);
//contractAccount.data.balance = balanceBackup;
// TODO
}

}

// Revert deployed contract
if (contractDeployed) {
// nothing to do ?
}

const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%', ' | Error: ', error.message);

const realExecDuration = performance.now()/1000 - execTsStart; //parseFloat(process.hrtime(execTsStart).join('.'));
const realGasUsed = toBn(Math.round(realExecDuration * cpuPricePerSecond)); // TODO: prendre en compte le storage utilisé dans le calcul du gasUsed
const realGasPercent = realGasUsed.mul(100).div(this.data.gasLimit).toNumber();
console.warn(utils.getDate(), "[BLOCKCHAIN]", '[WARNING] FAILED TRANSACTION ', this.data.hash, '| real gas usage percent =', realGasPercent, '%', ' | Error: ', error.message);

} else {
// Transaction Success

// save contract storage
Object.entries(vmStorage).forEach((entry, idx) => {
const [contractAddress, contractStorage] = entry;

const contractAccount = blockchain.getAccount(contractAddress, true);
contractAccount.data.storage = contractStorage;
contractAccount.save();
});

// show result (debug)
const gasPercent = gasUsed.mul(100).div(this.data.gasLimit).toNumber();;
console.log(utils.getDate(), "[BLOCKCHAIN]", 'SUCCECSS TRANSACTION ', this.data.hash, '| gas usage percent =', gasPercent, '%');
}


// total gas utilisé dans le block
block.data.gasUsed = block.data.gasUsed.add(gasUsed);


// assign block data on logs
txLogs.forEach((txLog, idx) => {
txLog.data.transactionIndex = transactionIndex;
txLog.data.logIndex = logIndexOffset + idx;
txLog.data.transactionHash = this.data.hash;
txLog.data.blockHash = block.hash;
txLog.data.blockNumber = block.data.number;
});


const receiptData = {
transactionHash: this.data.hash,
transactionIndex: transactionIndex,
blockNumber: block.data.number,
blockHash: block.hash,
//logsBloom: '0x0000',
contractAddress: createdContractAddress,
gasUsed,
cumulativeGasUsed: block.data.gasUsed,
effectiveGasPrice: this.data.gasPrice,
effectiveCost: this.data.gasPrice.mul(gasUsed),
logs: txLogs,
status,
}

const receipt = new Receipt(blockchain, receiptData);

return [receipt, txLogs];
}"
, executeTransactionContract: "(block) => {
let contractAddress = this.data.to;

const isNullRecipient = (! contractAddress || contractAddress.length <= 2);
if (isNullRecipient) {
throw new Error('CONTRACT_EXEC_ADDRESS_INVALID_BUG_1');
}

const contractAccount = blockchain.getAccount(contractAddress);

if (! contractAccount) {
throw new Error('CONTRACT_EXEC_ACCOUNT_NOT_FOUND');
}

// Execute existing contract method call
return blockchain.callContractMethodInVM(this.data, block);

}"
, transactionDeployContract: "(block) => {
let contractAddress = this.data.to;

let contractAccount;

if (contractAddress && contractAddress.length > 2) {
throw new Error('CONTRACT_DEPLOY_ADDRESS_INVALID_BUG_2');
}


// CONTRACT CREATION

if (! this.data.data || this.data.data.length <= 2) {
throw new Error('CONTRACT_DEPLOY_INVALID_DATA_EMPTY_CODE');
}


// build contract address
contractAddress = ethers.utils.getContractAddress(this.data).toLocaleLowerCase();

// parse contract source code & constructor parameters
const codes = ethers.utils.defaultAbiCoder.decode(["string", "string"], this.data.data);
if (! codes) {
throw new Error('CONTRACT_DEPLOY_CODE_PARSING_FAILED');
}

const sourceCode = codes[0];
const constructorParams = codes[1];
const constructorRawParams = ethers.utils.defaultAbiCoder.encode(["string"], [constructorParams]);
const codeHash = ethers.utils.id(sourceCode);
const codeAbi = null; // l'abi sera généré une fois que le contrat aura été instancié
const storageHash = ethers.utils.id('{}');

// create new account
const accountData = {
address: contractAddress,
code: sourceCode,
codeHash,
codeAbi,
storage: {},
storageHash,
};
contractAccount = blockchain.createAccount(contractAddress, accountData);


// instanciate contract + call constructor + recuperer storage
const txData = {
from: this.data.from,
to: contractAddress,
data: constructorRawParams,
value: this.data.value.toHexString(),
};
const callResult = blockchain.deployContractInVM(txData, block);
const creationLogs = callResult.logs;
const vmStorage = callResult.storage;


// emit event
const eventParams = [
contractAddress,
];
const _txData = {
...this.data,
to: blockchain.getNullAddress(),
}
const deployLog = new TxLog(blockchain, null, _txData, 'ContractCreated', eventParams, deployAbi);
creationLogs.push(deployLog);


return {
result: contractAddress,
logs: creationLogs,
storage: vmStorage,
};
}"
, timestamp: "1729335876427", status: 0, gasUsed: "1933" } }