diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index 9955f4ad7..5e94eaab6 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -329,6 +329,12 @@
flexmark-util-options
0.64.0
+
+
+ org.web3j
+ core
+ 4.9.8
+
com.zaxxer
HikariCP
diff --git a/obp-api/src/main/scala/code/bankconnectors/ethereum/DecodeRawTx.scala b/obp-api/src/main/scala/code/bankconnectors/ethereum/DecodeRawTx.scala
new file mode 100644
index 000000000..c84621332
--- /dev/null
+++ b/obp-api/src/main/scala/code/bankconnectors/ethereum/DecodeRawTx.scala
@@ -0,0 +1,119 @@
+package code.bankconnectors.ethereum
+
+import java.math.BigInteger
+import org.web3j.crypto.{Hash, RawTransaction, TransactionDecoder, Sign, SignedRawTransaction}
+import org.web3j.utils.{Numeric => W3Numeric}
+import net.liftweb.json._
+
+object DecodeRawTx {
+
+ private def fatal(msg: String): Nothing = {
+ Console.err.println(msg)
+ sys.exit(1)
+ }
+
+ // File/stdin helpers removed; input is provided as a function parameter now.
+
+ private def normalizeHex(hex: String): String = {
+ val h = Option(hex).getOrElse("").trim
+ if (!h.startsWith("0x")) fatal("Input must start with 0x")
+ val body = h.drop(2)
+ if (!body.matches("[0-9a-fA-F]+")) fatal("Invalid hex characters in input")
+ if (body.length % 2 != 0) fatal("Hex string length must be even")
+ "0x" + body.toLowerCase
+ }
+
+ private def detectType(hex: String): Int = {
+ val body = hex.stripPrefix("0x")
+ if (body.startsWith("02")) 2
+ else if (body.startsWith("01")) 1
+ else 0
+ }
+
+ // Build EIP-155 v when chainId is available; parity from v-byte (27/28 -> 0/1, otherwise lowest bit)
+ private def vToHex(sig: Sign.SignatureData, chainIdOpt: Option[BigInteger]): String = {
+ val vb: Int = {
+ val arr = sig.getV
+ if (arr != null && arr.length > 0) java.lang.Byte.toUnsignedInt(arr(0)) else 0
+ }
+ chainIdOpt match {
+ case Some(cid) if cid.signum() > 0 =>
+ val parity = if (vb == 27 || vb == 28) vb - 27 else (vb & 1)
+ val v = cid.multiply(BigInteger.valueOf(2)).add(BigInteger.valueOf(35L + parity))
+ "0x" + v.toString(16)
+ case _ =>
+ "0x" + Integer.toHexString(vb)
+ }
+ }
+
+ private def jStrOrNull(v: String): JValue = if (v == null) JNull else JString(v)
+ private def jOptStrOrNull(v: Option[String]): JValue = v.map(JString).getOrElse(JNull)
+
+ /**
+ * Decode raw Ethereum transaction hex and return a JSON string summarizing the fields.
+ * The input must be a 0x-prefixed hex string; no file or stdin reading is performed.
+ */
+ def decodeRawTxToJson(rawIn: String): String = {
+ val rawHex = normalizeHex(rawIn)
+ val txType = detectType(rawHex)
+
+ val decoded: RawTransaction =
+ try TransactionDecoder.decode(rawHex)
+ catch { case e: Throwable => fatal(s"decode failed: ${e.getMessage}") }
+
+ val (fromOpt, chainIdOpt, vHexOpt, rHexOpt, sHexOpt):
+ (Option[String], Option[BigInteger], Option[String], Option[String], Option[String]) = decoded match {
+ case srt: SignedRawTransaction =>
+ val from = Option(srt.getFrom)
+ val cid: Option[BigInteger] = try {
+ val c = srt.getChainId // long in web3j 4.x; -1 if absent
+ if (c > 0L) Some(BigInteger.valueOf(c)) else None
+ } catch { case _: Throwable => None }
+ val sig = srt.getSignatureData
+ val vH = vToHex(sig, cid)
+ val rH = W3Numeric.toHexString(sig.getR)
+ val sH = W3Numeric.toHexString(sig.getS)
+ (from, cid, Some(vH), Some(rH), Some(sH))
+ case _ =>
+ (None, None, None, None, None)
+ }
+
+ val hash = Hash.sha3(rawHex)
+ val gasPriceHex = Option(decoded.getGasPrice).map(W3Numeric.toHexStringWithPrefix).getOrElse(null)
+ val gasLimitHex = Option(decoded.getGasLimit).map(W3Numeric.toHexStringWithPrefix).getOrElse(null)
+ val valueHex = Option(decoded.getValue).map(W3Numeric.toHexStringWithPrefix).getOrElse(null)
+ val nonceHex = Option(decoded.getNonce).map(W3Numeric.toHexStringWithPrefix).getOrElse(null)
+ val toAddr = decoded.getTo
+ val inputData = Option(decoded.getData).getOrElse("0x")
+
+ val estimatedFeeHex =
+ (for {
+ gp <- Option(decoded.getGasPrice)
+ gl <- Option(decoded.getGasLimit)
+ } yield W3Numeric.toHexStringWithPrefix(gp.multiply(gl))).getOrElse(null)
+
+ val j = JObject(List(
+ JField("hash", JString(hash)),
+ JField("type", JString(txType.toString)),
+ JField("chainId", chainIdOpt.map(cid => JString(cid.toString)).getOrElse(JNull)),
+ JField("nonce", jStrOrNull(nonceHex)),
+ JField("gasPrice", jStrOrNull(gasPriceHex)),
+ JField("gas", jStrOrNull(gasLimitHex)),
+ JField("to", jStrOrNull(toAddr)),
+ JField("value", jStrOrNull(valueHex)),
+ JField("input", jStrOrNull(inputData)),
+ JField("from", jOptStrOrNull(fromOpt)),
+ JField("v", jOptStrOrNull(vHexOpt)),
+ JField("r", jOptStrOrNull(rHexOpt)),
+ JField("s", jOptStrOrNull(sHexOpt)),
+ JField("estimatedFee", jStrOrNull(estimatedFeeHex))
+ ))
+ compactRender(j)
+ }
+
+ def main(args: Array[String]): Unit = {
+ val raxHex = "0xf86b178203e882520894627306090abab3a6e1400e9345bc60c78a8bef57880de0b6b3a764000080820ff6a016878a008fb817df6d771749336fa0c905ec5b7fafcd043f0d9e609a2b5e41e0a0611dbe0f2ee2428360c72f4287a2996cb0d45cb8995cc23eb6ba525cb9580e02"
+ val out = decodeRawTxToJson(raxHex)
+ print(out)
+ }
+}
\ No newline at end of file