Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package scalus.cardano.node

import scalus.uplc.DebugScript
import scalus.cardano.address.Address
import scalus.cardano.ledger.rules.{Context, DefaultMutators, DefaultValidators, STS, State}
import scalus.cardano.ledger.*
Expand Down Expand Up @@ -36,6 +37,20 @@ class Emulator(
}
}

def submitSync(
transaction: Transaction,
debugScripts: Map[ScriptHash, DebugScript]
): Either[SubmitError, TransactionHash] = {
val ctxWithDebug = context.copy(debugScripts = debugScripts)
processTransaction(ctxWithDebug, state, transaction) match {
case Right(newState) =>
state = newState
Right(transaction.id)
case Left(t: TransactionException) =>
Left(SubmitError.fromException(t))
}
}

def setSlot(slot: SlotNo): Unit = {
context = Context(
fee = context.fee,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package scalus.cardano.node

import io.bullet.borer.Cbor
import scalus.uplc.DebugScript
import scalus.uplc.builtin.ByteString
import scalus.cardano.address.Address
import scalus.cardano.ledger.rules.{Context, UtxoEnv}
import scalus.cardano.ledger.*
import scalus.cardano.ledger.utils.AllResolvedScripts

import scala.scalajs.js
import scala.scalajs.js.annotation.{JSExportStatic, JSExportTopLevel}
Expand Down Expand Up @@ -40,12 +43,67 @@ class JEmulator(
*/
def submitTx(txCborBytes: Uint8Array): js.Dynamic = {
val tx = Transaction.fromCbor(txCborBytes.toArray.map(_.toByte))
emulator.submitSync(tx) match {
formatSubmitResult(emulator.submitSync(tx))
}

/** Submit a transaction with debug scripts for diagnostic replay.
*
* Debug scripts are provided as a dictionary mapping script hash hex to double-CBOR hex of the
* debug-compiled script. The language version is resolved from the release script in the
* transaction.
*
* @param txCborBytes
* CBOR-encoded transaction bytes
* @param debugScripts
* dictionary mapping scriptHashHex to doubleCborHex of debug script
* @return
* Object with isSuccess, txHash (on success), or error and logs (on failure)
*/
def submitTx(txCborBytes: Uint8Array, debugScripts: js.Dictionary[String]): js.Dynamic = {
val tx = Transaction.fromCbor(txCborBytes.toArray.map(_.toByte))

// Resolve scripts from the transaction to determine language versions
val resolvedScripts = AllResolvedScripts.allResolvedScriptsMap(tx, emulator.utxos) match
case Right(map) => map
case Left(error) =>
js.Dynamic.global.console.error(
s"Emulator.submitTx(debugScripts): failed to resolve scripts: $error"
)
Map.empty[ScriptHash, Script]

// Parse debug scripts dictionary
val debugScriptsMap: Map[ScriptHash, DebugScript] = debugScripts.flatMap {
case (hashHex, doubleCborHex) =>
val hash = ScriptHash.fromHex(hashHex)
val doubleCbor = ByteString.fromHex(doubleCborHex)
// Determine language from the release script in the transaction
val languageOpt = resolvedScripts.get(hash).collect { case ps: PlutusScript =>
ps.language
}
if languageOpt.isEmpty then
js.Dynamic.global.console.warn(
s"Debug script for hash $hashHex was provided but no matching Plutus script was found in the transaction."
)
languageOpt.map { language =>
val plutusScript: PlutusScript = language match
case Language.PlutusV1 => Script.PlutusV1(doubleCbor)
case Language.PlutusV2 => Script.PlutusV2(doubleCbor)
case Language.PlutusV3 => Script.PlutusV3(doubleCbor)
case _ => Script.PlutusV3(doubleCbor)
hash -> DebugScript(plutusScript)
}
}.toMap
Comment on lines 75 to 95
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug scripts dictionary parsing uses flatMap which silently drops entries where the hash is not found in resolvedScripts or the script is not a PlutusScript. This means if a user provides a debug script for a hash that doesn't appear in the transaction, it will be silently ignored. Consider adding logging or validation to inform users when their provided debug scripts are not being used, as this could indicate a mismatch between the provided hashes and the actual transaction scripts.

Suggested change
val debugScriptsMap: Map[ScriptHash, DebugScript] = debugScripts.flatMap {
case (hashHex, doubleCborHex) =>
val hash = ScriptHash.fromHex(hashHex)
val doubleCbor = ByteString.fromHex(doubleCborHex)
// Determine language from the release script in the transaction
val languageOpt = resolvedScripts.get(hash).collect { case ps: PlutusScript =>
ps.language
}
languageOpt.map { language =>
val plutusScript: PlutusScript = language match
case Language.PlutusV1 => Script.PlutusV1(doubleCbor)
case Language.PlutusV2 => Script.PlutusV2(doubleCbor)
case Language.PlutusV3 => Script.PlutusV3(doubleCbor)
case _ => Script.PlutusV3(doubleCbor)
hash -> DebugScript(plutusScript)
}
}.toMap
val debugScriptsMap: Map[ScriptHash, DebugScript] =
debugScripts.foldLeft(Map.empty[ScriptHash, DebugScript]) {
case (acc, (hashHex, doubleCborHex)) =>
val hash = ScriptHash.fromHex(hashHex)
val doubleCbor = ByteString.fromHex(doubleCborHex)
// Determine language from the release script in the transaction
val languageOpt = resolvedScripts.get(hash).collect { case ps: PlutusScript =>
ps.language
}
languageOpt match
case Some(language) =>
val plutusScript: PlutusScript = language match
case Language.PlutusV1 => Script.PlutusV1(doubleCbor)
case Language.PlutusV2 => Script.PlutusV2(doubleCbor)
case Language.PlutusV3 => Script.PlutusV3(doubleCbor)
case _ => Script.PlutusV3(doubleCbor)
acc + (hash -> DebugScript(plutusScript))
case None =>
// Inform users when a provided debug script is not used
js.Dynamic.global.console.warn(
s"Debug script for hash $hashHex was provided but no matching Plutus script was found in the transaction."
)
acc

Copilot uses AI. Check for mistakes.

formatSubmitResult(emulator.submitSync(tx, debugScriptsMap))
}

private def formatSubmitResult(result: Either[SubmitError, TransactionHash]): js.Dynamic =
result match {
case Right(txHash) =>
js.Dynamic.literal(isSuccess = true, txHash = txHash.toHex)
case Left(submitError) =>
submitError match {
case NodeSubmitError.ScriptFailure(msg, _, logs) if logs.nonEmpty =>
case NodeSubmitError.ScriptFailure(msg, logs, _) if logs.nonEmpty =>
js.Dynamic.literal(
isSuccess = false,
error = msg,
Expand All @@ -55,7 +113,6 @@ class JEmulator(
js.Dynamic.literal(isSuccess = false, error = submitError.message)
}
}
}

/** Get all UTxOs as CBOR. */
def getUtxosCbor(): Uint8Array = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package scalus.cardano.node

import scalus.uplc.DebugScript
import scalus.cardano.address.Address
import scalus.cardano.ledger.rules.{Context, DefaultMutators, DefaultValidators, STS, State}
import scalus.cardano.ledger.*
Expand Down Expand Up @@ -44,6 +45,24 @@ class Emulator(
}
}

@tailrec
final def submitSync(
transaction: Transaction,
debugScripts: Map[ScriptHash, DebugScript]
): Either[SubmitError, TransactionHash] = {
val currentState = stateRef.get()
val ctx = contextRef.get()
val ctxWithDebug = ctx.copy(debugScripts = debugScripts)

processTransaction(ctxWithDebug, currentState, transaction) match {
case Right(newState) =>
if stateRef.compareAndSet(currentState, newState) then Right(transaction.id)
else submitSync(transaction, debugScripts)
case Left(t: TransactionException) =>
Left(SubmitError.fromException(t))
}
}

@tailrec
final def setSlot(slot: SlotNo): Unit = {
val ctx = contextRef.get()
Expand Down
Loading
Loading