Merge pull request #2620 from hongwei1/develop

feature/OBPv6.0.0 added ETH payments endpoints ETH_SEND_RAW_TRANSACTION and ETH_SEND_TRANSACTION
This commit is contained in:
Simon Redfern 2025-10-02 13:35:22 +02:00 committed by GitHub
commit 3b7b31b4f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 969 additions and 20 deletions

199
build.sbt Normal file
View File

@ -0,0 +1,199 @@
ThisBuild / version := "1.10.1"
ThisBuild / scalaVersion := "2.12.20"
ThisBuild / organization := "com.tesobe"
// Java version compatibility
ThisBuild / javacOptions ++= Seq("-source", "11", "-target", "11")
ThisBuild / scalacOptions ++= Seq(
"-unchecked",
"-explaintypes",
"-target:jvm-1.8",
"-Yrangepos"
)
// Enable SemanticDB for Metals
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := "4.13.9"
// Fix dependency conflicts
ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
lazy val liftVersion = "3.5.0"
lazy val akkaVersion = "2.5.32"
lazy val jettyVersion = "9.4.50.v20221201"
lazy val avroVersion = "1.8.2"
lazy val commonSettings = Seq(
resolvers ++= Seq(
"Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases",
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
"Artima Maven Repository" at "https://repo.artima.com/releases",
"OpenBankProject M2 Repository" at "https://raw.githubusercontent.com/OpenBankProject/OBP-M2-REPO/master",
"jitpack.io" at "https://jitpack.io"
)
)
lazy val obpCommons = (project in file("obp-commons"))
.settings(
commonSettings,
name := "obp-commons",
libraryDependencies ++= Seq(
"net.liftweb" %% "lift-common" % liftVersion,
"net.liftweb" %% "lift-util" % liftVersion,
"net.liftweb" %% "lift-mapper" % liftVersion,
"org.scala-lang" % "scala-reflect" % "2.12.20",
"org.scalatest" %% "scalatest" % "3.2.15" % Test,
"org.scalactic" %% "scalactic" % "3.2.15",
"net.liftweb" %% "lift-json" % liftVersion,
"com.alibaba" % "transmittable-thread-local" % "2.11.5",
"org.apache.commons" % "commons-lang3" % "3.12.0",
"org.apache.commons" % "commons-text" % "1.10.0",
"com.google.guava" % "guava" % "32.0.0-jre"
)
)
lazy val obpApi = (project in file("obp-api"))
.dependsOn(obpCommons)
.settings(
commonSettings,
name := "obp-api",
libraryDependencies ++= Seq(
// Core dependencies
"net.liftweb" %% "lift-mapper" % liftVersion,
"net.databinder.dispatch" %% "dispatch-lift-json" % "0.13.1",
"ch.qos.logback" % "logback-classic" % "1.2.13",
"org.slf4j" % "log4j-over-slf4j" % "1.7.26",
"org.slf4j" % "slf4j-ext" % "1.7.26",
// Security
"org.bouncycastle" % "bcpg-jdk15on" % "1.70",
"org.bouncycastle" % "bcpkix-jdk15on" % "1.70",
"com.nimbusds" % "nimbus-jose-jwt" % "9.37.2",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.27",
// Commons
"org.apache.commons" % "commons-lang3" % "3.12.0",
"org.apache.commons" % "commons-text" % "1.10.0",
"org.apache.commons" % "commons-email" % "1.5",
"org.apache.commons" % "commons-compress" % "1.26.0",
"org.apache.commons" % "commons-pool2" % "2.11.1",
// Database
"org.postgresql" % "postgresql" % "42.4.4",
"com.h2database" % "h2" % "2.2.220" % Runtime,
"mysql" % "mysql-connector-java" % "8.0.30",
"com.microsoft.sqlserver" % "mssql-jdbc" % "11.2.0.jre11",
// Web
"javax.servlet" % "javax.servlet-api" % "3.1.0" % Provided,
"org.eclipse.jetty" % "jetty-server" % jettyVersion % Test,
"org.eclipse.jetty" % "jetty-webapp" % jettyVersion % Test,
"org.eclipse.jetty" % "jetty-util" % jettyVersion,
// Akka
"com.typesafe.akka" %% "akka-actor" % akkaVersion,
"com.typesafe.akka" %% "akka-remote" % akkaVersion,
"com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
"com.typesafe.akka" %% "akka-http-core" % "10.1.6",
// Avro
"com.sksamuel.avro4s" %% "avro4s-core" % avroVersion,
// Twitter
"com.twitter" %% "chill-akka" % "0.9.1",
"com.twitter" %% "chill-bijection" % "0.9.1",
// Cache
"com.github.cb372" %% "scalacache-redis" % "0.9.3",
"com.github.cb372" %% "scalacache-guava" % "0.9.3",
// Utilities
"com.github.dwickern" %% "scala-nameof" % "1.0.3",
"org.javassist" % "javassist" % "3.25.0-GA",
"com.alibaba" % "transmittable-thread-local" % "2.14.2",
"org.clapper" %% "classutil" % "1.4.0",
"com.github.grumlimited" % "geocalc" % "0.5.7",
"com.github.OpenBankProject" % "scala-macros" % "v1.0.0-alpha.3",
"org.scalameta" %% "scalameta" % "3.7.4",
// Akka Adapter - exclude transitive dependency on obp-commons to use local module
"com.github.OpenBankProject.OBP-Adapter-Akka-SpringBoot" % "adapter-akka-commons" % "v1.1.0" exclude("com.github.OpenBankProject.OBP-API", "obp-commons"),
// JSON Schema
"com.github.everit-org.json-schema" % "org.everit.json.schema" % "1.6.1",
"com.networknt" % "json-schema-validator" % "1.0.87",
// Swagger
"io.swagger.parser.v3" % "swagger-parser" % "2.0.13",
// Text processing
"org.atteo" % "evo-inflector" % "1.2.2",
// Payment
"com.stripe" % "stripe-java" % "12.1.0",
"com.twilio.sdk" % "twilio" % "9.2.0",
// gRPC
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % "0.8.4",
"io.grpc" % "grpc-all" % "1.48.1",
"io.netty" % "netty-tcnative-boringssl-static" % "2.0.27.Final",
"org.asynchttpclient" % "async-http-client" % "2.10.4",
// Database utilities
"org.scalikejdbc" %% "scalikejdbc" % "3.4.0",
// XML
"org.scala-lang.modules" %% "scala-xml" % "1.2.0",
// IBAN
"org.iban4j" % "iban4j" % "3.2.7-RELEASE",
// JavaScript
"org.graalvm.js" % "js" % "22.0.0.2",
"org.graalvm.js" % "js-scriptengine" % "22.0.0.2",
"ch.obermuhlner" % "java-scriptengine" % "2.0.0",
// Hydra
"sh.ory.hydra" % "hydra-client" % "1.7.0",
// HTTP
"com.squareup.okhttp3" % "okhttp" % "4.12.0",
"com.squareup.okhttp3" % "logging-interceptor" % "4.12.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
// RabbitMQ
"com.rabbitmq" % "amqp-client" % "5.22.0",
"net.liftmodules" %% "amqp_3.1" % "1.5.0",
// Elasticsearch
"org.elasticsearch" % "elasticsearch" % "8.14.0",
"com.sksamuel.elastic4s" %% "elastic4s-client-esjava" % "8.5.2",
// OAuth
"oauth.signpost" % "signpost-commonshttp4" % "1.2.1.2",
// Utilities
"cglib" % "cglib" % "3.3.0",
"com.sun.activation" % "jakarta.activation" % "1.2.2",
"com.nulab-inc" % "zxcvbn" % "1.9.0",
// Testing - temporarily disabled due to version incompatibility
// "org.scalatest" %% "scalatest" % "2.2.6" % Test,
// Jackson
"com.fasterxml.jackson.core" % "jackson-databind" % "2.12.7.1",
// Flexmark (markdown processing)
"com.vladsch.flexmark" % "flexmark-profile-pegdown" % "0.40.8",
"com.vladsch.flexmark" % "flexmark-util-options" % "0.64.0",
// Connection pool
"com.zaxxer" % "HikariCP" % "4.0.3",
// Test dependencies
"junit" % "junit" % "4.13.2" % Test,
"org.scalatest" %% "scalatest" % "3.2.15" % Test,
"org.seleniumhq.selenium" % "htmlunit-driver" % "2.36.0" % Test,
"org.testcontainers" % "rabbitmq" % "1.20.3" % Test
)
)

View File

@ -329,6 +329,12 @@
<artifactId>flexmark-util-options</artifactId>
<version>0.64.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.web3j/core -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.9.8</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>

View File

@ -1517,6 +1517,18 @@ regulated_entities = []
# super_admin_email=tom@tesobe.com
## Ethereum Connector Configuration
## ================================
## The Ethereum connector uses JSON-RPC to communicate with Ethereum nodes.
## It supports two transaction modes:
## 1) eth_sendRawTransaction - for pre-signed transactions (recommended for production)
## 2) eth_sendTransaction - for unlocked accounts (development/test environments)
##
## Ethereum RPC endpoint URL
## Default: http://127.0.0.1:8545 (local Ethereum node)
ethereum.rpc.url=http://127.0.0.1:8545
# Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md

View File

@ -29,7 +29,6 @@ import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.TransactionRequestTypes._
import com.openbankproject.commons.model.enums.{AttributeCategory, CardAttributeType, ChallengeType, TransactionRequestStatus}
import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils}
import net.liftweb.common.Full
import net.liftweb.json
import java.net.URLEncoder
@ -5759,6 +5758,16 @@ object SwaggerDefinitionsJSON {
description = descriptionExample.value,
metadata = Some(Map("202507022319" -> cardanoMetadataStringJsonV600))
)
lazy val transactionRequestBodyEthereumJsonV600 = TransactionRequestBodyEthereumJsonV600(
to = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
value = AmountOfMoneyJsonV121("ETH", "0.01"),
description = descriptionExample.value
)
lazy val transactionRequestBodyEthSendRawTransactionJsonV600 = TransactionRequestBodyEthSendRawTransactionJsonV600(
params = "0xf86b018203e882520894627306090abab3a6e1400e9345bc60c78a8bef57880de0b6b3a764000080820ff6a0d0367709eee090a6ebd74c63db7329372db1966e76d28ce219d1e105c47bcba7a0042d52f7d2436ad96e8714bf0309adaf870ad6fb68cfe53ce958792b3da36c12",
description = descriptionExample.value
)
//The common error or success format.
//Just some helper format to use in Json

View File

@ -79,7 +79,7 @@ object ErrorMessages {
// General messages (OBP-10XXX)
val InvalidJsonFormat = "OBP-10001: Incorrect json format."
val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number."
val InvalidISOCurrencyCode = """OBP-10003: Invalid Currency Value. Expected a 3-letter ISO Currency Code (e.g., 'USD', 'EUR') or 'lovelace' for Cardano transactions.""".stripMargin
val InvalidISOCurrencyCode = """OBP-10003: Invalid Currency Value. Expected a 3-letter ISO Currency Code (e.g., 'USD', 'EUR'), 'lovelace' (Cardano), or 'ETH' (Ethereum). Refer to ISO 4217 currency codes: https://www.iso.org/iso-4217-currency-codes.html""".stripMargin
val FXCurrencyCodeCombinationsNotSupported = "OBP-10004: ISO Currency code combination not supported for FX. Please modify the FROM_CURRENCY_CODE or TO_CURRENCY_CODE. "
val InvalidDateFormat = "OBP-10005: Invalid Date Format. Could not convert value to a Date."
val InvalidCurrency = "OBP-10006: Invalid Currency Value."

View File

@ -802,7 +802,8 @@ object NewStyle extends MdcLoggable{
Failure(s"$failMsg. Details: ${e.getMessage}", Full(e), Empty)
}
} map {
x => unboxFullOrFail(x, callContext, failMsg, failCode)
x =>
unboxFullOrFail(x, callContext, failMsg, failCode)
}
}

View File

@ -14,11 +14,11 @@ import code.bankconnectors.LocalMappedConnectorInternal._
import code.entitlement.Entitlement
import code.views.Views
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.Full
import net.liftweb.http.rest.RestHelper
import com.openbankproject.commons.ExecutionContext.Implicits.global
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
@ -160,6 +160,85 @@ trait APIMethods600 {
val transactionRequestType = TransactionRequestType("CARDANO")
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
staticResourceDocs += ResourceDoc(
createTransactionRequestEthereumeSendTransaction,
implementedInApiVersion,
nameOf(createTransactionRequestEthereumeSendTransaction),
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/ETH_SEND_TRANSACTION/transaction-requests",
"Create Transaction Request (ETH_SEND_TRANSACTION)",
s"""
|
|Send ETH via Ethereum JSON-RPC.
|AccountId should hold the 0x address for now.
|
|${transactionRequestGeneralText}
|
""".stripMargin,
transactionRequestBodyEthereumJsonV600,
transactionRequestWithChargeJSON400,
List(
$UserNotLoggedIn,
$BankNotFound,
$BankAccountNotFound,
InsufficientAuthorisationToCreateTransactionRequest,
InvalidTransactionRequestType,
InvalidJsonFormat,
NotPositiveAmount,
InvalidTransactionRequestCurrency,
TransactionDisabled,
UnknownError
),
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)
)
lazy val createTransactionRequestEthereumeSendTransaction: OBPEndpoint = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
"ETH_SEND_TRANSACTION" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("ETH_SEND_TRANSACTION")
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
staticResourceDocs += ResourceDoc(
createTransactionRequestEthSendRawTransaction,
implementedInApiVersion,
nameOf(createTransactionRequestEthSendRawTransaction),
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/ETH_SEND_RAW_TRANSACTION/transaction-requests",
"CREATE TRANSACTION REQUEST (ETH_SEND_RAW_TRANSACTION )",
s"""
|
|Send ETH via Ethereum JSON-RPC.
|AccountId should hold the 0x address for now.
|
|${transactionRequestGeneralText}
|
""".stripMargin,
transactionRequestBodyEthSendRawTransactionJsonV600,
transactionRequestWithChargeJSON400,
List(
$UserNotLoggedIn,
$BankNotFound,
$BankAccountNotFound,
InsufficientAuthorisationToCreateTransactionRequest,
InvalidTransactionRequestType,
InvalidJsonFormat,
NotPositiveAmount,
InvalidTransactionRequestCurrency,
TransactionDisabled,
UnknownError
),
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)
)
lazy val createTransactionRequestEthSendRawTransaction: OBPEndpoint = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
"ETH_SEND_RAW_TRANSACTION" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("ETH_SEND_RAW_TRANSACTION")
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
}
}

View File

@ -65,6 +65,20 @@ case class TransactionRequestBodyCardanoJsonV600(
metadata: Option[Map[String, CardanoMetadataStringJsonV600]] = None
) extends TransactionRequestCommonBodyJSON
// ---------------- Ethereum models (V600) ----------------
case class TransactionRequestBodyEthereumJsonV600(
params: Option[String] = None,// This is for eth_sendRawTransaction
to: String, // this is for eth_sendTransaction eg: 0x addressk
value: AmountOfMoneyJsonV121, // currency should be "ETH"; amount string (decimal)
description: String
) extends TransactionRequestCommonBodyJSON
// This is only for the request JSON body; we will construct `TransactionRequestBodyEthereumJsonV600` for OBP.
case class TransactionRequestBodyEthSendRawTransactionJsonV600(
params: String, // eth_sendRawTransaction params field.
description: String
)
case class UserJsonV600(
user_id: String,
email : String,

View File

@ -10,6 +10,7 @@ import code.atmattribute.AtmAttribute
import code.bankattribute.BankAttribute
import code.bankconnectors.akka.AkkaConnector_vDec2018
import code.bankconnectors.cardano.CardanoConnector_vJun2025
import code.bankconnectors.ethereum.EthereumConnector_vSept2025
import code.bankconnectors.rabbitmq.RabbitMQConnector_vOct2024
import code.bankconnectors.rest.RestConnector_vMar2019
import code.bankconnectors.storedprocedure.StoredProcedureConnector_vDec2019
@ -63,6 +64,7 @@ object Connector extends SimpleInjector {
"stored_procedure_vDec2019" -> StoredProcedureConnector_vDec2019,
"rabbitmq_vOct2024" -> RabbitMQConnector_vOct2024,
"cardano_vJun2025" -> CardanoConnector_vJun2025,
"ethereum_vSept2025" -> EthereumConnector_vSept2025,
// this proxy connector only for unit test, can set connector=proxy in test.default.props, but never set it in default.props
"proxy" -> ConnectorUtils.proxyConnector,
// internal is the dynamic connector, the developers can upload the source code and override connector method themselves.

View File

@ -60,6 +60,7 @@ import code.users.{UserAttribute, UserAttributeProvider, Users}
import code.util.Helper
import code.util.Helper._
import code.views.Views
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.dto.{CustomerAndAttribute, GetProductsParam, ProductCollectionItemsTree}
import com.openbankproject.commons.model._
@ -165,6 +166,10 @@ object LocalMappedConnector extends Connector with MdcLoggable {
isValidCurrencyISOCode(thresholdCurrency) match {
case true if((currency.toLowerCase.equals("lovelace")||(currency.toLowerCase.equals("ada")))) =>
(Full(AmountOfMoney(currency, "10000000000000")), callContext)
case true if(currency.equalsIgnoreCase("ETH")) =>
// For ETH, skip FX conversion and return a large threshold in wei-equivalent semantic (string value).
// Here we use a high number to effectively avoid challenge for typical dev/testing amounts.
(Full(AmountOfMoney("ETH", "10000")), callContext)
case true =>
fx.exchangeRate(thresholdCurrency, currency, Some(bankId), callContext) match {
case rate@Some(_) =>
@ -4539,7 +4544,7 @@ object LocalMappedConnector extends Connector with MdcLoggable {
// Get the threshold for a challenge. i.e. over what value do we require an out of Band security challenge to be sent?
(challengeThreshold, callContext) <- Connector.connector.vend.getChallengeThreshold(fromAccount.bankId.value, fromAccount.accountId.value, viewId.value, transactionRequestType.value, transactionRequestCommonBody.value.currency, initiator.userId, initiator.name, callContext) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold ", 400), i._2)
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold - ${nameOf(getChallengeThreshold _)}", 400), i._2)
}
challengeThresholdAmount <- NewStyle.function.tryons(s"$InvalidConnectorResponseForGetChallengeThreshold. challengeThreshold amount ${challengeThreshold.amount} not convertible to number", 400, callContext) {
BigDecimal(challengeThreshold.amount)
@ -4680,7 +4685,7 @@ object LocalMappedConnector extends Connector with MdcLoggable {
// Get the threshold for a challenge. i.e. over what value do we require an out of Band security challenge to be sent?
(challengeThreshold, callContext) <- Connector.connector.vend.getChallengeThreshold(fromAccount.bankId.value, fromAccount.accountId.value, viewId.value, transactionRequestType.value, transactionRequestCommonBody.value.currency, initiator.userId, initiator.name, callContext) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold ", 400), i._2)
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold - ${nameOf(getChallengeThreshold _)}", 400), i._2)
}
challengeThresholdAmount <- NewStyle.function.tryons(s"$InvalidConnectorResponseForGetChallengeThreshold. challengeThreshold amount ${challengeThreshold.amount} not convertible to number", 400, callContext) {
BigDecimal(challengeThreshold.amount)

View File

@ -13,7 +13,8 @@ import code.api.util.newstyle.ViewNewStyle
import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
import code.api.v2_1_0._
import code.api.v4_0_0._
import code.api.v6_0_0.TransactionRequestBodyCardanoJsonV600
import code.api.v6_0_0.{TransactionRequestBodyCardanoJsonV600, TransactionRequestBodyEthSendRawTransactionJsonV600, TransactionRequestBodyEthereumJsonV600}
import code.bankconnectors.ethereum.DecodeRawTx
import code.branches.MappedBranch
import code.fx.fx
import code.fx.fx.TTL
@ -128,7 +129,7 @@ object LocalMappedConnectorInternal extends MdcLoggable {
user.name,
callContext
) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold ", 400), i._2)
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForGetChallengeThreshold - ${nameOf(Connector.connector.vend.getChallengeThreshold _)}", 400), i._2)
}
challengeThresholdAmount <- NewStyle.function.tryons(s"$InvalidConnectorResponseForGetChallengeThreshold. challengeThreshold amount ${challengeThreshold.amount} not convertible to number", 400, callContext) {
BigDecimal(challengeThreshold.amount)
@ -773,8 +774,37 @@ object LocalMappedConnectorInternal extends MdcLoggable {
}
// Check the input JSON format, here is just check the common parts of all four types
transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) {
json.extract[TransactionRequestBodyCommonJSON]
transDetailsJson <- transactionRequestTypeValue match {
case ETH_SEND_RAW_TRANSACTION => for {
// Parse raw transaction JSON
transactionRequestBodyEthSendRawTransactionJsonV600 <- NewStyle.function.tryons(
s"$InvalidJsonFormat It should be $TransactionRequestBodyEthSendRawTransactionJsonV600 json format",
400,
callContext
) {
json.extract[TransactionRequestBodyEthSendRawTransactionJsonV600]
}
// Decode raw transaction to extract 'from' address
decodedTx = DecodeRawTx.decodeRawTxToJson(transactionRequestBodyEthSendRawTransactionJsonV600.params)
from = decodedTx.from
_ <- Helper.booleanToFuture(
s"$BankAccountNotFoundByAccountId Ethereum 'from' address must be the same as the accountId",
cc = callContext
) {
from.getOrElse("") == accountId.value
}
// Construct TransactionRequestBodyEthereumJsonV600 for downstream processing
transactionRequestBodyEthereum = TransactionRequestBodyEthereumJsonV600(
params = Some(transactionRequestBodyEthSendRawTransactionJsonV600.params),
to = decodedTx.to.getOrElse(""),
value = AmountOfMoneyJsonV121("ETH", decodedTx.value.getOrElse("0")),
description = transactionRequestBodyEthSendRawTransactionJsonV600.description
)
} yield (transactionRequestBodyEthereum)
case _ =>
NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) {
json.extract[TransactionRequestBodyCommonJSON]
}
}
transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) {
@ -785,9 +815,12 @@ object LocalMappedConnectorInternal extends MdcLoggable {
transactionAmountNumber > BigDecimal("0")
}
_ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) {
APIUtil.isValidCurrencyISOCode(transDetailsJson.value.currency)
}
_ <- (transactionRequestTypeValue match {
case ETH_SEND_RAW_TRANSACTION | ETH_SEND_TRANSACTION => Future.successful(true) // Allow ETH (non-ISO) for Ethereum requests
case _ => Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) {
APIUtil.isValidCurrencyISOCode(transDetailsJson.value.currency)
}
})
(createdTransactionRequest, callContext) <- transactionRequestTypeValue match {
case REFUND => {
@ -1411,15 +1444,15 @@ object LocalMappedConnectorInternal extends MdcLoggable {
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = "",
otherBankRoutingAddress = "",
otherBranchRoutingScheme = "",
otherBranchRoutingAddress = "",
otherAccountRoutingScheme = "",
otherAccountRoutingAddress = "",
otherBankRoutingScheme = CARDANO.toString,
otherBankRoutingAddress = transactionRequestBodyCardano.to.address,
otherBranchRoutingScheme = CARDANO.toString,
otherBranchRoutingAddress = transactionRequestBodyCardano.to.address,
otherAccountRoutingScheme = CARDANO.toString,
otherAccountRoutingAddress = transactionRequestBodyCardano.to.address,
otherAccountSecondaryRoutingScheme = "cardano",
otherAccountSecondaryRoutingAddress = transactionRequestBodyCardano.to.address,
callContext: Option[CallContext],
callContext = callContext
)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
@ -1444,6 +1477,77 @@ object LocalMappedConnectorInternal extends MdcLoggable {
callContext)
} yield (createdTransactionRequest, callContext)
}
case ETH_SEND_RAW_TRANSACTION | ETH_SEND_TRANSACTION => {
for {
// Handle ETH_SEND_RAW_TRANSACTION and ETH_SEND_TRANSACTION types with proper extraction and validation
(transactionRequestBodyEthereum, scheme) <-
if (transactionRequestTypeValue == ETH_SEND_RAW_TRANSACTION) {
Future.successful{(transDetailsJson.asInstanceOf[TransactionRequestBodyEthereumJsonV600], ETH_SEND_RAW_TRANSACTION.toString)}
} else {
for {
transactionRequestBodyEthereum <- NewStyle.function.tryons(
s"$InvalidJsonFormat It should be $TransactionRequestBodyEthereumJsonV600 json format",
400,
callContext
) {
json.extract[TransactionRequestBodyEthereumJsonV600]
}
} yield (transactionRequestBodyEthereum, ETH_SEND_TRANSACTION.toString)
}
// Basic validations
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Ethereum 'to' address is required", cc=callContext) {
Option(transactionRequestBodyEthereum.to).exists(_.nonEmpty)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Ethereum 'to' address must start with 0x and be 42 chars", cc=callContext) {
val toBody = transactionRequestBodyEthereum.to
toBody.startsWith("0x") && toBody.length == 42
}
_ <- Helper.booleanToFuture(s"$InvalidTransactionRequestCurrency Currency must be 'ETH'", cc=callContext) {
transactionRequestBodyEthereum.value.currency.equalsIgnoreCase("ETH")
}
// Create or get counterparty using the Ethereum address as secondary routing
(toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty(
name = "ethereum-" + transactionRequestBodyEthereum.to.take(27),
description = transactionRequestBodyEthereum.description,
currency = transactionRequestBodyEthereum.value.currency,
createdByUserId = u.userId,
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = scheme,
otherBankRoutingAddress = transactionRequestBodyEthereum.to,
otherBranchRoutingScheme = scheme,
otherBranchRoutingAddress = transactionRequestBodyEthereum.to,
otherAccountRoutingScheme = scheme,
otherAccountRoutingAddress = transactionRequestBodyEthereum.to,
otherAccountSecondaryRoutingScheme = scheme,
otherAccountSecondaryRoutingAddress = transactionRequestBodyEthereum.to,
callContext = callContext
)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { toCounterparty.isBeneficiary }
chargePolicy = sharedChargePolicy.toString
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyEthereum)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyEthereum,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
}
(challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext)
(transactionRequestAttributes, callContext) <- NewStyle.function.getTransactionRequestAttributes(

View File

@ -0,0 +1,177 @@
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 {
// Legacy (type 0)
// Mandatory: nonce, gasPrice, gasLimit, value (can be 0), data (can be 0x), v, r, s
// Conditional: to (present for transfers/calls; omitted for contract creation where data is init code)
// Optional/Recommended: chainId (EIP-155 replay protection; legacy pre155 may omit)
// EIP-2930 (type 1)
// Mandatory: chainId, nonce, gasPrice, gasLimit, accessList (can be empty []), v/r/s (or yParity+r+s)
// Conditional: to (omit for contract creation), value (can be 0), data (can be 0x)
// EIP-1559 (type 2)
// Mandatory: chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, accessList (can be empty []), v/r/s (or yParity+r+s)
// Conditional: to (omit for contract creation), value (can be 0), data (can be 0x)
// Derived (not part of signed payload)
// hash: derived from raw tx
// from: recovered from signature
// estimatedFee: computed (gasLimit × gasPrice or gasUsed × price at execution)
// type: 0/1/2 (0 is implicit for legacy)
//
// Case class representing the decoded transaction JSON structure
case class DecodedTxResponse(
hash: Option[String],
`type`: Option[Int],
chainId: Option[Long],
nonce: Option[Long],
gasPrice: Option[String],
gas: Option[String],
to: Option[String],
value: Option[String],
input: Option[String],
from: Option[String],
v: Option[String],
r: Option[String],
s: Option[String],
estimatedFee: Option[String]
)
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)
}
}
/**
* 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.
*
* Response is serialized from DecodedTxResponse case class with types:
* - type, chainId, nonce, value are numeric (where available)
* - gasPrice, gas, v, r, s, estimatedFee are hex strings with 0x prefix (where available)
* - input is always a string
*/
def decodeRawTxToJson(rawIn: String): DecodedTxResponse = {
implicit val formats: Formats = DefaultFormats
val rawHex = normalizeHex(rawIn)
val txType = Some(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 gasPriceHexOpt: Option[String] = Option(decoded.getGasPrice).map(W3Numeric.toHexStringWithPrefix)
val gasLimitHexOpt: Option[String] = Option(decoded.getGasLimit).map(W3Numeric.toHexStringWithPrefix)
// Convert value from WEI (BigInt) to ETH (BigDecimal) with 18 decimals
val valueDecOpt: Option[String] = Option(decoded.getValue).map { wei =>
(BigDecimal(wei) / BigDecimal("1000000000000000000")).toString()
}
val nonceDecOpt: Option[Long] = Option(decoded.getNonce).map(_.longValue())
val toAddrOpt: Option[String] = Option(decoded.getTo)
val inputData = Option(decoded.getData).getOrElse("0x")
val estimatedFeeHexOpt =
for {
gp <- Option(decoded.getGasPrice)
gl <- Option(decoded.getGasLimit)
} yield W3Numeric.toHexStringWithPrefix(gp.multiply(gl))
// Fallback: derive chainId from v when not provided by decoder (legacy EIP-155)
val chainIdNumOpt: Option[Long] = chainIdOpt.map(_.longValue()).orElse {
vHexOpt.flatMap { vh =>
val hex = vh.stripPrefix("0x")
if (hex.nonEmpty) {
val vBI = new BigInteger(hex, 16)
if (vBI.compareTo(BigInteger.valueOf(35)) >= 0) {
val parity = if (vBI.testBit(0)) 1L else 0L
Some(
vBI
.subtract(BigInteger.valueOf(35L + parity))
.divide(BigInteger.valueOf(2L))
.longValue()
)
} else None
} else None
}
}
DecodedTxResponse(
hash = Some(hash),
`type` = txType,
chainId = chainIdNumOpt,
nonce = nonceDecOpt,
gasPrice = gasPriceHexOpt,
gas = gasLimitHexOpt,
to = toAddrOpt,
value = valueDecOpt,
input = Some(inputData),
from = fromOpt,
v = vHexOpt,
r = rHexOpt,
s = sHexOpt,
estimatedFee = estimatedFeeHexOpt
)
}
}

View File

@ -0,0 +1,133 @@
package code.bankconnectors.ethereum
import code.api.util.APIUtil._
import code.api.util.{CallContext, ErrorMessages, NewStyle}
import code.api.v6_0_0.TransactionRequestBodyEthereumJsonV600
import code.bankconnectors._
import code.util.AkkaHttpClient._
import code.util.Helper
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
import net.liftweb.common._
import net.liftweb.json
import net.liftweb.json.JValue
import scala.collection.mutable.ArrayBuffer
/**
* EthereumConnector_vSept2025
* Minimal JSON-RPC based connector to send ETH between two addresses.
*
* Notes
* - Supports two modes:
* 1) If transactionRequestCommonBody.description is a 0x-hex string, use eth_sendRawTransaction
* 2) Otherwise fallback to eth_sendTransaction (requires unlocked accounts, e.g. Anvil)
* - For public RPC providers, prefer locally signed tx + eth_sendRawTransaction
* - BankAccount.accountId.value is expected to hold the 0x Ethereum address
*/
trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
implicit override val nameOfConnector = EthereumConnector_vSept2025.toString
override val messageDocs = ArrayBuffer[MessageDoc]()
private def rpcUrl: String = getPropsValue("ethereum.rpc.url").getOrElse("http://127.0.0.1:8545")
private def ethToWeiHex(amountEth: BigDecimal): String = {
val wei = amountEth.bigDecimal.movePointRight(18).toBigIntegerExact()
"0x" + wei.toString(16)
}
override def makePaymentv210(
fromAccount: BankAccount,
toAccount: BankAccount,
transactionRequestId: TransactionRequestId,
transactionRequestCommonBody: TransactionRequestCommonBodyJSON,
amount: BigDecimal,
description: String,
transactionRequestType: TransactionRequestType,
chargePolicy: String,
callContext: Option[CallContext]
): OBPReturnType[Box[TransactionId]] = {
val from = fromAccount.accountId.value
val to = toAccount.accountId.value
val valueHex = ethToWeiHex(amount)
val maybeRawTx: Option[String] = transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyEthereumJsonV600].params.map(_.trim).filter(s => s.startsWith("0x") && s.length > 2)
val safeFrom = if (from.length > 10) from.take(10) + "..." else from
val safeTo = if (to.length > 10) to.take(10) + "..." else to
val safeVal = if (valueHex.length > 14) valueHex.take(14) + "..." else valueHex
logger.debug(s"EthereumConnector_vSept2025.makePaymentv210 → from=$safeFrom to=$safeTo value=$safeVal url=${rpcUrl}")
val payload = maybeRawTx match {
case Some(raw) =>
s"""
|{
| "jsonrpc":"2.0",
| "method":"eth_sendRawTransaction",
| "params":["$raw"],
| "id":1
|}
|""".stripMargin
case None =>
s"""
|{
| "jsonrpc":"2.0",
| "method":"eth_sendTransaction",
| "params":[{
| "from":"$from",
| "to":"$to",
| "value":"$valueHex"
| }],
| "id":1
|}
|""".stripMargin
}
for {
request <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to build HTTP request", 500, callContext) {prepareHttpRequest(rpcUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), payload)
}
response <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to call Ethereum RPC", 500, callContext) {
makeHttpRequest(request)
}.flatten
body <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to read Ethereum RPC response", 500, callContext) {
response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String)
}.flatten
_ <- Helper.booleanToFuture(ErrorMessages.UnknownError + s" Ethereum RPC returned error: ${response.status.value}", 500, callContext) {
logger.debug(s"EthereumConnector_vSept2025.makePaymentv210 response: $body")
response.status.isSuccess()
}
txIdBox <- {
implicit val formats = json.DefaultFormats
val j: JValue = json.parse(body)
val errorNode = (j \ "error")
if (errorNode != json.JNothing && errorNode != json.JNull) {
val msg = (errorNode \ "message").extractOpt[String].getOrElse("Unknown Ethereum RPC error")
val code = (errorNode \ "code").extractOpt[BigInt].map(_.toString).getOrElse("?")
scala.concurrent.Future.successful(Failure(s"Ethereum RPC error(code=$code): $msg"))
} else {
NewStyle.function.tryons(ErrorMessages.InvalidJsonFormat + " Failed to parse Ethereum RPC response", 500, callContext) {
val resultHashOpt: Option[String] =
(j \ "result").extractOpt[String]
.orElse((j \ "result" \ "hash").extractOpt[String])
.orElse((j \ "result" \ "transactionHash").extractOpt[String])
resultHashOpt match {
case Some(hash) if hash.nonEmpty => TransactionId(hash)
case _ => throw new RuntimeException("Empty transaction hash")
}
}.map(Full(_))
}
}
} yield {
(txIdBox, callContext)
}
}
}
object EthereumConnector_vSept2025 extends EthereumConnector_vSept2025

View File

@ -1961,5 +1961,21 @@
<CcyNbr>null</CcyNbr>
<CcyMnrUnts>0</CcyMnrUnts> <!-- Lovelace is the basic unit, no smaller subdivision -->
</CcyNtry>
<!-- Ethereum (ETH) -->
<CcyNtry>
<CtryNm>Ethereum_ETH</CtryNm>
<CcyNm>ETH</CcyNm>
<Ccy>ETH</Ccy>
<CcyNbr>null</CcyNbr>
<CcyMnrUnts>18</CcyMnrUnts> <!-- 1 ETH = 10^18 wei -->
</CcyNtry>
<!-- Wei (the smallest unit of ETH) -->
<CcyNtry>
<CtryNm>Ethereum_wei</CtryNm>
<CcyNm>wei</CcyNm>
<Ccy>wei</Ccy>
<CcyNbr>null</CcyNbr>
<CcyMnrUnts>0</CcyMnrUnts> <!-- wei is base unit -->
</CcyNtry>
</CcyTbl>
</ISO_4217>

View File

@ -0,0 +1,53 @@
package code.bankconnectors.ethereum
import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers}
class DecodeRawTxTest extends FeatureSpec with Matchers with GivenWhenThen {
feature("Decode raw Ethereum transaction to case class") {
scenario("Decode a legacy signed raw transaction successfully") {
Given("a sample legacy signed raw transaction hex string")
// {
// "hash": "0x9d1afb5bd997d69a0fb2cb1bf1cf159f3448e7968fa25df1c26b368d9030b0c3",
// "type": 0,
// "chainId": 2025,
// "nonce": 23,
// "gasPrice": "0x03e8",
// "gas": "0x5208",
// "to": "0x627306090abab3a6e1400e9345bc60c78a8bef57",
// "value": "0x0de0b6b3a7640000",
// "input": "0x",
// "from": "0xf17f52151ebef6c7334fad080c5704d77216b732",
// "v": "0xff6",
// "r": "0x16878a008fb817df6d771749336fa0c905ec5b7fafcd043f0d9e609a2b5e41e0",
// "s": "0x611dbe0f2ee2428360c72f4287a2996cb0d45cb8995cc23eb6ba525cb9580e02",
// "estimatedFee": "0x01406f40"
// }
val rawTx = "0xf86b178203e882520894627306090abab3a6e1400e9345bc60c78a8bef57880de0b6b3a764000080820ff6a016878a008fb817df6d771749336fa0c905ec5b7fafcd043f0d9e609a2b5e41e0a0611dbe0f2ee2428360c72f4287a2996cb0d45cb8995cc23eb6ba525cb9580e02"
When("we decode it to DecodedTxResponse case class")
val response = DecodeRawTx.decodeRawTxToJson(rawTx)
Then("the response should contain the expected transaction fields")
response.hash shouldBe defined
response.hash.get should startWith ("0x")
response.`type` shouldBe Some(0)
response.nonce shouldBe Some(23)
response.gasPrice shouldBe Some("0x3e8")
response.gas shouldBe Some("0x5208")
response.from shouldBe Some("0xf17f52151ebef6c7334fad080c5704d77216b732")
response.to shouldBe Some("0x627306090abab3a6e1400e9345bc60c78a8bef57")
response.value shouldBe Some("1")
response.input shouldBe defined
response.input.get should (be ("0x") or be (""))
response.v shouldBe Some("0xff6")
response.r shouldBe defined
response.r.get should startWith ("0x")
response.s shouldBe defined
response.s.get should startWith ("0x")
response.estimatedFee shouldBe defined
response.estimatedFee.get should startWith ("0x")
}
}
}

View File

@ -0,0 +1,116 @@
//package code.connector
//
//import code.api.util.ErrorMessages
//import code.api.v5_1_0.V510ServerSetup
//import code.api.v6_0_0.TransactionRequestBodyEthereumJsonV600
//import code.bankconnectors.ethereum.EthereumConnector_vSept2025
//import com.github.dwickern.macros.NameOf
//import com.openbankproject.commons.model._
//import net.liftweb.common.Full
//import org.scalatest.Tag
//
//import scala.concurrent.Await
//import scala.concurrent.duration._
///**
// * Minimal unit test to invoke makePaymentv210 against local Anvil.
// * Assumptions:
// * - ethereum.rpc.url points to http://127.0.0.1:8545
// * - The RPC allows eth_sendTransaction (Anvil unlocked accounts)
// * - We pass BankAccount stubs with accountId holding 0x addresses
// */
//class EthereumConnector_vSept2025Test extends V510ServerSetup{
//
// object ConnectorTestTag extends Tag(NameOf.nameOfType[EthereumConnector_vSept2025Test])
//
// object StubConnector extends EthereumConnector_vSept2025
//
// private case class StubBankAccount(id: String) extends BankAccount {
// override val accountId: AccountId = AccountId(id)
// override val bankId: BankId = BankId("bank-x")
// override val accountType: String = "checking"
// override val balance: BigDecimal = BigDecimal(0)
// override val currency: String = "ETH"
// override val name: String = "stub"
// override val label: String = "stub"
// override val number: String = "stub"
// override val lastUpdate: java.util.Date = new java.util.Date()
// override val accountHolder: String = "stub"
// override val accountRoutings: List[AccountRouting] = Nil
// override def branchId: String = "stub"
// override def accountRules: List[AccountRule] = Nil
// }
//
// feature("Anvil local Ethereum Node, need to start the Anvil, and set `ethereum.rpc.url=http://127.0.0.1:8545` in props, and prepare the from, to account") {
//// setPropsValues("ethereum.rpc.url"-> "https://nkotb.openbankproject.com")
// scenario("successful case", ConnectorTestTag) {
// val from = StubBankAccount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
// val to = StubBankAccount("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
// val amount = BigDecimal("0.0001")
//
// val trxBody = TransactionRequestBodyEthereumJsonV600(
// to = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
// value = AmountOfMoneyJsonV121("ETH", amount.toString),
// description="test"
// )
//
// //// This is only for testing; you can comment it out when the local Anvil is running.
//// val resF = StubConnector.makePaymentv210(
//// from,
//// to,
//// TransactionRequestId(java.util.UUID.randomUUID().toString),
//// trxBody,
//// amount,
//// "test",
//// TransactionRequestType("ETH_SEND_TRANSACTION"),
//// "none",
//// None
//// )
////
//// val res = Await.result(resF, 10.seconds)
//// res._1 shouldBe a[Full[_]]
//// val txId = res._1.openOrThrowException(ErrorMessages.UnknownError)
//// txId.value should startWith("0x")
// }
// }
//
// feature("need to start the Anvil, and set `ethereum.rpc.url=https://nkotb.openbankproject.com` in props, and prepare the from, to accounts and the rawTx") {
//// setPropsValues("ethereum.rpc.url"-> "http://127.0.0.1:8545")
// scenario("successful case", ConnectorTestTag) {
//
// val from = StubBankAccount("0xf17f52151EbEF6C7334FAD080c5704D77216b732")
// val to = StubBankAccount("0x627306090abaB3A6e1400e9345bC60c78a8BEf57")
// val amount = BigDecimal("0.0001")
//
// // Use a fixed rawTx variable for testing eth_sendRawTransaction path (no external params)
// val rawTx = "0xf86a058203e882520894627306090abab3a6e1400e9345bc60c78a8bef57872386f26fc1000080820ff5a06de864bc825c4e976f5c432d0a57e3b3c9e19ec9843b5bb893a78a1389be650ea04fa063aba3984ff97e6454595f170e17e117f046568960aacf96f223c71ca0e2"
//
// val trxBody = TransactionRequestBodyEthereumJsonV600(
// params= Some(rawTx),
// to = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
// value = AmountOfMoneyJsonV121("ETH", amount.toString),
// description="test"
// )
////
//// // Enable integration test against private chain
//// val resF = StubConnector.makePaymentv210(
//// from,
//// to,
//// TransactionRequestId(java.util.UUID.randomUUID().toString),
//// trxBody,
//// amount,
//// "test",
//// TransactionRequestType("ETH_SEND_RAW_TRANSACTION") ,
//// "none",
//// None
//// )
////
//// val res = Await.result(resF, 30.seconds)
//// res._1 shouldBe a [Full[_]]
//// val txId = res._1.openOrThrowException(ErrorMessages.UnknownError)
//// txId.value should startWith ("0x")
// }
// }
//
//}
//
//

View File

@ -114,6 +114,8 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{
object REFUND extends Value
object AGENT_CASH_WITHDRAWAL extends Value
object CARDANO extends Value
object ETH_SEND_TRANSACTION extends Value
object ETH_SEND_RAW_TRANSACTION extends Value
}
sealed trait StrongCustomerAuthentication extends EnumValue

1
project/build.properties Normal file
View File

@ -0,0 +1 @@
sbt.version=1.8.2

8
project/metals.sbt Normal file
View File

@ -0,0 +1,8 @@
// format: off
// DO NOT EDIT! This file is auto-generated.
// This file enables sbt-bloop to create bloop config files.
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.12")
// format: on

10
project/plugins.sbt Normal file
View File

@ -0,0 +1,10 @@
// SBT plugins for OBP project
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.6")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
// Scala compiler plugin for macros (equivalent to paradise plugin in Maven)
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
// SemanticDB for Metals support
addCompilerPlugin("org.scalameta" % "semanticdb-scalac" % "4.13.9" cross CrossVersion.full)

View File

@ -3,6 +3,8 @@
### Most recent changes at top of file
```
Date Commit Action
26/09/2025 77d54c2e Added Ethereum Connector Configuration
Added props ethereum.rpc.url, default is http://127.0.0.1:8545
04/08/2025 d282d266 Enhanced Email Configuration with CommonsEmailWrapper
Replaced Lift Mailer with Apache Commons Email for improved email functionality.
Added comprehensive SMTP configuration options: