mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:06:50 +00:00
feature/Enhance EthereumConnector_vSept2025 to support eth_sendRawTransaction for raw transactions and improve error handling in response parsing
This commit is contained in:
parent
c523fb950a
commit
49daedf030
@ -18,7 +18,9 @@ import scala.collection.mutable.ArrayBuffer
|
||||
* Minimal JSON-RPC based connector to send ETH between two addresses.
|
||||
*
|
||||
* Notes
|
||||
* - This version calls eth_sendTransaction (requires unlocked accounts, e.g. Anvil)
|
||||
* - 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
|
||||
*/
|
||||
@ -51,27 +53,40 @@ trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
|
||||
val to = toAccount.accountId.value
|
||||
val valueHex = ethToWeiHex(amount)
|
||||
|
||||
val maybeRawTx: Option[String] = Option(transactionRequestCommonBody).map(_.description).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 = s"""
|
||||
|{
|
||||
| "jsonrpc":"2.0",
|
||||
| "method":"eth_sendTransaction",
|
||||
| "params":[{
|
||||
| "from":"$from",
|
||||
| "to":"$to",
|
||||
| "value":"$valueHex"
|
||||
| }],
|
||||
| "id":1
|
||||
|}
|
||||
|""".stripMargin
|
||||
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)
|
||||
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) {
|
||||
@ -87,18 +102,29 @@ trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
|
||||
response.status.isSuccess()
|
||||
}
|
||||
|
||||
txId <- NewStyle.function.tryons(ErrorMessages.InvalidJsonFormat + " Failed to parse Ethereum RPC response", 500, callContext) {
|
||||
txIdBox <- {
|
||||
implicit val formats = json.DefaultFormats
|
||||
val j: JValue = json.parse(body)
|
||||
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")
|
||||
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 {
|
||||
(Full(txId), callContext)
|
||||
(txIdBox, callContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
package code.connector
|
||||
|
||||
import code.api.util.ErrorMessages
|
||||
import code.api.v5_1_0.V510ServerSetup
|
||||
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:
|
||||
@ -34,9 +39,10 @@ class EthereumConnector_vSept2025Test extends V510ServerSetup{
|
||||
override def accountRules: List[AccountRule] = Nil
|
||||
}
|
||||
|
||||
feature("Make sure connector follow the obp general rules ") {
|
||||
scenario("OutBound case class should have the same param name with connector method", ConnectorTestTag) {
|
||||
val from = StubBankAccount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
|
||||
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")
|
||||
|
||||
@ -45,23 +51,59 @@ class EthereumConnector_vSept2025Test extends V510ServerSetup{
|
||||
override val description: String = "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("ETHEREUM") ,
|
||||
// "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")
|
||||
// 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("ETHEREUM") ,
|
||||
"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 = "0xf86b178203e882520894627306090abab3a6e1400e9345bc60c78a8bef57880de0b6b3a764000080820ff6a016878a008fb817df6d771749336fa0c905ec5b7fafcd043f0d9e609a2b5e41e0a0611dbe0f2ee2428360c72f4287a2996cb0d45cb8995cc23eb6ba525cb9580e02"
|
||||
val trxBody = new TransactionRequestCommonBodyJSON {
|
||||
override val value: AmountOfMoneyJsonV121 = AmountOfMoneyJsonV121("ETH", amount.toString)
|
||||
// Put rawTx here to trigger eth_sendRawTransaction (connector uses description starting with 0x)
|
||||
override val description: String = rawTx
|
||||
}
|
||||
|
||||
// Enable integration test against private chain
|
||||
val resF = StubConnector.makePaymentv210(
|
||||
from,
|
||||
to,
|
||||
TransactionRequestId(java.util.UUID.randomUUID().toString),
|
||||
trxBody,
|
||||
amount,
|
||||
"test",
|
||||
TransactionRequestType("ETHEREUM") ,
|
||||
"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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user