UNPKG

@harmoniclabs/pluts-emulator

Version:

Cardano emulator for offchian testing

836 lines (835 loc) 37.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeEmulatorWithWalletUtxOs = exports.initializeEmulator = exports.Emulator = void 0; const plu_ts_1 = require("@harmoniclabs/plu-ts"); const buildooor_1 = require("@harmoniclabs/buildooor"); const queue_1 = require("./queue.js"); const helper_1 = require("./utils/helper.js"); class Emulator { /** * Create a new Emulator * @param initialUtxoSet Initial UTxOs to populate the ledger * @param genesisInfos Chain genesis information * @param protocolParameters Protocol parameters * @param debugLevel Debug level (0: no debug, 1: basic debug, 2: detailed debug) */ constructor(initialUtxoSet = [], genesisInfos = buildooor_1.defaultMainnetGenesisInfos, protocolParameters = plu_ts_1.defaultProtocolParameters, debugLevel = 0) { // TO CHECK: Is that how to handle the datum table? this.datumTable = new Map(); if (!(0, buildooor_1.isGenesisInfos)(genesisInfos)) genesisInfos = buildooor_1.defaultMainnetGenesisInfos; this.genesisInfos = (0, buildooor_1.normalizedGenesisInfos)(genesisInfos); if (!(0, plu_ts_1.isProtocolParameters)(protocolParameters)) protocolParameters = plu_ts_1.defaultProtocolParameters; this.protocolParameters = protocolParameters; this.txBuilder = new buildooor_1.TxBuilder(this.protocolParameters, this.genesisInfos); // Initialize the time and slot based on the genesis information this.time = this.genesisInfos.systemStartPosixMs; this.slot = this.genesisInfos.startSlotNo; this.blockHeight = 0; // Initialize the state maps this.utxos = new Map(); this.stakeAddresses = new Map(); this.addresses = new Map(); this.datumTable = new Map(); this.mempool = new queue_1.Queue(); this.debugLevel = debugLevel; this.lastBlock = { time: this.time, hight: this.blockHeight, slot: this.slot, slot_leader: "emulator", size: 0, tx_count: 0, fees: BigInt(0) }; for (const iutxo of initialUtxoSet) { this.addUtxoToLedger(new plu_ts_1.UTxO(iutxo)); } } /** Add a UTxO to the ledger * @param utxo UTxO to add * @returns void */ addUtxoToLedger(utxo) { const ref = utxo.utxoRef.toString(); if (!this.utxos.has(ref)) { this.utxos.set(ref, utxo); const addr = utxo.resolved.address.toString(); if (!this.addresses.has(addr)) this.addresses.set(addr, new Set()); this.addresses.get(addr).add(ref); } } /** Remove a UTxO from the ledger * @param utxoRef UTxO reference to remove * @returns void */ removeUtxoFromLedger(utxoRef) { const ref = (0, plu_ts_1.forceTxOutRefStr)(utxoRef); const addr = this.utxos.get(ref)?.resolved.address.toString(); if (typeof addr !== "string") return; this.utxos.delete(ref); const addrRefs = this.addresses.get(addr); addrRefs.delete(ref); if (addrRefs.size <= 0) this.addresses.delete(addr); } /** Pretty printers */ /** Pretty print a UTxO * @param utxo UTxO to pretty print * @param detailed Whether to show detailed information (default: false) * @returns Pretty printed string */ prettyPrintUtxo(utxo, detailed = false) { const ref = utxo.utxoRef.toString(); let address = utxo.resolved.address.toString(); // TOFIX: // if (!detailed) { // address = address.substring(0,10) + "..." + address.substring(address.length - 5); // } const lovelace = utxo.resolved.value.lovelaces.toString(); let output = `UTxO Ref: ${ref}\n`; output += `\tAddress: ${address}\n`; output += `\tLovelace: ${lovelace}\n`; const assets = utxo.resolved.value.map; if (assets) { output += `\tAssets:\n`; for (const asset of assets) { const policy = asset.policy.toString(); for (const token of asset.assets) { const tokenName = Buffer.from(token.name).toString(); const quantity = token.quantity.toString(); output += `\t\tPolicy: ${policy} Token: ${tokenName} Quantity: ${quantity}\n`; } } } // Add datum information if available if (utxo.resolved.datum) { output += `\tDatum: ${utxo.resolved.datum.toString().substring(0, 20)}...\n`; if (detailed && utxo.resolved.datum) { output += ` Datum Data: ${JSON.stringify(utxo.resolved.datum)}...\n`; } } // Add reference script information if available if (utxo.resolved.refScript) { output += `\tReference Script: ${utxo.resolved.refScript.toString().substring(0, 20)}...\n`; if (detailed && utxo.resolved.refScript) { output += ` Reference Script Data: ${JSON.stringify(utxo.resolved.refScript)}...\n`; } } return output; } /** Pretty print a set of UTxOs * @param utxos UTxOs to pretty print * @param detailed Whether to show detailed information (default: false) * @returns Pretty printed string */ prettyPrintUtxos(utxos, detailed = false) { let output = "UTxOs:\n"; for (const utxo of utxos.values()) { output += this.prettyPrintUtxo(utxo, detailed) + "\n"; } return output; } /** Pretty print the ledger state * @param detailed Whether to show detailed information (default: false) * @return Pretty printed string of the entire ledger state */ prettyPrintLedgerState(detailed = false) { let output = "=== Ledger State ===\n"; // Basic ledger information output += `Block Height: ${this.blockHeight}\n`; output += `Current Slot: ${this.slot}\n`; output += `Current Time: ${new Date(this.time).toISOString()}\n\n`; // UTxOs const utxosCount = this.utxos.size; output += `=== UTxOs (${utxosCount}) ===\n`; if (utxosCount > 0) { // Group UTxOs by address const utxosByAddress = new Map(); for (const utxo of this.utxos.values()) { const addressStr = utxo.resolved.address.toString(); if (!utxosByAddress.has(addressStr)) { utxosByAddress.set(addressStr, []); } utxosByAddress.get(addressStr).push(utxo); } // Print UTxOs grouped by address for (const [address, addressUtxos] of utxosByAddress.entries()) { output += `Address: ${address}\n`; output += ` UTxOs: ${addressUtxos.length}\n`; // Total balance for the address const totalBalance = addressUtxos.reduce((sum, utxo) => sum + utxo.resolved.value.lovelaces, 0n); output += ` Total Balance: ${totalBalance} lovelaces\n`; if (detailed) { for (const utxo of addressUtxos) { output += this.prettyPrintUtxo(utxo, true) + "\n"; } } } } else { output += "No UTxOs in the ledger.\n"; } // Mempool output += `\n=== Mempool ===\n`; output += this.prettyPrintMempool(detailed); // Stake Addresses (if implemented) if (this.stakeAddresses.size > 0) { output += `\n=== Stake Addresses ===\n`; for (const [address, info] of this.stakeAddresses.entries()) { output += `Address: ${address}\n`; output += ` Rewards: ${info.rewards}\n`; } } // Datum Table if (this.datumTable.size > 0) { output += `\n=== Datum Table ===\n`; for (const [hash, datum] of this.datumTable.entries()) { output += `Datum Hash: ${hash}\n`; output += ` Datum: ${datum.toString()}\n`; } } output += "\n=== End of Ledger State ===\n"; return output; } /** Pretty print the mempool * @param detailed Whether to show detailed information (default: false) * @return Pretty printed string * */ prettyPrintMempool(detailed = false) { const txs = this.mempool; if (!txs.length) { return "Mempool is empty.\n"; } let output = `=== Mempool Transactions (${txs.length}) ===\n\n`; for (const tx of txs) { const txHash = tx.hash.toString(); const inputCount = tx.body.inputs.length; const outputCount = tx.body.outputs.length; const fee = tx.body.fee.toString() || "0"; const validityStart = tx.body.validityIntervalStart ? tx.body.validityIntervalStart.toString() : "N/A"; const validtityEnd = tx.body.ttl ? tx.body.ttl.toString() : "N/A"; output += `Transaction Hash: ${txHash}\n`; output += `\tInputs: ${inputCount}\n`; output += `\tOutputs: ${outputCount}\n`; output += `\tFee: ${fee}\n`; output += `\tValidity Range: Start ${validityStart}; End ${validtityEnd}\n`; // Add certificates information if available // TODO // Add withdrawals information if available if (tx.body.withdrawals) { output += `\tWithdrawals:\n`; for (const withdraw of tx.body.withdrawals.map) { const rewardAddress = withdraw.rewardAccount.toString(); const amount = withdraw.amount.toString(); output += `\t\tReward Address: ${rewardAddress} Amount: ${amount}\n`; } } output += `\n`; } output += `=== End of Mempool ===\n`; return output; } /** Getters */ /** Get genesis information */ getGenesisInfos() { return Promise.resolve({ ...this.genesisInfos }); } /** Get protocol parameters */ getProtocolParameters() { return Promise.resolve(this.protocolParameters); } /** Get the maximal size for a transaction */ getTxMaxSize() { return Number(this.protocolParameters.maxTxSize); } /** Get the current time */ getCurrentTime() { return this.time; } /** Get the current block height */ getCurrentSlot() { return this.slot; } /** Get the current block height */ getCurrentBlockHeight() { return this.blockHeight; } /** Get the current block information */ getChainTip() { return this.lastBlock; } /** Returns the set of UTxOs */ getUtxos() { return Array.from(this.utxos.values()); } /** * Get all transactions in the mempool */ getMempool() { return Array.from(this.mempool); } /** Helper */ /** Get the size of a transaction */ getTxSize(tx) { return tx ? ((tx instanceof plu_ts_1.Tx ? tx.toCbor() : tx).toBuffer().length) : 0; } fromSlotToPosix(slot) { return BigInt(slot) * BigInt(this.genesisInfos.slotLengthMs) + BigInt(this.genesisInfos.systemStartPosixMs); } /** * Calculate the minimum required fee for a transaction * @param tx The transaction * @returns The minimum required fee in lovelace */ calculateMinFee(tx) { // Get protocol parameters for fee calculation let a = this.protocolParameters.txFeePerByte; let b = this.protocolParameters.txFeeFixed; this.debug(1, `txFeePerByte: ${a} of type ${typeof (a)} txFeeFixed: ${b} of type ${typeof (b)}`); if (typeof a == undefined) { this.debug(0, "Invalid txFeePerByte. Defaulting to 0."); a = BigInt(0); } if (typeof b == undefined) { this.debug(0, "Invalid txFeeFixed. Defaulting to 0."); b = BigInt(0); } // Calculate transaction size in bytes const txSize = tx.toCbor().toString().length / 2; // Convert hex string to bytes // Calculate minimum fee: a * txSize + b const minFee = (BigInt(a) * BigInt(txSize)) + BigInt(b); return minFee; } /** Debug */ /** Set the debug level */ setDebugLevel(level) { this.debugLevel = level; } /** Get the debug level */ /** Debug log amethod * @param level Debug level (0: no debug, 1: basic debug, 2: detailed debug) * @param message Debug message * @returns void */ debug(level, message) { const COLOR_CODES = { RED: "\x1b[31m", YELLOW: "\x1b[33m", GREEN: "\x1b[32m", RESET: "\x1b[0m" }; if (this.debugLevel >= level) { let color; switch (level) { case 0: color = COLOR_CODES.RED; break; case 1: color = COLOR_CODES.YELLOW; break; case 2: color = COLOR_CODES.GREEN; break; default: color = COLOR_CODES.RESET; break; } console.log(`${color}[Emulator Debug level ${level}]: ${COLOR_CODES.RESET}${message}`); } } /** * Resolves the utxos that are present on the current ledger state * * Note: if some of the specified utxos are not present (have been spent already) * they will be filtered out * @param utxos UTxOs to resolve * @returns Promise<UTxO[]> Resolved UTxOs */ resolveUtxos(utxos) { this.debug(2, `Resolving UTxOs: ${utxos.map(u => (0, plu_ts_1.forceTxOutRefStr)(u)).join(', ')}`); return Promise.resolve([...new Set(utxos.map(plu_ts_1.forceTxOutRefStr))] .map(ref => this.utxos.get(ref)?.clone()) .filter(u => u instanceof plu_ts_1.UTxO)); } /** * Resolves UTxOs for a specific address * @param address Address to find UTxOs for * @returns Array of UTxOs belonging to the address, or undefined if none found */ resolveUtxosbyAddress(address) { // Ensure we have a proper AddressStr by using toString() from the Address object // This maintains the type safety const addressStr = address instanceof plu_ts_1.Address ? address.toString() : address; this.debug(2, `Resolving UTxOs for address: ${addressStr}`); // Check if the address exists in our address map if (!this.addresses.has(addressStr)) { this.debug(1, `No UTxOs found for address: ${addressStr}`); return undefined; } // Get the set of UTxO references for this address const utxoRefs = this.addresses.get(addressStr); this.debug(2, `Found ${utxoRefs.size} UTxO references for address: ${addressStr}`); // Resolve each UTxO reference to its full UTxO object const utxos = []; for (const ref of utxoRefs) { const utxo = this.utxos.get(ref); if (utxo) { utxos.push(utxo.clone()); } } // Log the resolved UTxOs this.debug(2, `Resolved ${utxos.length} UTxOs for address ${addressStr}:`); // Log individual UTxO details - this will only execute if debug level is sufficient utxos.forEach(utxo => { this.debug(2, ` UTxO: ${utxo.utxoRef.toString()}, Value: ${utxo.resolved.value.lovelaces} lovelaces`); }); return utxos.length > 0 ? utxos : undefined; } /** * Retrieves UTxOs for a specific address (Blockfrost API compatible method) * @param address Address to find UTxOs for * @returns Promise with array of UTxOs belonging to the address */ async addressUtxos(address) { // Just delegate to our main implementation const utxos = this.resolveUtxosbyAddress(address); // Return empty array instead of undefined to match Blockfrost behavior return Promise.resolve(utxos || []); } /** * Resolves datum hashes to their corresponding datum values * @param hashes Array of Hash32 objects representing datum hashes to resolve * @returns Promise with an array of resolved datums with their hashes */ async resolveDatumHashes(hashes) { this.debug(2, `Resolving ${hashes.length} datum hashes`); // Map to store resolved datums const resolvedDatums = []; // Iterate through each hash and try to find it in the datum table for (const hash of hashes) { const hashStr = hash instanceof plu_ts_1.Hash32 ? hash.toString() : String(hash); this.debug(2, `Looking up datum hash: ${hashStr}`); // Try to get the datum from the datum table const datum = this.datumTable.get(hashStr); if (datum) { this.debug(2, `Found datum for hash ${hashStr}`); resolvedDatums.push({ hash: hashStr, datum: datum }); } else { this.debug(1, `Datum hash ${hashStr} not found in datum table`); } } this.debug(2, `Resolved ${resolvedDatums.length} out of ${hashes.length} datum hashes`); return Promise.resolve(resolvedDatums); } /** * Advance to a future block * @param blocks Number of blocks to advance */ awaitBlock(blocks = 1) { if (blocks <= 0) { this.debug(0, "Invalid call to awaitBlock. Argument blocks must be greater than zero."); return; } this.blockHeight += blocks; this.slot += blocks * (this.genesisInfos.slotLengthMs * 20 / 1000); // Not sure where to compute the 20 from this.time += blocks * this.genesisInfos.slotLengthMs; this.debug(1, `Advancing to block number ${this.blockHeight} (slot ${this.slot}). Time: ${new Date(this.time).toISOString()}`); // Number of blocks processed let blockProcessed = 0; while (this.mempool.length > 0 && blockProcessed < blocks) { this.debug(2, `Processing block ${blockProcessed + 1} of ${blocks}`); this.updateLedger(); blockProcessed++; } // Fast forward if the mempool is empty if (blockProcessed < blocks && this.mempool.length === 0) { this.debug(2, `Fast forwarding remaning ${blocks - blockProcessed} blocks as mempool is empty`); } } /** Advance to a future slot * @param slots Number of slots to advance * @returns void * */ awaitSlot(slots = 1) { if (slots <= 0) { this.debug(0, "Invalid call to awaitSlot. Argument slots must be greater than zero."); return; } this.slot += slots; this.time += slots * this.genesisInfos.slotLengthMs; const currentHeight = this.blockHeight; this.blockHeight = Math.floor(this.slot / (this.genesisInfos.slotLengthMs * 20 / 1000)); // Not sure where to compute the 20 from if (this.blockHeight > currentHeight) { this.debug(1, `Advancing to block number ${this.blockHeight} (slot ${this.slot}). Time: ${new Date(this.time).toISOString()}`); let blocksToBeProcessed = this.blockHeight - currentHeight; this.debug(2, `Processing ${blocksToBeProcessed} blocks`); while (blocksToBeProcessed > 0 && this.mempool.length > 0) { this.updateLedger(); blocksToBeProcessed--; } if (blocksToBeProcessed > 0 && this.mempool.length === 0) { this.debug(2, `Fast forwarding remaining ${blocksToBeProcessed} blocks as mempool is empty`); } } else { this.debug(2, `Slot ${this.slot} is in the same block as ${this.blockHeight}. No blocks to be processed`); } } /** Update the ledger by processing the mempool, respecting the block size limit */ updateLedger() { this.debug(1, `Updating ledger, mempool length: ${this.mempool.length}`); // Check if the mempool is empty. Should not happen here. if (this.mempool.length === 0) { this.debug(2, "Mempool is empty. No transactions to process."); return; } const maxBlockBodySize = this.protocolParameters.maxBlockBodySize; this.debug(2, `Max block body size: ${maxBlockBodySize}`); // Process transaction in the mempool until the block size limit is reached let currentBlockSize = 0; let txsProcessed = 0; let totalFees = BigInt(0); while (this.mempool.length > 0) { // Peek at the next transaction in the mempool without removing it const nextTx = this.mempool.peek(); if (!nextTx) { this.debug(2, "No more transactions in the mempool."); break; } // Get the size of the transaction const txSize = this.getTxSize(nextTx); if (currentBlockSize + txSize > maxBlockBodySize) { this.debug(2, `Next transaction, of size ${txSize}, will not fit in the block. Current block size: ${currentBlockSize}.`); break; // Block is full, process next transaction in the next block } this.debug(2, `Processing transaction of size ${txSize}.`); // Dequeue the transaction from the mempool const tx = this.mempool.dequeue(); if (!tx) { this.debug(2, "No transaction to process."); break; } try { // Process the transaction this.processTx(tx); // Calculate the fee totalFees += tx.body.fee; // Update the counters currentBlockSize += txSize; txsProcessed++; this.debug(2, `Processed transaction ${tx.hash.toString()}, size ${txSize} bytes, block: ${currentBlockSize}/${maxBlockBodySize} bytes.`); } catch (error) { this.debug(0, `Failed to process transaction ${tx.hash.toString()}: ${error}`); } } // Update the last block information this.lastBlock = { time: this.time, hight: this.blockHeight, slot: this.slot, slot_leader: "emulator", size: currentBlockSize, tx_count: txsProcessed, fees: totalFees, }; this.debug(1, `Block processing complete. ${txsProcessed} transactions processed for ${currentBlockSize} bytes.`); } /** Process a transaction into the ledger state * @param tx Transaction to process * @returns void */ processTx(tx) { const txHash = tx.hash.toString(); this.debug(1, `Processing transaction ${txHash}`); const isValidTx = this.validateTx(tx); if (!isValidTx) { this.debug(0, `Transaction ${txHash} failed validation on-chain. Skipping.`); // TODO: Add collateral slashing // this.debug(0, `Slashing collateral for transaction ${txHash}`); // this.slashCollateral(tx); // return; } // Remove the inputs from the ledger for (const input of tx.body.inputs) { const utxoRef = (0, plu_ts_1.forceTxOutRefStr)(input); this.debug(2, `Removing input ${utxoRef} from ledger`); this.removeUtxoFromLedger(utxoRef); } // Add the outputs to the ledger tx.body.outputs.forEach((output, index) => { // Create a UTxO from the output const utxo = new plu_ts_1.UTxO({ resolved: output, utxoRef: new plu_ts_1.TxOutRef({ id: txHash, index: index }), }); this.debug(2, `Adding output ${utxo.utxoRef.toString()} to ledger`); this.addUtxoToLedger(utxo); }); // Log the updated UTxO set this.debug(1, `Updated UTxO Set: ${this.utxos}`); // Process withdrawals // Note: We're not really putting rewards in the accounts so far so need to fix that. TODO if (tx.body.withdrawals) { for (const withdraw of tx.body.withdrawals.map) { const rewardAddress = withdraw.rewardAccount; const amount = withdraw.amount; const staking = this.stakeAddresses.get(rewardAddress.toString()); if (staking) { staking.rewards -= amount; } } } // Process certificates // Note: Not implemented yet. TODO // Store any new datum in the datum table if (tx.witnesses.datums) { for (const [datumHash, datum] of tx.witnesses.datums.entries()) { this.datumTable.set(datumHash.toString(), datum); } } // ...? } /** Submit a transaction to the mempool * @param txCBOR Transaction to submit (CBOR or Tx object) * @returns Transaction hash * Note: [RS] I think we should allow users to transactions that will fail script validation */ async submitTx(txCBOR) { const tx = txCBOR instanceof plu_ts_1.Tx ? txCBOR : plu_ts_1.Tx.fromCbor(txCBOR); this.debug(1, `Submitting transaction ${tx.hash.toString()}`); this.debug(1, `Transaction body: ${JSON.stringify(tx.body)}`); const isValidTx = await this.validateTx(tx); if (isValidTx) { this.debug(1, `Transaction ${tx.hash.toString()} has passed phase-1 validation. Proceeding for phase-2.`); const phase2ValidTx = await this.txBuilder.validatePhaseTwo(tx); this.debug(1, `Phase2 validation result: ${await this.txBuilder.validatePhaseTwoVerbose(tx)} `); if (phase2ValidTx) { // Add the transaction to the mempool this.mempool.enqueue(tx); this.debug(1, `Transaction ${tx.hash.toString()} is valid: Adding to mempool, length: ${this.mempool.length}.`); return Promise.resolve(tx.hash.toString()); } else { this.debug(0, `Transaction ${tx.hash.toString()} failed phase-2 validation. Slashing collateral.`); await this.slashCollateral(tx); return Promise.reject(`Transaction ${tx.hash.toString()} failed phase-2 validation.`); } } else { return Promise.reject(`Transaction ${tx.hash.toString()} failed phase-1 validation.`); } } /** * Validate a transaction against the current state * @param tx Transaction to validate */ async validateTx(tx) { const txHash = tx.hash.toString(); this.debug(2, `Validating transaction: ${txHash}`); // 0. Check that the transaction is well-formed if (!tx.body) { this.debug(0, "Invalid transaction: no body."); return false; } // 1. Check that the inputs are present in the ledger for (const input of tx.body.inputs) { const inputStr = (0, plu_ts_1.forceTxOutRefStr)(input); if (!this.utxos.has(inputStr)) { this.debug(0, `Input ${inputStr} not found in the ledger.`); return false; } } // 2. Check that the transaction has at least one input // Note: A Tx can have no output: e.g. https://cexplorer.io/tx/d2a2098fabb73ace002e2cf7bf7131a56723cd0745b1ef1a4f9e29fd27c0eb68 if (tx.body.inputs.length === 0) { this.debug(0, "Transaction must have at least one input or mint tokens"); return false; } // 3. Check for duplicate inputs const inputSet = new Set(); for (const input of tx.body.inputs) { const inputStr = (0, plu_ts_1.forceTxOutRefStr)(input); if (inputSet.has(inputStr)) { this.debug(0, `Duplicate input detected: ${inputStr}`); return false; } inputSet.add(inputStr); } // 4. Check transaction size against limit const txSize = this.getTxSize(tx); const maxTxSize = this.protocolParameters.maxTxSize; if (txSize > maxTxSize) { this.debug(0, `Transaction size (${txSize} bytes) exceeds maximum allowed size (${maxTxSize} bytes)`); return false; } // 5. Collateral presence check for phase 1 if (!this.validateCollateral(tx)) { this.debug(0, `Insufficient collateral. Atleast 5 ADA collateral is required for script inputs.`); return false; } this.debug(2, `Transaction ${txHash} is valid`); return true; } /** * Validate tx inputs to ensure sufficient collateral is provided in it * @param tx Transaction to validate collateral for * @returns */ validateCollateral(tx) { const minCollateral = BigInt(this.protocolParameters.collateralPercentage || 0) * tx.body.fee / BigInt(100); const hasScriptInput = tx.body.inputs.some(input => { const utxo = this.utxos.get(input.utxoRef.toString()); if (!utxo || !utxo.resolved.address || !utxo.resolved.address.paymentCreds) { throw new Error(`Invalid input: ${input.utxoRef.toString()}`); } return utxo && utxo.resolved.address.paymentCreds.type === plu_ts_1.CredentialType.Script; // Check if input is a script }); // TODO: Check on an example with refScript const hasRefScriptInputs = tx.body.refInputs?.some(input => { const utxo = this.utxos.get(input.utxoRef.toString()); if (!utxo || !utxo.resolved.address || !utxo.resolved.address.paymentCreds) { throw new Error(`Invalid ref input: ${input.utxoRef.toString()}`); } return utxo && utxo.resolved.address.paymentCreds.type === plu_ts_1.CredentialType.Script; // Check if ref input is a script }); if (hasScriptInput || hasRefScriptInputs) { const collateralInputs = tx.body.collateralInputs || []; collateralInputs.forEach(input => { const utxo = this.utxos.get(input.utxoRef.toString()); if (!utxo || !utxo.resolved.address) { throw new Error(`Invalid collateral input: ${input.utxoRef.toString()}`); } if (!utxo.resolved.address.paymentCreds) { throw new Error(`Invalid collateral address: ${JSON.stringify(utxo.resolved.address)}`); } }); const collateralSum = collateralInputs.reduce((sum, input) => { const utxo = this.utxos.get(input.utxoRef.toString()); return utxo ? sum + utxo.resolved.value.lovelaces : sum; }, BigInt(0)); if (collateralSum < minCollateral) { this.debug(0, `Insufficient collateral: ${collateralSum} lovelaces provided, but ${minCollateral} required.`); return false; } } return true; // No collateral required if no script inputs are present } /** * Slash collateral for a failed transaction * @param tx The transaction that failed phase-2 validation */ async slashCollateral(tx) { this.debug(1, `Executing slashCollateral for transaction ${tx.hash.toString()}`); const collateralInputs = tx.body.collateralInputs || []; const collateralPercentage = this.protocolParameters.collateralPercentage || 150; // Default to 150% if not defined if (collateralInputs.length === 0) { this.debug(0, `No collateral inputs found for transaction ${tx.hash.toString()}`); return Promise.resolve(); // Return a resolved promise if no collateral inputs are found } for (const input of collateralInputs) { const utxoRef = (0, plu_ts_1.forceTxOutRefStr)(input); const utxo = this.utxos.get(utxoRef); if (!utxo) { this.debug(0, `Collateral UTxO ${utxoRef} not found in the ledger.`); continue; } const fee = tx.body.fee || BigInt(0); const collateralToSlash = (fee * BigInt(collateralPercentage)) / BigInt(100); this.debug(1, `Slashing collateral from UTxO ${utxoRef}: ${collateralToSlash} lovelaces`); if (utxo.resolved.value.lovelaces >= collateralToSlash) { // Calculate remaining collateral const remainingLovelaces = utxo.resolved.value.lovelaces - collateralToSlash; const changeAddress = utxo.resolved.address; if (remainingLovelaces > BigInt(0)) { // Return the remaining collateral to the change address // Create a Value object for remaining ADA const remainingAdaValue = plu_ts_1.Value.lovelaces(remainingLovelaces); // Combine ADA with other tokens using the static Value.add method const remainingValue = plu_ts_1.Value.add(remainingAdaValue, utxo.resolved.value); const remainingUtxo = new plu_ts_1.UTxO({ resolved: { address: changeAddress, value: remainingValue, datum: utxo.resolved.datum, refScript: utxo.resolved.refScript, }, utxoRef: new plu_ts_1.TxOutRef({ id: tx.hash.toString(), index: 0, // Assuming index 0 for the new UTxO }), }); this.addUtxoToLedger(remainingUtxo); this.debug(1, `Returned remaining collateral to change address: ${changeAddress.toString()}`); } else { this.debug(1, `No remaining collateral to return.`); } } else { this.debug(1, `Collateral is insufficient to cover the slashing amount.`); } // Remove the slashed collateral UTxO from the ledger this.removeUtxoFromLedger(utxoRef); this.debug(1, `Slashed ${collateralToSlash} lovelaces from collateral UTxO ${utxoRef}`); } return Promise.resolve(); // Ensure the method returns a resolved promise } } exports.Emulator = Emulator; /** * Initialize an emulator with UTxOs for testing * @param addresses Map of addresses and their initial balances in lovelaces * @returns Configured Emulator instance */ function initializeEmulator(addresses = new Map()) { const initialUtxos = []; let index = 0; // Create UTxOs for each address with specified amount for (const [address, lovelaces] of addresses.entries()) { if (!address || typeof address.toString !== "function") { throw new Error(`Invalid address: ${address}`); } const txHash = (0, helper_1.generateRandomTxHash)(index); const utxo = (0, helper_1.createInitialUTxO)(lovelaces, address, txHash); initialUtxos.push(utxo); index++; } const instance = new Emulator(initialUtxos, buildooor_1.defaultMainnetGenesisInfos, plu_ts_1.defaultProtocolParameters, 0 // Debug level ); return instance; } exports.initializeEmulator = initializeEmulator; /** * Initialize an emulator when handling a single wallet address. * If the address already has a UTxO with 15 ADA, reuse it; otherwise, throw error expecting wallet to be populated manually. * @param utxos The UTxOs contained in the browser wallet to initialize the emulator with. * @returns Configured Emulator instances */ function initializeEmulatorWithWalletUtxOs(utxos) { const initialUtxos = []; // Check if wallet already has a UTxO with 15 ADA if (!utxos.length) throw new Error("Wallet doesn't have enough funds. Have you requested funds from the faucet?"); const utxo = utxos.find(u => u.resolved.value.lovelaces >= 15000000); if (utxo === undefined) throw new Error("Not enough ADA"); initialUtxos.push(utxo); return new Emulator(initialUtxos, buildooor_1.defaultMainnetGenesisInfos, plu_ts_1.defaultProtocolParameters, 0 // Debug level ); } exports.initializeEmulatorWithWalletUtxOs = initializeEmulatorWithWalletUtxOs;