feature/Add Ethereum transaction request handling and models in API version 6.0.0

This commit is contained in:
hongwei 2025-09-18 22:58:19 +02:00
parent b6173f92a5
commit 01116ebd17
6 changed files with 43 additions and 27 deletions

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

@ -64,7 +64,7 @@ object Connector extends SimpleInjector {
"stored_procedure_vDec2019" -> StoredProcedureConnector_vDec2019,
"rabbitmq_vOct2024" -> RabbitMQConnector_vOct2024,
"cardano_vJun2025" -> CardanoConnector_vJun2025,
"cardano_vSept2025" -> EthereumConnector_vSept2025,
"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

@ -128,7 +128,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)
@ -1414,12 +1414,12 @@ 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 = callContext
@ -1457,8 +1457,8 @@ object LocalMappedConnectorInternal extends MdcLoggable {
Option(transactionRequestBodyEthereum.payment.to).exists(_.nonEmpty)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Ethereum 'to' address must start with 0x and be 42 chars", cc=callContext) {
val a = transactionRequestBodyEthereum.payment.to
a.startsWith("0x") && a.length == 42
val toBody = transactionRequestBodyEthereum.payment.to
toBody.startsWith("0x") && toBody.length == 42
}
_ <- Helper.booleanToFuture(s"$InvalidTransactionRequestCurrency Currency must be 'ETH'", cc=callContext) {
transactionRequestBodyEthereum.value.currency.equalsIgnoreCase("ETH")
@ -1466,19 +1466,19 @@ object LocalMappedConnectorInternal extends MdcLoggable {
// Create or get counterparty using the Ethereum address as secondary routing
(toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty(
name = "ethereum-" + transactionRequestBodyEthereum.payment.to.take(10),
name = "ethereum-" + transactionRequestBodyEthereum.payment.to.take(27),
description = transactionRequestBodyEthereum.description,
currency = transactionRequestBodyEthereum.value.currency,
createdByUserId = u.userId,
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = "",
otherBankRoutingAddress = "",
otherBranchRoutingScheme = "",
otherBranchRoutingAddress = "",
otherAccountRoutingScheme = "",
otherAccountRoutingAddress = "",
otherBankRoutingScheme = ETHEREUM.toString,
otherBankRoutingAddress = transactionRequestBodyEthereum.payment.to,
otherBranchRoutingScheme = ETHEREUM.toString,
otherBranchRoutingAddress = transactionRequestBodyEthereum.payment.to,
otherAccountRoutingScheme = ETHEREUM.toString,
otherAccountRoutingAddress = transactionRequestBodyEthereum.payment.to,
otherAccountSecondaryRoutingScheme = "ethereum",
otherAccountSecondaryRoutingAddress = transactionRequestBodyEthereum.payment.to,
callContext = callContext

View File

@ -90,13 +90,8 @@ trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
txId <- NewStyle.function.tryons(ErrorMessages.InvalidJsonFormat + " Failed to parse Ethereum RPC response", 500, callContext) {
implicit val formats = json.DefaultFormats
val j: JValue = json.parse(body)
val rpcError = (j \\ "error").children.headOption
rpcError.foreach { e =>
val msg = (e \\ "message").values.toString
val code = (e \\ "code").values.toString
throw new RuntimeException(s"Ethereum RPC error(code=$code): $msg")
}
val maybe = (j \\ "result").extractOpt[String]
val maybe = (j \ "result").extractOpt[String]
.orElse((j \ "error" \ "message").extractOpt[String].map(msg => throw new RuntimeException(msg)))
maybe match {
case Some(hash) if hash.nonEmpty => TransactionId(hash)
case _ => throw new RuntimeException("Empty transaction hash")

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>