From cdf1a99006c356dc0edf1ca41cae2a8b9aa75a96 Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Fri, 7 Aug 2020 17:19:05 +0100 Subject: [PATCH 01/39] Initial commit --- .gitignore | 2 ++ build.sbt | 19 +++++++++++++++++ project/build.properties | 1 + src/main/scala/iog/psg/cardano/Main.scala | 26 +++++++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 .gitignore create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 src/main/scala/iog/psg/cardano/Main.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee44a96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +target diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..37c3fef --- /dev/null +++ b/build.sbt @@ -0,0 +1,19 @@ + +scalaVersion := "2.13.3" + +val circeVersion = "0.13.0" +val akkaVersion = "2.6.8" +val akkaHttpVersion = "10.2.0" + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion +) + +javacOptions ++= Seq("-source", "11", "-target", "11") + + +scalacOptions ++= Seq("-unchecked", "-deprecation") diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..a919a9b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.8 diff --git a/src/main/scala/iog/psg/cardano/Main.scala b/src/main/scala/iog/psg/cardano/Main.scala new file mode 100644 index 0000000..9966a75 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/Main.scala @@ -0,0 +1,26 @@ +package iog.psg.cardano + +import akka.actor.typed.ActorSystem +import akka.actor.typed.scaladsl.Behaviors +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.HttpRequest + +object Main { + + val baseUri = "http://localhost:8090/v2/" + val wallet = "wallets" + + implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") + implicit val context = system.executionContext + + def main(args: Array[String]): Unit = { + val response = Http().singleRequest(HttpRequest(uri = baseUri + wallet)) + + response.map(resp => { + println(resp) + System.exit(0) + }) + + } + +} From 7b4840ff1cba2dd70f2486f84813b42451134766 Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Mon, 10 Aug 2020 18:12:04 +0100 Subject: [PATCH 02/39] Develop sanity checking test script. --- build.sbt | 9 +- .../scala/iog/psg/cardano/CardanoApi.scala | 71 ++++++++++ .../iog/psg/cardano/CardanoApiCodec.scala | 133 ++++++++++++++++++ .../psg/cardano/CardanoApiTestScript.scala | 79 +++++++++++ src/main/scala/iog/psg/cardano/Main.scala | 26 ---- 5 files changed, 289 insertions(+), 29 deletions(-) create mode 100644 src/main/scala/iog/psg/cardano/CardanoApi.scala create mode 100644 src/main/scala/iog/psg/cardano/CardanoApiCodec.scala create mode 100644 src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala delete mode 100644 src/main/scala/iog/psg/cardano/Main.scala diff --git a/build.sbt b/build.sbt index 37c3fef..6a507b1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,19 +1,22 @@ scalaVersion := "2.13.3" -val circeVersion = "0.13.0" val akkaVersion = "2.6.8" val akkaHttpVersion = "10.2.0" +val akkaHttpCirce = "1.31.0" +val circeVersion = "0.13.0" libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, "com.typesafe.akka" %% "akka-stream" % akkaVersion, "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, - "com.typesafe.akka" %% "akka-stream" % akkaVersion + "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "io.circe" %% "circe-generic-extras" % circeVersion, + "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, ) javacOptions ++= Seq("-source", "11", "-target", "11") -scalacOptions ++= Seq("-unchecked", "-deprecation") +scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations") diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala new file mode 100644 index 0000000..fbafa32 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -0,0 +1,71 @@ +package iog.psg.cardano + + +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model.HttpMethods._ +import akka.http.scaladsl.model.{HttpRequest, RequestEntity} +import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ +import io.circe.generic.auto._ +import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec} +import iog.psg.cardano.CardanoApiCodec.{CreateRestore, ListAddresses, MnemonicSentence, Payment} + +import scala.concurrent.{ExecutionContext, Future} + + +class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext) { + + private val wallets = s"${baseUriWithPort}wallets" + private val network = s"${baseUriWithPort}network" + implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + + + def listWallets: HttpRequest = HttpRequest(uri = wallets) + + def networkInfo: HttpRequest = HttpRequest(uri = s"${network}/information") + + def createRestoreWallet( + name: String, + passphrase: String, + mnemonicSentence: MnemonicSentence, + addressPoolGap: Option[Int] = None + + ): Future[HttpRequest] = { + + val createRestore = + CreateRestore( + name, + passphrase, + mnemonicSentence.mnemonicSentence, + addressPoolGap + ) + + Marshal(createRestore).to[RequestEntity].map { marshalled => + HttpRequest( + uri = s"${baseUriWithPort}wallets", + method = POST, + entity = marshalled + ) + } + + } + + def listAddresses(listAddr: ListAddresses): Future[HttpRequest] = { + Marshal(listAddr).to[RequestEntity] map { marshalled => + HttpRequest( + uri = s"${wallets}/${listAddr.walletId}/addresses", + method = GET, + entity = marshalled + ) + } + } + + def fundPayments(walletId: String, payments: Seq[Payment]): Future[HttpRequest] = { + Marshal(payments).to[RequestEntity] map { marshalled => + HttpRequest( + uri = s"${wallets}/${walletId}/coin-selections/random", + method = POST, + entity = marshalled + ) + } + } +} diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala new file mode 100644 index 0000000..2a5b986 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -0,0 +1,133 @@ +package iog.psg.cardano + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.model.HttpResponse +import akka.http.scaladsl.server.Directives.as +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.Materializer +import de.heikoseeberger.akkahttpcirce.BaseCirceSupport +import iog.psg.cardano.CardanoApiCodec.NetworkInfo +import spray.json._ +import io.circe.generic.extras._ +import io.circe.syntax._ +import io.circe.parser.decode +import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ +import io.circe.Encoder +import io.circe.generic.auto._ +import io.circe.generic.extras.semiauto.deriveConfiguredEncoder +import io.circe.generic.semiauto.deriveEncoder + +import scala.concurrent.{ExecutionContext, Future} + +object CardanoApiCodec { + + implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + + def dropNulls[A](encoder: Encoder[A]): Encoder[A] = + encoder.mapJson(_.dropNullValues) + + implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) + implicit val createListAddrEntityEncoder: Encoder[ListAddresses] = dropNulls(deriveConfiguredEncoder) + + type AddressFilter = String + val Used: AddressFilter = "used" + val UnUsed: AddressFilter = "unused" + + type SyncStatus = String + val ready: SyncStatus = "ready" + + case class SyncProgress(status: String) + + @ConfiguredJsonCodec case class NetworkTip( + epochNumber: Long, + slotNumber: Long, + height: Option[QuantityUnit]) + + case class NodeTip(height: QuantityUnit) + + + case class ListAddresses(walletId: String, state: Option[AddressFilter]) + + case class Payment(address: String, amount: QuantityUnit) + + trait MnemonicSentence { + val mnemonicSentence: IndexedSeq[String] + } + + case class GenericMnemonicSentence(mnemonicSentence: IndexedSeq[String]) extends MnemonicSentence { + require( + mnemonicSentence.length == 15 || + mnemonicSentence.length == 21 || + mnemonicSentence.length == 24, s"Mnemonic word list must be 15, 21, or 24 long (not ${mnemonicSentence.length})") + } + + object GenericMnemonicSentence { + def apply(mnemonicString: String): GenericMnemonicSentence = + GenericMnemonicSentence(mnemonicString.split(" ").toIndexedSeq) + } + + @ConfiguredJsonCodec case class NextEpoch(epochStartTime: String, epochNumber: Long) + + @ConfiguredJsonCodec case class NetworkInfo( + syncProgress: SyncProgress, + networkTip: NetworkTip, + nodeTip: NodeTip, + nextEpoch: NextEpoch + ) + + @ConfiguredJsonCodec case class CreateRestore( + name: String, + passphrase: String, + mnemonicSentence: IndexedSeq[String], + addressPoolGap: Option[Int] = None + ) { + require( + mnemonicSentence.length == 15 || + mnemonicSentence.length == 21 || + mnemonicSentence.length == 24, s"Mnemonic word list must be 15, 21, or 24 long (not ${mnemonicSentence.length})") + } + + case class QuantityUnit(quantity: Long, unit: String) + + case class Balance(available: QuantityUnit, reward: QuantityUnit, total: QuantityUnit) + case class State(status: String) + + trait Address { + val address: String + val amount: QuantityUnit + } + + case class InAddress(address: String, amount: QuantityUnit, id: String, index: Int) extends Address + case class OutAddress(address: String, amount: QuantityUnit) extends Address + + case class FundPaymentsResponse(inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress]) + + @ConfiguredJsonCodec case class WalletAddress( + id: String, + addressPoolGap: Int, + balance: Balance, + name: String, + state: State, + tip: NetworkTip + ) + + implicit class ResponseOps(resp: HttpResponse)(implicit ec: Materializer) { + + def toNetworkInfoResponse: Future[NetworkInfo] = Unmarshal(resp.entity).to[NetworkInfo] + + def toWalletAddress: Future[WalletAddress] = { + println(resp) + Unmarshal(resp.entity).to[WalletAddress] + } + + def toWalletAddresses: Future[Seq[WalletAddress]] = { + println(resp) + Unmarshal(resp.entity).to[Seq[WalletAddress]] + } + + def toFundPaymentsResponse: Future[FundPaymentsResponse] = + Unmarshal(resp.entity).to[FundPaymentsResponse] + } + + +} diff --git a/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala new file mode 100644 index 0000000..8467683 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -0,0 +1,79 @@ +package iog.psg.cardano + +import akka.actor.typed.ActorSystem +import akka.actor.typed.scaladsl.Behaviors +import akka.http.scaladsl.Http +import CardanoApiCodec._ +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} + +import scala.concurrent.{Await, Awaitable, Future} +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +object CardanoApiTestScript { + + implicit def toFuture(req: HttpRequest): Future[HttpRequest] = Future.successful(req) + + implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") + implicit val context = system.executionContext + implicit val waitForDuration = 5.seconds + + def main(args: Array[String]): Unit = { + + val baseUri = args.headOption.getOrElse(throw new IllegalArgumentException("Pass the base URL to the cardano wallet API as a parameter")) + val walletName = args(1) + + println(s"Using base url '$baseUri''") + println(s"Using base url '$baseUri''") + + val api = new CardanoApi(baseUri) + + val netInfo = makeBlockingRequest(api.networkInfo, _.toNetworkInfoResponse).getOrElse(fail("Failed to get net work info, url correct?")) + + if (netInfo.syncProgress.status == ready) { + val walletAddresses = makeBlockingRequest(api.listWallets, _.toWalletAddresses).get + walletAddresses.foreach(addr => { + println(s"Name: ${addr.name} balance: ${addr.balance}") + println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") + }) + val walletAddress = walletAddresses.headOption.getOrElse { + val mnem = GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") + //val str = "wrestle trumpet visual ivory security reduce property ecology mutual market mimic cancel liquid mention cluster" + println("Generating wallet...") + makeBlockingRequest( + api.createRestoreWallet(walletName, "password", mnem), + _.toWalletAddress).get + } + + val wha = makeBlockingRequest(api.listAddresses( + ListAddresses(walletAddress.id, None) + ), _.toFundPaymentsResponse) + + println(wha) + + } else { + fail(s"Network not ready ${netInfo.syncProgress}") + } + + } + + def fail[T](msg: String): T = throw new RuntimeException(msg) + + def await[T](a: Awaitable[T]): T = Await.result(a, waitForDuration) + + + def makeBlockingRequest[T](reqF: Future[HttpRequest], mapper: HttpResponse => Future[T]): Option[T] = Try { + await( + mapper( + await( + Http().singleRequest(await(reqF))) + ) + ) + } match { + case Failure(e) => + println(e) + None + case Success(v) =>Some(v) + } + +} diff --git a/src/main/scala/iog/psg/cardano/Main.scala b/src/main/scala/iog/psg/cardano/Main.scala deleted file mode 100644 index 9966a75..0000000 --- a/src/main/scala/iog/psg/cardano/Main.scala +++ /dev/null @@ -1,26 +0,0 @@ -package iog.psg.cardano - -import akka.actor.typed.ActorSystem -import akka.actor.typed.scaladsl.Behaviors -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.HttpRequest - -object Main { - - val baseUri = "http://localhost:8090/v2/" - val wallet = "wallets" - - implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") - implicit val context = system.executionContext - - def main(args: Array[String]): Unit = { - val response = Http().singleRequest(HttpRequest(uri = baseUri + wallet)) - - response.map(resp => { - println(resp) - System.exit(0) - }) - - } - -} From 34f8fff5ee46866c0213f95ae679f01002a26f71 Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Tue, 11 Aug 2020 18:34:17 +0100 Subject: [PATCH 03/39] Get some shape on the API, prepare for testing. --- .../scala/iog/psg/cardano/CardanoApi.scala | 191 ++++++++++++++++-- .../iog/psg/cardano/CardanoApiCodec.scala | 101 ++++++--- .../psg/cardano/CardanoApiTestScript.scala | 79 -------- .../psg/cardano/CardanoApiTestScript.scala | 83 ++++++++ 4 files changed, 322 insertions(+), 132 deletions(-) delete mode 100644 src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index fbafa32..47a154d 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -1,27 +1,99 @@ package iog.psg.cardano +import akka.actor.ActorSystem +import akka.http.scaladsl.Http import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model.HttpMethods._ -import akka.http.scaladsl.model.{HttpRequest, RequestEntity} +import akka.http.scaladsl.model.Uri.Query +import akka.http.scaladsl.model._ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ -import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec} -import iog.psg.cardano.CardanoApiCodec.{CreateRestore, ListAddresses, MnemonicSentence, Payment} +import io.circe.generic.extras.Configuration +import iog.psg.cardano.CardanoApi.{CardanoApiRequest, DescendingOrder, Order} +import iog.psg.cardano.CardanoApiCodec._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.util.Try +object CardanoApi { -class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext) { + type CardanoApiResponse[T] = Either[ErrorMessage, T] + + case class CardanoApiRequest[T](request: HttpRequest, entityMapper: HttpEntity.Strict => Future[CardanoApiResponse[T]]) + + sealed trait Order { + val value: String + } + + object AscendingOrder extends Order { + override val value: String = "ascending" + } + + object DescendingOrder extends Order { + override val value: String = "descending" + } + + implicit val maxWaitTime: FiniteDuration = 1.minute + + object CardanoApiOps { + + implicit class FutOp[T](val request: CardanoApiRequest[T]) extends AnyVal { + def toFuture: Future[CardanoApiRequest[T]] = Future.successful(request) + } + + implicit class CardanoApiRequestOps[T](requestF: Future[CardanoApiRequest[T]]) + (implicit ec: ExecutionContext, + as: ActorSystem, + maxToStrictWaitTime: FiniteDuration + ) { + def execute: Future[CardanoApiResponse[T]] = { + requestF flatMap { request => + Http().singleRequest(request.request) flatMap { response => + if (response.status == StatusCodes.Forbidden) { + Future.successful(Left(ErrorMessage("Status code 'Forbidden' causes decoding error due to octet content type", StatusCodes.Forbidden.value))) + } else { + // Load into memory using toStrict + // a. no responses utilise streaming and + // b. the Either unmarshaller requires it + response.entity.toStrict(maxToStrictWaitTime) flatMap { strictEntity => + request.entityMapper(strictEntity) + } + } + } + } + } + + def executeBlocking(implicit maxWaitTime: Duration): Try[CardanoApiResponse[T]] = Try { + Await.result( + execute, + maxWaitTime + ) + } + } + + } + +} + +class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: ActorSystem) { private val wallets = s"${baseUriWithPort}wallets" private val network = s"${baseUriWithPort}network" + implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames - def listWallets: HttpRequest = HttpRequest(uri = wallets) + def listWallets: CardanoApiRequest[Seq[Wallet]] = CardanoApiRequest( + HttpRequest(uri = wallets), + _.toWallets + ) - def networkInfo: HttpRequest = HttpRequest(uri = s"${network}/information") + def networkInfo: CardanoApiRequest[NetworkInfo] = CardanoApiRequest( + HttpRequest(uri = s"${network}/information"), + _.toNetworkInfoResponse + ) def createRestoreWallet( name: String, @@ -29,7 +101,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext) { mnemonicSentence: MnemonicSentence, addressPoolGap: Option[Int] = None - ): Future[HttpRequest] = { + ): Future[CardanoApiRequest[Wallet]] = { val createRestore = CreateRestore( @@ -40,32 +112,107 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext) { ) Marshal(createRestore).to[RequestEntity].map { marshalled => - HttpRequest( - uri = s"${baseUriWithPort}wallets", - method = POST, - entity = marshalled + CardanoApiRequest( + HttpRequest( + uri = s"${baseUriWithPort}wallets", + method = POST, + entity = marshalled + ), + _.toWallet ) } } - def listAddresses(listAddr: ListAddresses): Future[HttpRequest] = { - Marshal(listAddr).to[RequestEntity] map { marshalled => + def listAddresses(walletId: String, + state: Option[AddressFilter]): CardanoApiRequest[Seq[WalletAddressId]] = { + + val baseUri = Uri(s"${wallets}/${walletId}/addresses") + val url = state.map { s => + baseUri.withQuery(Query("state" -> s)) + }.getOrElse(baseUri) + + CardanoApiRequest( HttpRequest( - uri = s"${wallets}/${listAddr.walletId}/addresses", - method = GET, - entity = marshalled + uri = url, + method = GET + ), + _.toWalletAddressIds + ) + + } + + def listTransactions(walletId: String, + start: Option[DateTime] = None, + end: Option[DateTime] = None, + order: Order = DescendingOrder, + minWithdrawal: Int = 1): CardanoApiRequest[Seq[Transaction]] = { + val baseUri = Uri(s"${wallets}/${walletId}/transactions") + + val queries = + Seq("start", "end", "order", "minWithdrawal").zip(Seq(start, end, order, Some(minWithdrawal))) + .collect { + case (queryParamName, Some(dt: DateTime)) => queryParamName -> dt.toIsoDateTimeString() + case (queryParamName, Some(minWith: Int)) => queryParamName -> minWith.toString + } + + val uriWithQueries = baseUri.withQuery(Query(queries: _*)) + CardanoApiRequest( + HttpRequest( + uri = uriWithQueries, + method = GET + ), + _.toWalletTransactions + ) + } + + def createTransaction(walletId: String, + passphrase: String, + payments: Payments, + withdrawal: Option[String] + ): Future[CardanoApiRequest[CreateTransactionResponse]] = { + + val createTx = CreateTransaction(passphrase, payments.payments, withdrawal) + + Marshal(createTx).to[RequestEntity] map { marshalled => + CardanoApiRequest( + HttpRequest( + uri = s"${wallets}/${walletId}/transactions", + method = POST, + entity = marshalled + ), + _.toCreateTransactionResponse ) } } - def fundPayments(walletId: String, payments: Seq[Payment]): Future[HttpRequest] = { + def fundPayments(walletId: String, + payments: Payments): Future[CardanoApiRequest[FundPaymentsResponse]] = { Marshal(payments).to[RequestEntity] map { marshalled => - HttpRequest( - uri = s"${wallets}/${walletId}/coin-selections/random", - method = POST, - entity = marshalled + CardanoApiRequest( + HttpRequest( + uri = s"${wallets}/${walletId}/coin-selections/random", + method = POST, + entity = marshalled + ), + _.toFundPaymentsResponse ) } } + + def getTransaction( + walletId: String, + transactionId: String): CardanoApiRequest[CreateTransactionResponse] = { + + val uri = Uri(s"${wallets}/${walletId}/transactions/${transactionId}") + + CardanoApiRequest( + HttpRequest( + uri = uri, + method = GET + ), + _.toCreateTransactionResponse + ) + } + } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 2a5b986..88c91af 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -1,23 +1,17 @@ package iog.psg.cardano -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model.HttpResponse -import akka.http.scaladsl.server.Directives.as +import akka.http.scaladsl.model.HttpEntity import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer -import de.heikoseeberger.akkahttpcirce.BaseCirceSupport -import iog.psg.cardano.CardanoApiCodec.NetworkInfo -import spray.json._ -import io.circe.generic.extras._ -import io.circe.syntax._ -import io.circe.parser.decode import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.Encoder import io.circe.generic.auto._ +import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder -import io.circe.generic.semiauto.deriveEncoder +import iog.psg.cardano.CardanoApi.CardanoApiResponse -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future object CardanoApiCodec { @@ -27,7 +21,7 @@ object CardanoApiCodec { encoder.mapJson(_.dropNullValues) implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) - implicit val createListAddrEntityEncoder: Encoder[ListAddresses] = dropNulls(deriveConfiguredEncoder) + implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) type AddressFilter = String val Used: AddressFilter = "used" @@ -36,6 +30,8 @@ object CardanoApiCodec { type SyncStatus = String val ready: SyncStatus = "ready" + case class ErrorMessage(message: String, code: String) + case class SyncProgress(status: String) @ConfiguredJsonCodec case class NetworkTip( @@ -45,8 +41,11 @@ object CardanoApiCodec { case class NodeTip(height: QuantityUnit) + case class WalletAddressId(id: String, state: Option[AddressFilter]) + + case class CreateTransaction(passphrase: String, payments: Seq[Payment], withdrawal: Option[String]) - case class ListAddresses(walletId: String, state: Option[AddressFilter]) + case class Payments(payments: Seq[Payment]) case class Payment(address: String, amount: QuantityUnit) @@ -90,6 +89,7 @@ object CardanoApiCodec { case class QuantityUnit(quantity: Long, unit: String) case class Balance(available: QuantityUnit, reward: QuantityUnit, total: QuantityUnit) + case class State(status: String) trait Address { @@ -98,35 +98,74 @@ object CardanoApiCodec { } case class InAddress(address: String, amount: QuantityUnit, id: String, index: Int) extends Address + case class OutAddress(address: String, amount: QuantityUnit) extends Address + @ConfiguredJsonCodec case class StakeAddress(stakeAddress: String, amount: QuantityUnit) + + case class Transaction(inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress]) + case class FundPaymentsResponse(inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress]) - @ConfiguredJsonCodec case class WalletAddress( - id: String, - addressPoolGap: Int, - balance: Balance, - name: String, - state: State, - tip: NetworkTip - ) + @ConfiguredJsonCodec case class Block(slotNumber: Int, epochNumber: Int, height: QuantityUnit) + @ConfiguredJsonCodec case class TimedBlock(time: String, block: Block) + + @ConfiguredJsonCodec case class CreateTransactionResponse( + id: String, + amount: QuantityUnit, + insertedAt: TimedBlock, + pendingSince: TimedBlock, + depth: QuantityUnit, + direction: String, + inputs: Seq[InAddress], + outputs: Seq[OutAddress], + withdrawals: Seq[StakeAddress], + status: String + ) + + @ConfiguredJsonCodec case class Wallet( + id: String, + addressPoolGap: Int, + balance: Balance, + name: String, + state: State, + tip: NetworkTip + ) + + implicit class ResponseOps(strictEntity: HttpEntity.Strict)(implicit ec: Materializer) { + + def toNetworkInfoResponse: Future[CardanoApiResponse[NetworkInfo]] = { + Unmarshal(strictEntity).to[CardanoApiResponse[NetworkInfo]] + } + + def toWallet: Future[CardanoApiResponse[Wallet]] = { + println(strictEntity) + Unmarshal(strictEntity).to[CardanoApiResponse[Wallet]] + } + + def toWallets: Future[CardanoApiResponse[Seq[Wallet]]] = { + println(strictEntity) + Unmarshal(strictEntity).to[CardanoApiResponse[Seq[Wallet]]] + } + - implicit class ResponseOps(resp: HttpResponse)(implicit ec: Materializer) { + def toWalletAddressIds: Future[CardanoApiResponse[Seq[WalletAddressId]]] = { + println(strictEntity) + Unmarshal(strictEntity).to[CardanoApiResponse[Seq[WalletAddressId]]] + } - def toNetworkInfoResponse: Future[NetworkInfo] = Unmarshal(resp.entity).to[NetworkInfo] + def toFundPaymentsResponse: Future[CardanoApiResponse[FundPaymentsResponse]] = { + (Unmarshal(strictEntity).to[Either[ErrorMessage, FundPaymentsResponse]]) + } - def toWalletAddress: Future[WalletAddress] = { - println(resp) - Unmarshal(resp.entity).to[WalletAddress] + def toWalletTransactions: Future[CardanoApiResponse[Seq[Transaction]]] = { + (Unmarshal(strictEntity).to[Either[ErrorMessage, Seq[Transaction]]]) } - def toWalletAddresses: Future[Seq[WalletAddress]] = { - println(resp) - Unmarshal(resp.entity).to[Seq[WalletAddress]] + def toCreateTransactionResponse: Future[CardanoApiResponse[CreateTransactionResponse]] = { + (Unmarshal(strictEntity).to[Either[ErrorMessage, CreateTransactionResponse]]) } - def toFundPaymentsResponse: Future[FundPaymentsResponse] = - Unmarshal(resp.entity).to[FundPaymentsResponse] } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala deleted file mode 100644 index 8467683..0000000 --- a/src/main/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ /dev/null @@ -1,79 +0,0 @@ -package iog.psg.cardano - -import akka.actor.typed.ActorSystem -import akka.actor.typed.scaladsl.Behaviors -import akka.http.scaladsl.Http -import CardanoApiCodec._ -import akka.http.scaladsl.model.{HttpRequest, HttpResponse} - -import scala.concurrent.{Await, Awaitable, Future} -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -object CardanoApiTestScript { - - implicit def toFuture(req: HttpRequest): Future[HttpRequest] = Future.successful(req) - - implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") - implicit val context = system.executionContext - implicit val waitForDuration = 5.seconds - - def main(args: Array[String]): Unit = { - - val baseUri = args.headOption.getOrElse(throw new IllegalArgumentException("Pass the base URL to the cardano wallet API as a parameter")) - val walletName = args(1) - - println(s"Using base url '$baseUri''") - println(s"Using base url '$baseUri''") - - val api = new CardanoApi(baseUri) - - val netInfo = makeBlockingRequest(api.networkInfo, _.toNetworkInfoResponse).getOrElse(fail("Failed to get net work info, url correct?")) - - if (netInfo.syncProgress.status == ready) { - val walletAddresses = makeBlockingRequest(api.listWallets, _.toWalletAddresses).get - walletAddresses.foreach(addr => { - println(s"Name: ${addr.name} balance: ${addr.balance}") - println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") - }) - val walletAddress = walletAddresses.headOption.getOrElse { - val mnem = GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") - //val str = "wrestle trumpet visual ivory security reduce property ecology mutual market mimic cancel liquid mention cluster" - println("Generating wallet...") - makeBlockingRequest( - api.createRestoreWallet(walletName, "password", mnem), - _.toWalletAddress).get - } - - val wha = makeBlockingRequest(api.listAddresses( - ListAddresses(walletAddress.id, None) - ), _.toFundPaymentsResponse) - - println(wha) - - } else { - fail(s"Network not ready ${netInfo.syncProgress}") - } - - } - - def fail[T](msg: String): T = throw new RuntimeException(msg) - - def await[T](a: Awaitable[T]): T = Await.result(a, waitForDuration) - - - def makeBlockingRequest[T](reqF: Future[HttpRequest], mapper: HttpResponse => Future[T]): Option[T] = Try { - await( - mapper( - await( - Http().singleRequest(await(reqF))) - ) - ) - } match { - case Failure(e) => - println(e) - None - case Success(v) =>Some(v) - } - -} diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala new file mode 100644 index 0000000..ae9756c --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -0,0 +1,83 @@ +package iog.psg.cardano + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi.CardanoApiOps._ +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, maxWaitTime} +import iog.psg.cardano.CardanoApiCodec.{ErrorMessage, GenericMnemonicSentence, Payment, Payments, QuantityUnit, ready} + +import scala.util.{Failure, Success, Try} + +object CardanoApiTestScript { + + private implicit val system = ActorSystem("SingleRequest") + private implicit val context = system.dispatcher + //implicit val waitForDuration = 5.seconds + + def main(args: Array[String]): Unit = { + + val baseUri = args.headOption.getOrElse(throw new IllegalArgumentException("Pass the base URL to the cardano wallet API as a parameter")) + val walletName = args(1) + + println(s"Using base url '$baseUri''") + println(s"Using wallet name '$walletName''") + + val api = new CardanoApi(baseUri) + + val netInfo = unwrap(api.networkInfo.toFuture.executeBlocking) + + println(netInfo) + + if (netInfo.syncProgress.status == ready) { + val walletAddresses = unwrap(api.listWallets.toFuture.executeBlocking) + + walletAddresses.foreach(addr => { + println(s"Name: ${addr.name} balance: ${addr.balance}") + println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") + }) + val walletAddress = walletAddresses.headOption.getOrElse { + val mnem = GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") + val mnem24 = GenericMnemonicSentence("enforce chicken cactus pupil wagon brother stuff pumpkin hobby noble genius fish air only sign hour apart fruit market acid beach top subway swear") + //val str = "wrestle trumpet visual ivory security reduce property ecology mutual market mimic cancel liquid mention cluster" + println("Generating wallet...") + unwrap(api.createRestoreWallet(walletName, "password", mnem24).executeBlocking) + } + + val allWalletAddresses = + unwrap(api.listAddresses(walletAddress.id, None).toFuture.executeBlocking) + + println(allWalletAddresses) + + val payments = Payments( + Seq( + Payment(allWalletAddresses.head.id, QuantityUnit(1, "lovelace")) + ) + ) + + val paymentsResponse = + unwrapOpt(api.fundPayments(walletAddress.id, payments).executeBlocking) + + + println(paymentsResponse) + + } else { + fail(s"Network not ready ${netInfo.syncProgress}") + } + + } + + private def unwrap[T](apiResult: Try[CardanoApiResponse[T]]): T = unwrapOpt(apiResult).get + + private def unwrapOpt[T](apiResult: Try[CardanoApiResponse[T]]): Option[T] = apiResult match { + case Success(Left(ErrorMessage(message, code))) => + println(s"API Error message $message, code $code") + None + case Success(Right(t: T)) => Some(t) + case Failure(exception) => + println(exception) + None + } + + private def fail[T](msg: String): T = throw new RuntimeException(msg) + + +} From 1f040145e2481f09c925f14abce8501159f53c6d Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Thu, 13 Aug 2020 21:43:09 +0100 Subject: [PATCH 04/39] Backup. --- build.sbt | 5 + .../scala/iog/psg/cardano/CardanoApi.scala | 148 ++++++--- .../iog/psg/cardano/CardanoApiCodec.scala | 292 +++++++++++++----- .../iog/psg/cardano/CardanoApiMain.scala | 197 ++++++++++++ .../iog/psg/cardano/util/ArgumentParser.scala | 38 +++ .../scala/iog/psg/cardano/util/Trace.scala | 61 ++++ .../psg/cardano/CardanoApiTestScript.scala | 185 ++++++++--- .../psg/cardano/util/ArgumentParserSpec.scala | 39 +++ 8 files changed, 819 insertions(+), 146 deletions(-) create mode 100644 src/main/scala/iog/psg/cardano/CardanoApiMain.scala create mode 100644 src/main/scala/iog/psg/cardano/util/ArgumentParser.scala create mode 100644 src/main/scala/iog/psg/cardano/util/Trace.scala create mode 100644 src/test/scala/iog/psg/cardano/util/ArgumentParserSpec.scala diff --git a/build.sbt b/build.sbt index 6a507b1..4e5ecd6 100644 --- a/build.sbt +++ b/build.sbt @@ -6,6 +6,10 @@ val akkaHttpVersion = "10.2.0" val akkaHttpCirce = "1.31.0" val circeVersion = "0.13.0" +/** + * Don't include a logger binding as this is a library for embedding + * http://www.slf4j.org/codes.html#StaticLoggerBinder + */ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, "com.typesafe.akka" %% "akka-stream" % akkaVersion, @@ -14,6 +18,7 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-stream" % akkaVersion, "io.circe" %% "circe-generic-extras" % circeVersion, "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, + "org.scalatest" %% "scalatest" % "3.1.2" % Test ) javacOptions ++= Seq("-source", "11", "-target", "11") diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 47a154d..706b5cc 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -1,41 +1,45 @@ package iog.psg.cardano +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model.ContentType.{Binary, WithFixedCharset} import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.Uri.Query import akka.http.scaladsl.model._ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ import io.circe.generic.extras.Configuration -import iog.psg.cardano.CardanoApi.{CardanoApiRequest, DescendingOrder, Order} -import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.CardanoApi.Order.Order +import iog.psg.cardano.CardanoApiCodec.toErrorMessage import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} import scala.concurrent.{Await, ExecutionContext, Future} import scala.util.Try +/** + * Defines the API which wraps the Cardano API, depends on CardanoApiCodec for it's implementation, + * so clients will import the Codec also. + */ object CardanoApi { + case class ErrorMessage(message: String, code: String) + type CardanoApiResponse[T] = Either[ErrorMessage, T] - case class CardanoApiRequest[T](request: HttpRequest, entityMapper: HttpEntity.Strict => Future[CardanoApiResponse[T]]) + case class CardanoApiRequest[T](request: HttpRequest, mapper: HttpResponse => Future[CardanoApiResponse[T]]) - sealed trait Order { - val value: String + object Order extends Enumeration { + type Order = Value + val ascendingOrder = Value("ascending") + val descendingOrder = Value("descending") } - object AscendingOrder extends Order { - override val value: String = "ascending" - } - - object DescendingOrder extends Order { - override val value: String = "descending" - } - - implicit val maxWaitTime: FiniteDuration = 1.minute + implicit val defaultMaxWaitTime: FiniteDuration = 15.seconds object CardanoApiOps { @@ -50,18 +54,9 @@ object CardanoApi { ) { def execute: Future[CardanoApiResponse[T]] = { requestF flatMap { request => - Http().singleRequest(request.request) flatMap { response => - if (response.status == StatusCodes.Forbidden) { - Future.successful(Left(ErrorMessage("Status code 'Forbidden' causes decoding error due to octet content type", StatusCodes.Forbidden.value))) - } else { - // Load into memory using toStrict - // a. no responses utilise streaming and - // b. the Either unmarshaller requires it - response.entity.toStrict(maxToStrictWaitTime) flatMap { strictEntity => - request.entityMapper(strictEntity) - } - } - } + Http() + .singleRequest(request.request) + .flatMap(request.mapper) } } @@ -79,6 +74,10 @@ object CardanoApi { class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: ActorSystem) { + import iog.psg.cardano.CardanoApi._ + import iog.psg.cardano.CardanoApiCodec._ + import AddressFilter.AddressFilter + private val wallets = s"${baseUriWithPort}wallets" private val network = s"${baseUriWithPort}network" @@ -86,12 +85,26 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act def listWallets: CardanoApiRequest[Seq[Wallet]] = CardanoApiRequest( - HttpRequest(uri = wallets), + HttpRequest( + uri = wallets, + method = GET + ), _.toWallets ) + def getWallet(walletId: String): CardanoApiRequest[Wallet] = CardanoApiRequest( + HttpRequest( + uri = s"${wallets}/$walletId", + method = GET + ), + _.toWallet + ) + def networkInfo: CardanoApiRequest[NetworkInfo] = CardanoApiRequest( - HttpRequest(uri = s"${network}/information"), + HttpRequest( + uri = s"${network}/information", + method = GET + ), _.toNetworkInfoResponse ) @@ -114,7 +127,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act Marshal(createRestore).to[RequestEntity].map { marshalled => CardanoApiRequest( HttpRequest( - uri = s"${baseUriWithPort}wallets", + uri = s"$wallets", method = POST, entity = marshalled ), @@ -128,8 +141,9 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act state: Option[AddressFilter]): CardanoApiRequest[Seq[WalletAddressId]] = { val baseUri = Uri(s"${wallets}/${walletId}/addresses") + val url = state.map { s => - baseUri.withQuery(Query("state" -> s)) + baseUri.withQuery(Query("state" -> s.toString)) }.getOrElse(baseUri) CardanoApiRequest( @@ -143,16 +157,17 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } def listTransactions(walletId: String, - start: Option[DateTime] = None, - end: Option[DateTime] = None, - order: Order = DescendingOrder, + start: Option[ZonedDateTime] = None, + end: Option[ZonedDateTime] = None, + order: Order = Order.descendingOrder, minWithdrawal: Int = 1): CardanoApiRequest[Seq[Transaction]] = { val baseUri = Uri(s"${wallets}/${walletId}/transactions") val queries = Seq("start", "end", "order", "minWithdrawal").zip(Seq(start, end, order, Some(minWithdrawal))) .collect { - case (queryParamName, Some(dt: DateTime)) => queryParamName -> dt.toIsoDateTimeString() + case (queryParamName, Some(o: Order)) => queryParamName -> o.toString + case (queryParamName, Some(dt: ZonedDateTime)) => queryParamName -> zonedDateToString(dt) case (queryParamName, Some(minWith: Int)) => queryParamName -> minWith.toString } @@ -166,10 +181,10 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act ) } - def createTransaction(walletId: String, - passphrase: String, - payments: Payments, - withdrawal: Option[String] + def createTransaction(fromWalletId: String, + passphrase: String, + payments: Payments, + withdrawal: Option[String] ): Future[CardanoApiRequest[CreateTransactionResponse]] = { val createTx = CreateTransaction(passphrase, payments.payments, withdrawal) @@ -177,7 +192,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act Marshal(createTx).to[RequestEntity] map { marshalled => CardanoApiRequest( HttpRequest( - uri = s"${wallets}/${walletId}/transactions", + uri = s"${wallets}/${fromWalletId}/transactions", method = POST, entity = marshalled ), @@ -186,6 +201,25 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } + def estimateFee(fromWalletId: String, + payments: Payments, + withdrawal: String = "self" + ): Future[CardanoApiRequest[EstimateFeeResponse]] = { + + val estimateFees = EstimateFee(payments.payments, withdrawal) + + Marshal(estimateFees).to[RequestEntity] map { marshalled => + CardanoApiRequest( + HttpRequest( + uri = s"${wallets}/${fromWalletId}/payment-fees", + method = POST, + entity = marshalled + ), + _.toEstimateFeeResponse + ) + } + } + def fundPayments(walletId: String, payments: Payments): Future[CardanoApiRequest[FundPaymentsResponse]] = { Marshal(payments).to[RequestEntity] map { marshalled => @@ -215,4 +249,40 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act ) } + def updatePassphrase( + walletId: String, + oldPassphrase: String, + newPassphrase: String): Future[CardanoApiRequest[Unit]] = { + + val uri = Uri(s"${wallets}/${walletId}/passphrase") + val updater = UpdatePassphrase(oldPassphrase, newPassphrase) + + Marshal(updater).to[RequestEntity] map { marshalled => { + CardanoApiRequest( + HttpRequest( + uri = uri, + method = PUT, + entity = marshalled + ), + _.toUnit + ) + } + } + } + + def deleteWallet( + walletId: String + ): CardanoApiRequest[Unit] = { + + val uri = Uri(s"${wallets}/${walletId}") + + CardanoApiRequest( + HttpRequest( + uri = uri, + method = DELETE, + ), + _.toUnit + ) + } + } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 88c91af..6607c4a 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -1,17 +1,29 @@ package iog.psg.cardano -import akka.http.scaladsl.model.HttpEntity +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import akka.http.scaladsl.model.ContentType.{Binary, WithFixedCharset} +import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaTypes, StatusCodes} import akka.http.scaladsl.unmarshalling.Unmarshal import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer +import akka.util.ByteString import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ -import io.circe.Encoder +import io.circe.{Decoder, Encoder, Json} import io.circe.generic.auto._ import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder -import iog.psg.cardano.CardanoApi.CardanoApiResponse +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} +import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter +import iog.psg.cardano.CardanoApiCodec.SyncState.SyncState +import iog.psg.cardano.CardanoApiCodec.TxDirection.TxDirection +import iog.psg.cardano.CardanoApiCodec.TxState.TxState +import iog.psg.cardano.CardanoApiCodec.Units.Units -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success, Try} object CardanoApiCodec { @@ -23,16 +35,46 @@ object CardanoApiCodec { implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) - type AddressFilter = String - val Used: AddressFilter = "used" - val UnUsed: AddressFilter = "unused" + implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => + stringToZonedDate(s) match { + case Success(goodDateTime) => Right(goodDateTime) + case Failure(exception) => Left(exception.toString) + } + } + + implicit val decodeUnits: Decoder[Units] = Decoder.decodeString.map(Units.withName) + implicit val encodeUnits: Encoder[Units] = (a: Units) => Json.fromString(a.toString) + + implicit val decodeSyncState: Decoder[SyncState] = Decoder.decodeString.map(SyncState.withName) + implicit val encodeSyncState: Encoder[SyncState] = (a: SyncState) => Json.fromString(a.toString) + + implicit val decodeAddressFilter: Decoder[AddressFilter] = Decoder.decodeString.map(AddressFilter.withName) + implicit val encodeAddressFilter: Encoder[AddressFilter] = (a: AddressFilter) => Json.fromString(a.toString) + + implicit val decodeTxState: Decoder[TxState] = Decoder.decodeString.map(TxState.withName) + implicit val encodeTxState: Encoder[TxState] = (a: TxState) => Json.fromString(a.toString) + + implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) + implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) - type SyncStatus = String - val ready: SyncStatus = "ready" + object AddressFilter extends Enumeration { + type AddressFilter = Value - case class ErrorMessage(message: String, code: String) + val used = Value("used") + val unUsed = Value("unused") + + } + + case class SyncStatus(status: SyncState, progress: Option[QuantityUnit]) + + object SyncState extends Enumeration { + type SyncState = Value + val ready: SyncState = Value("ready") + val syncing: SyncState = Value("syncing") + val notResponding: SyncState = Value("not_responding") + + } - case class SyncProgress(status: String) @ConfiguredJsonCodec case class NetworkTip( epochNumber: Long, @@ -43,12 +85,16 @@ object CardanoApiCodec { case class WalletAddressId(id: String, state: Option[AddressFilter]) - case class CreateTransaction(passphrase: String, payments: Seq[Payment], withdrawal: Option[String]) + private[cardano] case class CreateTransaction(passphrase: String, payments: Seq[Payment], withdrawal: Option[String]) + + private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String) case class Payments(payments: Seq[Payment]) case class Payment(address: String, amount: QuantityUnit) + @ConfiguredJsonCodec case class UpdatePassphrase(oldPassphrase: String, newPassphrase: String) + trait MnemonicSentence { val mnemonicSentence: IndexedSeq[String] } @@ -65,16 +111,19 @@ object CardanoApiCodec { GenericMnemonicSentence(mnemonicString.split(" ").toIndexedSeq) } - @ConfiguredJsonCodec case class NextEpoch(epochStartTime: String, epochNumber: Long) + @ConfiguredJsonCodec + case class NextEpoch(epochStartTime: ZonedDateTime, epochNumber: Long) - @ConfiguredJsonCodec case class NetworkInfo( - syncProgress: SyncProgress, + @ConfiguredJsonCodec + case class NetworkInfo( + syncProgress: SyncStatus, networkTip: NetworkTip, nodeTip: NodeTip, nextEpoch: NextEpoch ) - @ConfiguredJsonCodec case class CreateRestore( + @ConfiguredJsonCodec + case class CreateRestore( name: String, passphrase: String, mnemonicSentence: IndexedSeq[String], @@ -86,84 +135,183 @@ object CardanoApiCodec { mnemonicSentence.length == 24, s"Mnemonic word list must be 15, 21, or 24 long (not ${mnemonicSentence.length})") } - case class QuantityUnit(quantity: Long, unit: String) + object Units extends Enumeration { + type Units = Value + val percent = Value("percent") + val lovelace = Value("lovelace") + val block = Value("block") + } - case class Balance(available: QuantityUnit, reward: QuantityUnit, total: QuantityUnit) - case class State(status: String) + object TxDirection extends Enumeration { + type TxDirection = Value + val outgoing = Value("outgoing") + val incoming = Value("incoming") + } - trait Address { - val address: String - val amount: QuantityUnit + object TxState extends Enumeration { + type TxState = Value + val pending = Value("pending") + val inLedger = Value("in_ledger") } - case class InAddress(address: String, amount: QuantityUnit, id: String, index: Int) extends Address + case class QuantityUnit( + quantity: Long, + unit: Units + ) + + case class Balance( + available: QuantityUnit, + reward: QuantityUnit, + total: QuantityUnit + ) + + case class InAddress( + address: Option[String], + amount: Option[QuantityUnit], + id: String, + index: Int) + + case class OutAddress( + address: String, + amount: QuantityUnit + ) + + @ConfiguredJsonCodec + case class StakeAddress( + stakeAddress: String, + amount: QuantityUnit + ) + + case class Transaction( + inputs: IndexedSeq[InAddress], + outputs: Seq[OutAddress] + ) + + case class FundPaymentsResponse( + inputs: IndexedSeq[InAddress], + outputs: Seq[OutAddress] + ) + + @ConfiguredJsonCodec + case class Block( + slotNumber: Int, + epochNumber: Int, + height: QuantityUnit + ) + + @ConfiguredJsonCodec + case class TimedBlock( + time: ZonedDateTime, + block: Block + ) + + + @ConfiguredJsonCodec + case class EstimateFeeResponse( + estimatedMin: QuantityUnit, + estimatedMax: QuantityUnit + ) + + @ConfiguredJsonCodec + case class CreateTransactionResponse( + id: String, + amount: QuantityUnit, + insertedAt: Option[TimedBlock], + pendingSince: Option[TimedBlock], + depth: Option[QuantityUnit], + direction: TxDirection, + inputs: Seq[InAddress], + outputs: Seq[OutAddress], + withdrawals: Seq[StakeAddress], + status: TxState + ) + + @ConfiguredJsonCodec + case class Wallet( + id: String, + addressPoolGap: Int, + balance: Balance, + name: String, + state: SyncStatus, + tip: NetworkTip + ) + + def stringToZonedDate(dateAsString: String): Try[ZonedDateTime] = { + Try(ZonedDateTime.parse(dateAsString, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + } - case class OutAddress(address: String, amount: QuantityUnit) extends Address + def zonedDateToString(zonedDateTime: ZonedDateTime): String = { + zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + } - @ConfiguredJsonCodec case class StakeAddress(stakeAddress: String, amount: QuantityUnit) + def toErrorMessage(bs: ByteString): Either[io.circe.Error, ErrorMessage] = { + import io.circe.parser.decode + decode[ErrorMessage](bs.utf8String) + } - case class Transaction(inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress]) + implicit class ResponseOps(response: HttpResponse)( + implicit mat: Materializer, + timeout: FiniteDuration, + ec: ExecutionContext) { - case class FundPaymentsResponse(inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress]) + private def strictEntity: Future[HttpEntity.Strict] = response.entity.toStrict(timeout) - @ConfiguredJsonCodec case class Block(slotNumber: Int, epochNumber: Int, height: QuantityUnit) - @ConfiguredJsonCodec case class TimedBlock(time: String, block: Block) - @ConfiguredJsonCodec case class CreateTransactionResponse( - id: String, - amount: QuantityUnit, - insertedAt: TimedBlock, - pendingSince: TimedBlock, - depth: QuantityUnit, - direction: String, - inputs: Seq[InAddress], - outputs: Seq[OutAddress], - withdrawals: Seq[StakeAddress], - status: String - ) + private def extractErrorResponse[T](strictEntity: Future[HttpEntity.Strict]): Future[CardanoApiResponse[T]] = { + strictEntity.map(e => toErrorMessage(e.data) match { + case Left(err) => Left(ErrorMessage(err.getMessage, "UNPARSEABLE RESULT")) + case Right(v) => Left(v) + }) - @ConfiguredJsonCodec case class Wallet( - id: String, - addressPoolGap: Int, - balance: Balance, - name: String, - state: State, - tip: NetworkTip - ) + } - implicit class ResponseOps(strictEntity: HttpEntity.Strict)(implicit ec: Materializer) { + def toNetworkInfoResponse: Future[CardanoApiResponse[NetworkInfo]] + = to[NetworkInfo](Unmarshal(_).to[CardanoApiResponse[NetworkInfo]]) - def toNetworkInfoResponse: Future[CardanoApiResponse[NetworkInfo]] = { - Unmarshal(strictEntity).to[CardanoApiResponse[NetworkInfo]] - } - def toWallet: Future[CardanoApiResponse[Wallet]] = { - println(strictEntity) - Unmarshal(strictEntity).to[CardanoApiResponse[Wallet]] - } + def to[T](f: HttpEntity.Strict => Future[CardanoApiResponse[T]]): Future[CardanoApiResponse[T]] = { - def toWallets: Future[CardanoApiResponse[Seq[Wallet]]] = { - println(strictEntity) - Unmarshal(strictEntity).to[CardanoApiResponse[Seq[Wallet]]] + response.entity.contentType match { + case WithFixedCharset(MediaTypes.`application/json`) => + // Load into memory using toStrict + // a. no responses utilise streaming and + // b. the Either unmarshaller requires it + strictEntity.flatMap(f) + + case Binary(MediaTypes.`application/octet-stream`) => + extractErrorResponse[T](strictEntity) + } } + def toWallet: Future[CardanoApiResponse[Wallet]] + = to[Wallet](Unmarshal(_).to[CardanoApiResponse[Wallet]]) - def toWalletAddressIds: Future[CardanoApiResponse[Seq[WalletAddressId]]] = { - println(strictEntity) - Unmarshal(strictEntity).to[CardanoApiResponse[Seq[WalletAddressId]]] - } + def toWallets: Future[CardanoApiResponse[Seq[Wallet]]] + = to[Seq[Wallet]](Unmarshal(_).to[CardanoApiResponse[Seq[Wallet]]]) - def toFundPaymentsResponse: Future[CardanoApiResponse[FundPaymentsResponse]] = { - (Unmarshal(strictEntity).to[Either[ErrorMessage, FundPaymentsResponse]]) - } - def toWalletTransactions: Future[CardanoApiResponse[Seq[Transaction]]] = { - (Unmarshal(strictEntity).to[Either[ErrorMessage, Seq[Transaction]]]) - } + def toWalletAddressIds: Future[CardanoApiResponse[Seq[WalletAddressId]]] + = to[Seq[WalletAddressId]](Unmarshal(_).to[CardanoApiResponse[Seq[WalletAddressId]]]) + + def toFundPaymentsResponse: Future[CardanoApiResponse[FundPaymentsResponse]] + = to[FundPaymentsResponse](Unmarshal(_).to[CardanoApiResponse[FundPaymentsResponse]]) + + def toWalletTransactions: Future[CardanoApiResponse[Seq[Transaction]]] + = to[Seq[Transaction]](Unmarshal(_).to[CardanoApiResponse[Seq[Transaction]]]) + + def toCreateTransactionResponse: Future[CardanoApiResponse[CreateTransactionResponse]] + = to[CreateTransactionResponse](Unmarshal(_).to[CardanoApiResponse[CreateTransactionResponse]]) + + def toEstimateFeeResponse: Future[CardanoApiResponse[EstimateFeeResponse]] = + to[EstimateFeeResponse](Unmarshal(_).to[CardanoApiResponse[EstimateFeeResponse]]) - def toCreateTransactionResponse: Future[CardanoApiResponse[CreateTransactionResponse]] = { - (Unmarshal(strictEntity).to[Either[ErrorMessage, CreateTransactionResponse]]) + def toUnit: Future[CardanoApiResponse[Unit]] = { + if (response.status == StatusCodes.NoContent) { + Future.successful(Right(())) + } else { + extractErrorResponse[Unit](strictEntity) + } } } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala new file mode 100644 index 0000000..b6d1584 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -0,0 +1,197 @@ +package iog.psg.cardano + +import java.io.File + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestOps, FutOp} +import iog.psg.cardano.CardanoApi.{Order, CardanoApiResponse, ErrorMessage, defaultMaxWaitTime} +import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} +import iog.psg.cardano.util.{ArgumentParser, ConsoleTrace, FileTrace, NoOpTrace, Trace} + +import scala.util.{Failure, Success, Try} + +object CardanoApiMain { + + object CmdLine { + val help = "-help" + val traceToFile = "-trace" + val noConsole = "-noConsole" + val netInfo = "-netInfo" + val baseUrl = "-baseUrl" + val listWallets = "-wallets" + val createWallet = "-createWallet" + val restoreWallet = "-restoreWallet" + val name = "-name" + val passphrase = "-passphrase" + val mnemonic = "-mnemonic" + val addressPoolGap = "-addressPoolGap" + val listWalletAddresses = "-listAddresses" + val listWalletTransactions = "-listTxs" + val state = "-state" + val walletId = "-walletId" + val start = "-start" + val end = "-end" + val order = "-order" + val minWithdrawal = "-minWithdrawal" + val createTx = "-createTx" + val fundTx = "-fundTx" + val getTx = "-getTx" + val amount = "-amount" + val address = "-address" + + } + + val defaultBaseUrl = "http://127.0.0.1:8090/v2/" + val defaultTraceFile = "cardano-api.log" + + def main(args: Array[String]): Unit = { + val arguments = new ArgumentParser(args) + + if (arguments.noArgs || arguments.contains(CmdLine.help)) { + showHelp() + } else { + val conTracer = if (arguments.contains(CmdLine.noConsole)) NoOpTrace else ConsoleTrace + + implicit val trace = conTracer.withTrace( + if (arguments.contains(CmdLine.traceToFile)) { + val fileName = arguments(CmdLine.traceToFile).getOrElse(defaultTraceFile) + new FileTrace(new File(fileName)) + } else NoOpTrace + ) + + + /*def getArgument(arg: String): String = { + arguments(name).getOrElse { + val msg = s"No value provided for $arg" + throw new IllegalArgumentException(msg) + } + }*/ + + def hasArgument(arg: String): Boolean = { + val result = arguments.contains(arg) + if (result) trace(arg) + result + } + + def hasArgumentWithValue(arg: String): Boolean = { + val result = arguments(arg).isDefined + if (result) trace(arg) + result + } + + implicit val system = ActorSystem("SingleRequest") + implicit val context = system.dispatcher + + Try { + + val url = arguments(CmdLine.baseUrl).getOrElse(defaultBaseUrl) + + trace(s"baseurl:$url") + + val api = new CardanoApi(url) + + if (hasArgument(CmdLine.netInfo)) { + val result = unwrap(api.networkInfo.toFuture.executeBlocking) + trace(result.toString) + } else if (hasArgument(CmdLine.listWallets)) { + val result = unwrap(api.listWallets.toFuture.executeBlocking) + result.foreach(trace.apply) + + } else if (hasArgument(CmdLine.listWalletAddresses)) { + val walletId = arguments.get(CmdLine.walletId) + val addrState = arguments(CmdLine.state).map(AddressFilter.withName) + val result = unwrap(api.listAddresses(walletId, addrState).toFuture.executeBlocking) + result.foreach(trace.apply) + + } else if (hasArgument(CmdLine.getTx)) { + val walletId = arguments.get(CmdLine.walletId) + val txId = arguments.get(CmdLine.getTx) + val result = unwrap(api.getTransaction(walletId, txId).toFuture.executeBlocking) + trace(result) + + } else if (hasArgument(CmdLine.createTx)) { + } else if (hasArgument(CmdLine.fundTx)) { + val walletId = arguments.get(CmdLine.walletId) + val amount = arguments.get(CmdLine.amount).toLong + val addr = arguments.get(CmdLine.address) + val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) + val payments = Payments(Seq(singlePayment)) + val result = unwrap(api.fundPayments( + walletId, + payments + ).executeBlocking) + trace(result) + + + } else if (hasArgument(CmdLine.listWalletTransactions)) { + val walletId = arguments.get(CmdLine.walletId) + //val startDate = arguments(start) + //val endDate = arguments(end) + val orderOf = arguments(CmdLine.order).flatMap(s => Try(Order.withName(s)).toOption).getOrElse(Order.descendingOrder) + val minWithdrawalTx = arguments(CmdLine.minWithdrawal).map(_.toInt).getOrElse(1) + + val result = unwrap(api.listTransactions( + walletId = walletId, + order = orderOf, + minWithdrawal = minWithdrawalTx + ).toFuture.executeBlocking) + + if(result.isEmpty) { + trace("No txs returned") + } else { + result.foreach(trace.apply) + } + + } else if (hasArgument(CmdLine.listWallets)) { + val result = unwrap(api.listWallets.toFuture.executeBlocking) + result.foreach(trace.apply) + + } else if (hasArgument(CmdLine.createWallet) || hasArgument(CmdLine.restoreWallet)) { + val name = arguments.get(CmdLine.name) + val passphrase = arguments.get(CmdLine.passphrase) + val mnemonic = arguments.get(CmdLine.mnemonic) + val addressPoolGap = arguments(CmdLine.addressPoolGap).map(_.toInt) + + val result = unwrap(api.createRestoreWallet( + name, + passphrase, + GenericMnemonicSentence(mnemonic), + addressPoolGap + ).executeBlocking) + + trace(result) + + } else { + trace("No command recognised") + } + + }.recover { + case e => trace(e.toString) + } + trace.close() + system.terminate() + + } + } + + + def showHelp(): Unit = { + println("Enter commands, and so on...") + } + + + def unwrap[T](apiResult: Try[CardanoApiResponse[T]])(implicit t: Trace): T = unwrapOpt(apiResult).get + + def unwrapOpt[T](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { + case Success(Left(ErrorMessage(message, code))) => + trace(s"API Error message $message, code $code") + None + case Success(Right(t: T)) => Some(t) + case Failure(exception) => + println(exception) + None + } + + def fail[T](msg: String): T = throw new RuntimeException(msg) + +} diff --git a/src/main/scala/iog/psg/cardano/util/ArgumentParser.scala b/src/main/scala/iog/psg/cardano/util/ArgumentParser.scala new file mode 100644 index 0000000..65abca2 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/util/ArgumentParser.scala @@ -0,0 +1,38 @@ +package iog.psg.cardano.util + +import scala.annotation.tailrec + +class ArgumentParser(args: Array[String]) { + + lazy val (keyValues, params) = parse(args.toSeq, (Map.empty.withDefaultValue(None), List())) + + @tailrec + private def parse( + args: Seq[String], + acc: (Map[String, Option[String]], List[String]) + ): (Map[String, Option[String]], List[String]) = { + + args match { + case Seq() => acc + case Seq(head) => + require(head.startsWith("-"), "Mismatched parameters") + (acc._1, head +: acc._2) + case Seq(head, value, tail@_*) if value.startsWith("-") => + require(head.startsWith("-"), "Mismatched parameters") + parse(value +: tail, (acc._1, head +: acc._2)) + case Seq(head, value, tail@_*) => + require(head.startsWith("-"), "Mismatched parameters") + parse(tail, (acc._1 + (head -> Option(value)), acc._2)) + } + + } + + def apply(key: String): Option[String] = keyValues(key) + + def get(key: String): String = keyValues(key).getOrElse(throw new IllegalArgumentException(s"Key $key has no value")) + + def contains(key: String): Boolean = params.contains(key) || keyValues.keySet.contains(key) + + def noArgs: Boolean = args.isEmpty +} + diff --git a/src/main/scala/iog/psg/cardano/util/Trace.scala b/src/main/scala/iog/psg/cardano/util/Trace.scala new file mode 100644 index 0000000..28bffc3 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/util/Trace.scala @@ -0,0 +1,61 @@ +package iog.psg.cardano.util + +import java.io.{BufferedWriter, File, FileWriter} + +import scala.util.Try + +trait Trace extends AutoCloseable { + + parent => + + def apply(s: Object): Unit + + def withTrace(other: Trace): Trace = other match { + case NoOpTrace => this + case _ => + new Trace { + override def apply(s: Object): Unit = { + parent.apply(s) + other.apply(s) + } + + override def close(): Unit = { + Try(parent.close()).recover { + case e => println(e) + } + Try(other.close()).recover { + case e => println(e) + } + } + } + } +} + + +object NoOpTrace extends Trace { + override def apply(s: Object): Unit = () + override def close(): Unit = () + + override def withTrace(other: Trace): Trace = other +} + +object ConsoleTrace extends Trace { + override def apply(s: Object): Unit = println(s) + override def close(): Unit = () +} + + +class FileTrace(f: File) extends Trace { + private val traceFile = new BufferedWriter(new FileWriter(f)) + + override def apply(s: Object): Unit = { + traceFile.write(s.toString) + traceFile.newLine() + } + + override def close(): Unit = { + Try(traceFile.close()).recover { + case e => println(e) + } + } +} diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala index ae9756c..6238f86 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -2,67 +2,182 @@ package iog.psg.cardano import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps._ -import iog.psg.cardano.CardanoApi.{CardanoApiResponse, maxWaitTime} -import iog.psg.cardano.CardanoApiCodec.{ErrorMessage, GenericMnemonicSentence, Payment, Payments, QuantityUnit, ready} +import iog.psg.cardano.CardanoApi._ +import iog.psg.cardano.CardanoApiCodec.TxState.TxState +import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, SyncState, TxState, Units} import scala.util.{Failure, Success, Try} object CardanoApiTestScript { + private implicit val system = ActorSystem("SingleRequest") private implicit val context = system.dispatcher - //implicit val waitForDuration = 5.seconds def main(args: Array[String]): Unit = { - val baseUri = args.headOption.getOrElse(throw new IllegalArgumentException("Pass the base URL to the cardano wallet API as a parameter")) - val walletName = args(1) + Try { + val baseUri = args.headOption.getOrElse(throw new IllegalArgumentException("Pass the base URL to the cardano wallet API as a parameter")) + + val walletNameFrom = "alan1" + val walletToMnem = GenericMnemonicSentence("enforce chicken cactus pupil wagon brother stuff pumpkin hobby noble genius fish air only sign hour apart fruit market acid beach top subway swear") + val walletNameTo = "cardano-api-to" + val walletFromMnem = //GenericMnemonicSentence("sustain noble raise quarter elephant police smile exhibit pass goose acoustic muffin enrich march boy music ostrich maple predict song silk naive trip jump" + GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") + val walletToPassphrase = "password910" + val walletFromPassphrase = "1234567890" + val newWalletPassword = "password!123" - println(s"Using base url '$baseUri''") - println(s"Using wallet name '$walletName''") + val lovelaceToTransfer = 10000000 - val api = new CardanoApi(baseUri) + val walletsNamesOfInterest = Seq(walletNameFrom, walletNameTo) - val netInfo = unwrap(api.networkInfo.toFuture.executeBlocking) + println(s"Using base url '$baseUri''") + println(s"Using wallet name '$walletNameFrom''") - println(netInfo) + val api = new CardanoApi(baseUri) - if (netInfo.syncProgress.status == ready) { - val walletAddresses = unwrap(api.listWallets.toFuture.executeBlocking) - walletAddresses.foreach(addr => { - println(s"Name: ${addr.name} balance: ${addr.balance}") - println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") - }) - val walletAddress = walletAddresses.headOption.getOrElse { - val mnem = GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") - val mnem24 = GenericMnemonicSentence("enforce chicken cactus pupil wagon brother stuff pumpkin hobby noble genius fish air only sign hour apart fruit market acid beach top subway swear") - //val str = "wrestle trumpet visual ivory security reduce property ecology mutual market mimic cancel liquid mention cluster" - println("Generating wallet...") - unwrap(api.createRestoreWallet(walletName, "password", mnem24).executeBlocking) + def waitForTx(txState: TxState, walletId: String, txId: String):Unit = { + if(txState == TxState.pending) { + println(s"$txState") + Thread.sleep(5000) + val txUpdate = unwrap(api.getTransaction(walletId, txId).toFuture.executeBlocking) + waitForTx(txUpdate.status, walletId, txId) + } + println(s"$txState !!") } - val allWalletAddresses = - unwrap(api.listAddresses(walletAddress.id, None).toFuture.executeBlocking) - println(allWalletAddresses) + val netInfo = unwrap(api.networkInfo.toFuture.executeBlocking) + + println(netInfo) + + if (netInfo.syncProgress.status == SyncState.ready) { + val walletAddresses = unwrap(api.listWallets.toFuture.executeBlocking) - val payments = Payments( - Seq( - Payment(allWalletAddresses.head.id, QuantityUnit(1, "lovelace")) + walletAddresses.foreach(addr => { + println(s"Name: ${addr.name} balance: ${addr.balance}") + println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") + }) + + val walletsOfInterest = walletAddresses.filter(w => walletsNamesOfInterest.contains(w.name)) + val fromWallet = walletsOfInterest.find(_.name == walletNameFrom).getOrElse { + println("Generating 'from' wallet...") + unwrap(api.createRestoreWallet(walletNameFrom, walletFromPassphrase, walletFromMnem).executeBlocking) + } + + val unitResult = unwrap( + api. + updatePassphrase( + fromWallet.id, + walletFromPassphrase, + newWalletPassword) + .executeBlocking ) - ) - val paymentsResponse = - unwrapOpt(api.fundPayments(walletAddress.id, payments).executeBlocking) + /*val fromWalletFromNewPassword = unwrap( + api. + createRestoreWallet( + walletNameFrom, + newWalletPassword, + walletFromMnem) + .executeBlocking + )*/ + + val unitResultPutItBack = unwrap( + api. + updatePassphrase( + fromWallet.id, + newWalletPassword, + walletFromPassphrase) + .executeBlocking + ) + println(s"From wallet name, id, balance ${fromWallet.name}, ${fromWallet.id}, ${fromWallet.balance}") - println(paymentsResponse) + val toWallet = walletsOfInterest.find(_.name == walletNameTo).getOrElse { + println("Generating 'to' wallet...") + unwrap(api.createRestoreWallet(walletNameTo, walletToPassphrase, walletToMnem).executeBlocking) + } + + println(s"To wallet name, id, balance ${toWallet.name}, ${toWallet.id}, ${toWallet.balance}") + if (fromWallet.balance.available.quantity > 2) { + + val toWalletAddresses = + unwrap(api.listAddresses(toWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking) + + + val paymentTo = toWalletAddresses.headOption.getOrElse(fail("No unused addresses in the To wallet?")) + + val payments = Payments( + Seq( + Payment(paymentTo.id, QuantityUnit(lovelaceToTransfer, Units.lovelace)) + ) + ) - } else { - fail(s"Network not ready ${netInfo.syncProgress}") - } + val tx = unwrap(api.createTransaction( + fromWallet.id, + walletFromPassphrase, + payments, + None, + ).executeBlocking) + + waitForTx(tx.status, fromWallet.id, tx.id) + + + val fromWalletAddresses = + unwrap(api.listAddresses(fromWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking) + + val paymentBack = fromWalletAddresses.headOption.getOrElse(fail("No unused addresses in the From wallet?")) + //transfer back + val returnPayments = Payments( + Seq( + Payment(paymentBack.id, QuantityUnit(lovelaceToTransfer, Units.lovelace)) + ) + ) + + val estimate = unwrap(api.estimateFee(toWallet.id, returnPayments).executeBlocking) + val returnPayments2 = Payments( + Seq( + Payment(paymentBack.id, QuantityUnit(lovelaceToTransfer - estimate.estimatedMax.quantity, Units.lovelace)) + ) + ) + + val returnTx = unwrap(api.createTransaction( + toWallet.id, + walletToPassphrase, + returnPayments2, + None, + ).executeBlocking) + + waitForTx(returnTx.status, toWallet.id, returnTx.id) + + println(s"Successfully transferred value between 2 wallets") + val refreshedToWallet = unwrap(api.getWallet(toWallet.id).toFuture.executeBlocking) + val refreshedFromWallet = unwrap(api.getWallet(fromWallet.id).toFuture.executeBlocking) + val fromDiffBalance = refreshedFromWallet.balance.available.quantity - fromWallet.balance.available.quantity + val toDiffBalance = refreshedToWallet.balance.available.quantity - toWallet.balance.available.quantity + println(s"Balance of 'from' wallet is now ${refreshedFromWallet.balance} diff: $fromDiffBalance") + println(s"Balance of 'to' wallet is now ${refreshedToWallet.balance} diff $toDiffBalance") + + } else { + println(s"From wallet ${fromWallet.name} balance ${fromWallet.balance} is too low, cannot continue") + + val fromWalletAddressesFirstFew = + unwrap(api.listAddresses(fromWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking).take(5) + println("Use https://testnets.cardano.org/en/cardano/tools/faucet/ to lodge TESTNET credit to below address") + println(fromWalletAddressesFirstFew) + } + + } else { + fail(s"Network not ready ${netInfo.syncProgress}") + } + } recover { + case e => println(e) + } + system.terminate() } private def unwrap[T](apiResult: Try[CardanoApiResponse[T]]): T = unwrapOpt(apiResult).get diff --git a/src/test/scala/iog/psg/cardano/util/ArgumentParserSpec.scala b/src/test/scala/iog/psg/cardano/util/ArgumentParserSpec.scala new file mode 100644 index 0000000..31091e7 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/ArgumentParserSpec.scala @@ -0,0 +1,39 @@ +package iog.psg.cardano.util + + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +/** + */ +class ArgumentParserSpec extends AnyFlatSpec with Matchers { + + "ArgumentParser" should "handle params with values and without" in { + + val args = Array("-httpServer", "-rpcserver", "-password", "password", "-recovery", "-norecovery", "-myValue", "90") + val sut = new ArgumentParser(args) + assert(sut("-password") === Some("password")) + assert(sut("-recovery") === None) + assert(sut.contains("-recovery")) + assert(sut.contains("-myValue")) + assert(sut("-myValue") === Some("90")) + } + + it should "handle 3" in { + val args2 = Array("-httpServer", "-password", "password") + val sut2 = new ArgumentParser(args2) + assert(sut2.get("-password") == "password") + } + + it should "handle 1" in { + val args2 = Array("-httpServer") + val sut2 = new ArgumentParser(args2) + assert(sut2.contains("-httpServer")) + } + + it should "handle 1 pair" in { + val args2 = Array("-httpServer", "something") + val sut2 = new ArgumentParser(args2) + assert(sut2("-httpServer").contains("something")) + } +} + From 4c026a778273618687bed11b4c8c36a790945c06 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Fri, 28 Aug 2020 14:23:32 +0100 Subject: [PATCH 05/39] Feature/ci (#1) Working API allowing transfers, wallet manipulation. Uses a live cardano wallet (testnet) backend for tests. --- .github/workflows/ci.yml | 36 +++ LICENSE | 201 +++++++++++++++ README.md | 4 + build.sbt | 16 +- cmdline.sh | 8 + project/build.properties | 2 +- project/plugins.sbt | 5 + .../scala/iog/psg/cardano/CardanoApi.scala | 72 ++++-- .../iog/psg/cardano/CardanoApiCodec.scala | 24 +- .../iog/psg/cardano/CardanoApiMain.scala | 143 +++++++---- .../iog/psg/cardano/util/Configure.scala | 11 + src/test/resources/application.conf | 13 + .../iog/psg/cardano/CardanoApiMainSpec.scala | 232 ++++++++++++++++++ .../psg/cardano/CardanoApiTestScript.scala | 20 +- 14 files changed, 692 insertions(+), 95 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100755 cmdline.sh create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/iog/psg/cardano/util/Configure.scala create mode 100644 src/test/resources/application.conf create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0595c0d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Clean, build, test and package + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run tests + run: sbt coverage test coverageReport + env: + BASE_URL: http://cardano-wallet-testnet.iog.solutions:8090/v2/ + - name: Archive code coverage results + uses: actions/upload-artifact@v2 + with: + name: code-coverage-report + path: target/scala-2.13/scoverage-report + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Package + run: sbt publish + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef0ea47 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 iog-psg + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d6cf6d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# psg-cardano-wallet-api +Scala client to the Cardano wallet REST API + +Note - not every field in the wallet API is represented, some of those used in delagation are ignored. \ No newline at end of file diff --git a/build.sbt b/build.sbt index 4e5ecd6..b2df00d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,19 @@ +import sbtghpackages.TokenSource.{GitConfig,Or,Environment} + +name:= "psg-cardano-wallet-api" + +version := "0.1.2-SNAPSHOT" scalaVersion := "2.13.3" +organization := "iog.psg" + +githubOwner := "input-output-hk" + +githubRepository := "psg-cardano-wallet-api" + +githubTokenSource := Or(GitConfig("github.token"), Environment("GITHUB_TOKEN")) + val akkaVersion = "2.6.8" val akkaHttpVersion = "10.2.0" val akkaHttpCirce = "1.31.0" @@ -23,5 +36,6 @@ libraryDependencies ++= Seq( javacOptions ++= Seq("-source", "11", "-target", "11") - scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations") + + diff --git a/cmdline.sh b/cmdline.sh new file mode 100755 index 0000000..f017f5f --- /dev/null +++ b/cmdline.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +VER=0.1.1-SNAPSHOT +BASE_URL="http://cardano-wallet-testnet.iog.solutions:8090/v2/" + + +#run sbt assembly to create this jar +exec java -jar target/scala-2.13/psg-cardano-wallet-api-assembly-${VER}.jar -baseUrl ${BASE_URL} "$@" diff --git a/project/build.properties b/project/build.properties index a919a9b..0837f7a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.8 +sbt.version=1.3.13 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..32411f6 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.2") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") \ No newline at end of file diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 706b5cc..334d261 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -2,24 +2,21 @@ package iog.psg.cardano import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.marshalling.Marshal -import akka.http.scaladsl.model.ContentType.{Binary, WithFixedCharset} import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.Uri.Query import akka.http.scaladsl.model._ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ import io.circe.generic.extras.Configuration +import iog.psg.cardano.CardanoApi.IOExecutionContext import iog.psg.cardano.CardanoApi.Order.Order -import iog.psg.cardano.CardanoApiCodec.toErrorMessage import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} import scala.concurrent.{Await, ExecutionContext, Future} -import scala.util.Try /** * Defines the API which wraps the Cardano API, depends on CardanoApiCodec for it's implementation, @@ -27,6 +24,11 @@ import scala.util.Try */ object CardanoApi { + //Marker class, differenciate between multiple ec's in a client + case class IOExecutionContext(ec: ExecutionContext) { + implicit val ioEc: ExecutionContext = ec + } + case class ErrorMessage(message: String, code: String) type CardanoApiResponse[T] = Either[ErrorMessage, T] @@ -43,40 +45,29 @@ object CardanoApi { object CardanoApiOps { + implicit class TransformOp[T](val knot: Future[CardanoApiResponse[Future[CardanoApiResponse[T]]]]) extends AnyVal { + //Cannot call this transform as it clashes with futire + def unknot(implicit ec: ExecutionContext): Future[CardanoApiResponse[T]] = knot.flatMap { + case Left(errorMessage) => Future.successful(Left(errorMessage)) + case Right(vaue) => vaue + } + } + implicit class FutOp[T](val request: CardanoApiRequest[T]) extends AnyVal { def toFuture: Future[CardanoApiRequest[T]] = Future.successful(request) } - implicit class CardanoApiRequestOps[T](requestF: Future[CardanoApiRequest[T]]) - (implicit ec: ExecutionContext, - as: ActorSystem, - maxToStrictWaitTime: FiniteDuration - ) { - def execute: Future[CardanoApiResponse[T]] = { - requestF flatMap { request => - Http() - .singleRequest(request.request) - .flatMap(request.mapper) - } - } - - def executeBlocking(implicit maxWaitTime: Duration): Try[CardanoApiResponse[T]] = Try { - Await.result( - execute, - maxWaitTime - ) - } - } } } -class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: ActorSystem) { +class CardanoApi(baseUriWithPort: String)(implicit ec: IOExecutionContext, as: ActorSystem) { import iog.psg.cardano.CardanoApi._ import iog.psg.cardano.CardanoApiCodec._ import AddressFilter.AddressFilter + import ec.ioEc private val wallets = s"${baseUriWithPort}wallets" private val network = s"${baseUriWithPort}network" @@ -84,6 +75,31 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + + object Ops { + //tie execute to ioEc + implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]]) { + def execute: Future[CardanoApiResponse[T]] = { + requestF.flatMap(_.execute) + } + + def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = + Await.result(execute, maxWaitTime) + + } + + implicit class CardanoApiRequestOps[T](request: CardanoApiRequest[T]) { + def execute: Future[CardanoApiResponse[T]] = { + Http() + .singleRequest(request.request) + .flatMap(request.mapper) + } + + def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = + Await.result(execute, maxWaitTime) + } + + } def listWallets: CardanoApiRequest[Seq[Wallet]] = CardanoApiRequest( HttpRequest( uri = wallets, @@ -160,7 +176,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act start: Option[ZonedDateTime] = None, end: Option[ZonedDateTime] = None, order: Order = Order.descendingOrder, - minWithdrawal: Int = 1): CardanoApiRequest[Seq[Transaction]] = { + minWithdrawal: Int = 1): CardanoApiRequest[Seq[CreateTransactionResponse]] = { val baseUri = Uri(s"${wallets}/${walletId}/transactions") val queries = @@ -171,13 +187,15 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act case (queryParamName, Some(minWith: Int)) => queryParamName -> minWith.toString } + val uriWithQueries = baseUri.withQuery(Query(queries: _*)) + CardanoApiRequest( HttpRequest( uri = uriWithQueries, method = GET ), - _.toWalletTransactions + _.toCreateTransactionResponses ) } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 6607c4a..031f278 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -4,7 +4,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import akka.http.scaladsl.model.ContentType.{Binary, WithFixedCharset} -import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaTypes, StatusCodes} +import akka.http.scaladsl.model.{ContentType, HttpEntity, HttpResponse, MediaTypes, StatusCodes} import akka.http.scaladsl.unmarshalling.Unmarshal import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer @@ -93,7 +93,7 @@ object CardanoApiCodec { case class Payment(address: String, amount: QuantityUnit) - @ConfiguredJsonCodec case class UpdatePassphrase(oldPassphrase: String, newPassphrase: String) + @ConfiguredJsonCodec private[cardano] case class UpdatePassphrase(oldPassphrase: String, newPassphrase: String) trait MnemonicSentence { val mnemonicSentence: IndexedSeq[String] @@ -183,11 +183,6 @@ object CardanoApiCodec { amount: QuantityUnit ) - case class Transaction( - inputs: IndexedSeq[InAddress], - outputs: Seq[OutAddress] - ) - case class FundPaymentsResponse( inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress] @@ -255,7 +250,7 @@ object CardanoApiCodec { timeout: FiniteDuration, ec: ExecutionContext) { - private def strictEntity: Future[HttpEntity.Strict] = response.entity.toStrict(timeout) + private def strictEntityF: Future[HttpEntity.Strict] = response.entity.toStrict(timeout) private def extractErrorResponse[T](strictEntity: Future[HttpEntity.Strict]): Future[CardanoApiResponse[T]] = { @@ -277,10 +272,13 @@ object CardanoApiCodec { // Load into memory using toStrict // a. no responses utilise streaming and // b. the Either unmarshaller requires it - strictEntity.flatMap(f) + strictEntityF.flatMap(f) case Binary(MediaTypes.`application/octet-stream`) => - extractErrorResponse[T](strictEntity) + extractErrorResponse[T](strictEntityF) + + case c: ContentType => + Future.failed(new RuntimeException(s"Unexpected type ${c.mediaType}, ${c.charsetOption}")) } } @@ -297,8 +295,8 @@ object CardanoApiCodec { def toFundPaymentsResponse: Future[CardanoApiResponse[FundPaymentsResponse]] = to[FundPaymentsResponse](Unmarshal(_).to[CardanoApiResponse[FundPaymentsResponse]]) - def toWalletTransactions: Future[CardanoApiResponse[Seq[Transaction]]] - = to[Seq[Transaction]](Unmarshal(_).to[CardanoApiResponse[Seq[Transaction]]]) + def toCreateTransactionResponses: Future[CardanoApiResponse[Seq[CreateTransactionResponse]]] + = to[Seq[CreateTransactionResponse]](Unmarshal(_).to[CardanoApiResponse[Seq[CreateTransactionResponse]]]) def toCreateTransactionResponse: Future[CardanoApiResponse[CreateTransactionResponse]] = to[CreateTransactionResponse](Unmarshal(_).to[CardanoApiResponse[CreateTransactionResponse]]) @@ -310,7 +308,7 @@ object CardanoApiCodec { if (response.status == StatusCodes.NoContent) { Future.successful(Right(())) } else { - extractErrorResponse[Unit](strictEntity) + extractErrorResponse[Unit](strictEntityF) } } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index b6d1584..465b43f 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -1,13 +1,14 @@ package iog.psg.cardano import java.io.File +import java.time.ZonedDateTime import akka.actor.ActorSystem -import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestOps, FutOp} -import iog.psg.cardano.CardanoApi.{Order, CardanoApiResponse, ErrorMessage, defaultMaxWaitTime} +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, IOExecutionContext, Order, defaultMaxWaitTime} import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} -import iog.psg.cardano.util.{ArgumentParser, ConsoleTrace, FileTrace, NoOpTrace, Trace} +import iog.psg.cardano.util._ +import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} object CardanoApiMain { @@ -19,9 +20,14 @@ object CardanoApiMain { val netInfo = "-netInfo" val baseUrl = "-baseUrl" val listWallets = "-wallets" + val deleteWallet = "-deleteWallet" + val getWallet = "-wallet" val createWallet = "-createWallet" val restoreWallet = "-restoreWallet" + val estimateFee = "-estimateFee" val name = "-name" + val updatePassphrase = "-updatePassphrase" + val oldPassphrase = "-oldPassphrase" val passphrase = "-passphrase" val mnemonic = "-mnemonic" val addressPoolGap = "-addressPoolGap" @@ -36,6 +42,7 @@ object CardanoApiMain { val createTx = "-createTx" val fundTx = "-fundTx" val getTx = "-getTx" + val txId = "-txId" val amount = "-amount" val address = "-address" @@ -45,27 +52,27 @@ object CardanoApiMain { val defaultTraceFile = "cardano-api.log" def main(args: Array[String]): Unit = { + val arguments = new ArgumentParser(args) + val conTracer = if (arguments.contains(CmdLine.noConsole)) NoOpTrace else ConsoleTrace - if (arguments.noArgs || arguments.contains(CmdLine.help)) { - showHelp() - } else { - val conTracer = if (arguments.contains(CmdLine.noConsole)) NoOpTrace else ConsoleTrace + implicit val trace = conTracer.withTrace( + if (arguments.contains(CmdLine.traceToFile)) { + val fileName = arguments(CmdLine.traceToFile).getOrElse(defaultTraceFile) + new FileTrace(new File(fileName)) + } else NoOpTrace + ) - implicit val trace = conTracer.withTrace( - if (arguments.contains(CmdLine.traceToFile)) { - val fileName = arguments(CmdLine.traceToFile).getOrElse(defaultTraceFile) - new FileTrace(new File(fileName)) - } else NoOpTrace - ) + run(arguments) + } - /*def getArgument(arg: String): String = { - arguments(name).getOrElse { - val msg = s"No value provided for $arg" - throw new IllegalArgumentException(msg) - } - }*/ + private[cardano] def run(arguments: ArgumentParser)(implicit trace: Trace): Unit = { + + + if (arguments.noArgs || arguments.contains(CmdLine.help)) { + showHelp() + } else { def hasArgument(arg: String): Boolean = { val result = arguments.contains(arg) @@ -73,14 +80,8 @@ object CardanoApiMain { result } - def hasArgumentWithValue(arg: String): Boolean = { - val result = arguments(arg).isDefined - if (result) trace(arg) - result - } - - implicit val system = ActorSystem("SingleRequest") - implicit val context = system.dispatcher + implicit val system: ActorSystem = ActorSystem("SingleRequest") + implicit val ioEc: IOExecutionContext = IOExecutionContext(system.dispatcher) Try { @@ -89,27 +90,69 @@ object CardanoApiMain { trace(s"baseurl:$url") val api = new CardanoApi(url) + import api.Ops._ if (hasArgument(CmdLine.netInfo)) { - val result = unwrap(api.networkInfo.toFuture.executeBlocking) - trace(result.toString) + val result = unwrap(api.networkInfo.executeBlocking) + trace(result) } else if (hasArgument(CmdLine.listWallets)) { - val result = unwrap(api.listWallets.toFuture.executeBlocking) + val result = unwrap(api.listWallets.executeBlocking) result.foreach(trace.apply) + } else if (hasArgument(CmdLine.estimateFee)) { + val walletId = arguments.get(CmdLine.walletId) + val amount = arguments.get(CmdLine.amount).toLong + val addr = arguments.get(CmdLine.address) + val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) + val payments = Payments(Seq(singlePayment)) + val result = unwrap(api.estimateFee(walletId, payments).executeBlocking) + trace(result) + + } else if (hasArgument(CmdLine.getWallet)) { + val walletId = arguments.get(CmdLine.walletId) + val result = unwrap(api.getWallet(walletId).executeBlocking) + trace(result) + + } else if (hasArgument(CmdLine.updatePassphrase)) { + val walletId = arguments.get(CmdLine.walletId) + val oldPassphrase = arguments.get(CmdLine.oldPassphrase) + val newPassphrase = arguments.get(CmdLine.passphrase) + + val result: Unit = unwrap(api.updatePassphrase(walletId, oldPassphrase, newPassphrase).executeBlocking) + trace("Unit result from delete wallet") + + } else if (hasArgument(CmdLine.deleteWallet)) { + val walletId = arguments.get(CmdLine.walletId) + val result: Unit = unwrap(api.deleteWallet(walletId).executeBlocking) + trace("Unit result from delete wallet") + } else if (hasArgument(CmdLine.listWalletAddresses)) { val walletId = arguments.get(CmdLine.walletId) - val addrState = arguments(CmdLine.state).map(AddressFilter.withName) - val result = unwrap(api.listAddresses(walletId, addrState).toFuture.executeBlocking) - result.foreach(trace.apply) + val addressesState = Some(AddressFilter.withName(arguments.get(CmdLine.state))) + val result = unwrap(api.listAddresses(walletId, addressesState).executeBlocking) + trace(result) } else if (hasArgument(CmdLine.getTx)) { val walletId = arguments.get(CmdLine.walletId) - val txId = arguments.get(CmdLine.getTx) - val result = unwrap(api.getTransaction(walletId, txId).toFuture.executeBlocking) + val txId = arguments.get(CmdLine.txId) + val result = unwrap(api.getTransaction(walletId, txId).executeBlocking) trace(result) } else if (hasArgument(CmdLine.createTx)) { + val walletId = arguments.get(CmdLine.walletId) + val amount = arguments.get(CmdLine.amount).toLong + val addr = arguments.get(CmdLine.address) + val pass = arguments.get(CmdLine.passphrase) + val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) + val payments = Payments(Seq(singlePayment)) + val result = unwrap(api.createTransaction( + walletId, + pass, + payments, + None + ).executeBlocking) + trace(result) + } else if (hasArgument(CmdLine.fundTx)) { val walletId = arguments.get(CmdLine.walletId) val amount = arguments.get(CmdLine.amount).toLong @@ -125,27 +168,29 @@ object CardanoApiMain { } else if (hasArgument(CmdLine.listWalletTransactions)) { val walletId = arguments.get(CmdLine.walletId) - //val startDate = arguments(start) - //val endDate = arguments(end) - val orderOf = arguments(CmdLine.order).flatMap(s => Try(Order.withName(s)).toOption).getOrElse(Order.descendingOrder) + val startDate = arguments(CmdLine.start).map(strToZonedDateTime) + val endDate = arguments(CmdLine.end).map(strToZonedDateTime) + val orderOf = arguments(CmdLine.order).flatMap(s => Try(Order.withName(s)).toOption).getOrElse(Order.descendingOrder) val minWithdrawalTx = arguments(CmdLine.minWithdrawal).map(_.toInt).getOrElse(1) val result = unwrap(api.listTransactions( - walletId = walletId, - order = orderOf, + walletId, + startDate, + endDate, + orderOf, minWithdrawal = minWithdrawalTx - ).toFuture.executeBlocking) + ).executeBlocking) - if(result.isEmpty) { + if (result.isEmpty) { trace("No txs returned") } else { result.foreach(trace.apply) } } else if (hasArgument(CmdLine.listWallets)) { - val result = unwrap(api.listWallets.toFuture.executeBlocking) + val result = unwrap(api.listWallets.executeBlocking) result.foreach(trace.apply) - + } else if (hasArgument(CmdLine.createWallet) || hasArgument(CmdLine.restoreWallet)) { val name = arguments.get(CmdLine.name) val passphrase = arguments.get(CmdLine.passphrase) @@ -175,14 +220,18 @@ object CardanoApiMain { } - def showHelp(): Unit = { + private def strToZonedDateTime(dtStr: String): ZonedDateTime = { + ZonedDateTime.parse(dtStr) + } + + private def showHelp(): Unit = { println("Enter commands, and so on...") } - def unwrap[T](apiResult: Try[CardanoApiResponse[T]])(implicit t: Trace): T = unwrapOpt(apiResult).get + def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T])(implicit t: Trace): T = unwrapOpt(Try(apiResult)).get - def unwrapOpt[T](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { + def unwrapOpt[T:ClassTag](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { case Success(Left(ErrorMessage(message, code))) => trace(s"API Error message $message, code $code") None diff --git a/src/main/scala/iog/psg/cardano/util/Configure.scala b/src/main/scala/iog/psg/cardano/util/Configure.scala new file mode 100644 index 0000000..4e2a292 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/util/Configure.scala @@ -0,0 +1,11 @@ +package iog.psg.cardano.util + +import com.typesafe.config.{Config, ConfigFactory} + +trait Configure { + + implicit val config = ConfigFactory.load() + def config(name: String): Config = config.getConfig(name) +} + +object ConfigureFactory extends Configure diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf new file mode 100644 index 0000000..20898a3 --- /dev/null +++ b/src/test/resources/application.conf @@ -0,0 +1,13 @@ +cardano.wallet.baseUrl="http://localhost:8091/v2/" +cardano.wallet.baseUrl=${?BASE_URL} +cardano.wallet.passphrase="password10" +cardano.wallet.passphrase=${?PASSPHRASE} +cardano.wallet.name="cardanoapimainspec" +cardano.wallet.amount=2000000 +cardano.wallet.mnemonic="receive post siren monkey mistake morning teach section mention rural idea say offer number ribbon toward rigid pluck begin ticket auto" +cardano.wallet.id="b63eacb4c89bd942cacfe0d3ed47459bbf0ce5c9" + +cardano.wallet2.mnemonic="later image spider wrestle tunnel bomb ahead glance broken merry still nominee property clever wedding reduce tribe buzz voyage clay sheriff" +cardano.wallet2.id="527eac22af137dcd159fade57ab0686931feed7c" +cardano.wallet2.name="somethrowawayname" +cardano.wallet2.passphrase="somethrowawayname" \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala new file mode 100644 index 0000000..620a408 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -0,0 +1,232 @@ +package iog.psg.cardano + +import java.time.ZonedDateTime + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi._ +import iog.psg.cardano.CardanoApiMain.CmdLine +import iog.psg.cardano.util.{ArgumentParser, Configure, Trace} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { + + + private implicit val system = ActorSystem("SingleRequest") + private implicit val context = system.dispatcher + private implicit val ioEc = IOExecutionContext(context) + private val baseUrl = config.getString("cardano.wallet.baseUrl") + private val testWalletName = config.getString("cardano.wallet.name") + private val testWallet2Name = config.getString("cardano.wallet2.name") + private val testWalletMnemonic = config.getString("cardano.wallet.mnemonic") + private val testWallet2Mnemonic = config.getString("cardano.wallet2.mnemonic") + private val testWalletId = config.getString("cardano.wallet.id") + private val testWallet2Id = config.getString("cardano.wallet2.id") + private val testWalletPassphrase = config.getString("cardano.wallet.passphrase") + private val testWallet2Passphrase = config.getString("cardano.wallet2.passphrase") + private val testAmountToTransfer = config.getString("cardano.wallet.amount") + + private val defaultArgs = Array(CmdLine.baseUrl, baseUrl) + + private def makeArgs(args: String*): Array[String] = + defaultArgs ++ args + + private def runCmdLine(args: String*): Seq[String] = { + val arguments = new ArgumentParser(makeArgs(args: _*)) + + var results: Seq[String] = Seq.empty + implicit val memTrace = new Trace { + override def apply(s: Object): Unit = results = s.toString +: results + + override def close(): Unit = () + } + + CardanoApiMain.run(arguments) + + results.reverse + } + + "The Cmd line Main" should "support retrieving netInfo" in { + val results = runCmdLine(CmdLine.netInfo) + assert(results.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl'") + } + + it should "not create a wallet with a bad mnemonic" in { + val badMnem = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21" + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, "password", + CmdLine.name, "some name", + CmdLine.mnemonic, badMnem) + assert(results.exists(_.contains("Found an unknown word")), "Bad menmonic not stopped") + } + + it should "find our test wallet" in { + val wallets = runCmdLine( + CmdLine.listWallets) + + wallets.find(w => w.contains(testWalletName) && + w.contains(testWalletId)) + .getOrElse { + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, testWalletPassphrase, + CmdLine.name, testWalletName, + CmdLine.mnemonic, testWalletMnemonic) + + assert(results.exists(_.contains(testWalletId)), "Test Wallet not created") + } + + } + + it should "get our wallet" in { + val results = runCmdLine( + CmdLine.getWallet, + CmdLine.walletId, testWalletId) + + assert(results.exists(_.contains(testWalletId)), "Test wallet not found.") + + } + + it should "create or find wallet 2" in { + + val wallets = runCmdLine(CmdLine.listWallets) + + wallets.find(w => w.contains(testWallet2Name) && + w.contains(testWallet2Id)) + .getOrElse { + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, testWallet2Passphrase, + CmdLine.name, testWallet2Name, + CmdLine.mnemonic, testWallet2Mnemonic) + + assert(results.last.contains(testWallet2Id), "Test wallet 2 not found.") + } + } + + it should "allow password change in test wallet 2" in { + runCmdLine( + CmdLine.updatePassphrase, + CmdLine.oldPassphrase, testWallet2Passphrase, + CmdLine.passphrase, testWalletPassphrase, + CmdLine.walletId, testWallet2Id) + + } + + it should "fund payments" in { + + val results = runCmdLine( + CmdLine.fundTx, + CmdLine.amount, testAmountToTransfer, + CmdLine.address, getUnusedAddressWallet2, + CmdLine.walletId, testWalletId) + + assert(results.last.contains("FundPaymentsResponse"), "FundPaymentsResponse failed?") + //results.foreach(println) + } + + private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) + + private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) + + def getUnusedAddress(walletId: String): String = { + val results = runCmdLine( + CmdLine.listWalletAddresses, + CmdLine.state, "unused", + CmdLine.walletId, walletId) + + + val all = results.last.split(",") + val cleanedUp = all.map(s => { + if (s.indexOf("addr") > 0) + Some(s.substring(s.indexOf("addr"))) + else None + }) collect { + case Some(goodAddr) => goodAddr + } + cleanedUp.head + } + + it should "transact from a to a" in { + + val unusedAddr = getUnusedAddressWallet1 + + // estimate fee + val estimateResults = runCmdLine( + CmdLine.estimateFee, + CmdLine.amount, testAmountToTransfer, + CmdLine.address, unusedAddr, + CmdLine.walletId, testWalletId) + + //estimateResults.foreach(println) + + val preTxTime = ZonedDateTime.now().minusMinutes(10) + + val resultsCreateTx = runCmdLine( + CmdLine.createTx, + CmdLine.passphrase, testWalletPassphrase, + CmdLine.amount, testAmountToTransfer, + CmdLine.address, unusedAddr, + CmdLine.walletId, testWalletId) + + assert(resultsCreateTx.last.contains("pending"), "Transaction should be pending") + + val txId = extractTxId(resultsCreateTx.last) + + val resultsGetTx = runCmdLine( + CmdLine.getTx, + CmdLine.txId, txId, + CmdLine.walletId, testWalletId) + + assert(resultsGetTx.last.contains(txId), "The getTx result didn't contain the id") + //list Txs + + val postTxTime = ZonedDateTime.now().plusMinutes(50) + + def listWalletTxs: Seq[String] = runCmdLine( + CmdLine.listWalletTransactions, + CmdLine.minWithdrawal, "1", + CmdLine.start, preTxTime.toString, + CmdLine.walletId, testWalletId) + + var resultsListTxs = listWalletTxs + // Disable the retry for CI, there is a question mark around whether the + // wallet will always return the txId, hence the fudge. + var retryCount = 0 + if(retryCount > 0) { + val sleepInterval: Long = 1000 * 60 + println(s"Looking for $txId, this could take $retryCount * $sleepInterval ms...") + + while (!resultsListTxs.exists(_.contains(txId)) && retryCount > 0) { + resultsListTxs = listWalletTxs + retryCount -= 1 + resultsListTxs.foreach(println) + //It seems the wallet can be slow to react sometimes. + Thread.sleep(sleepInterval) + } + + assert(resultsListTxs.exists(_.contains(txId)), "TxId never shows up in list?") + } else if (!resultsListTxs.exists(_.contains(txId))) { + println(s"Warning: $txId NOT found in listTransactions.") + } + } + + def extractTxId(toStringCreateTransactionResult: String): String = { + toStringCreateTransactionResult.split(",").head.stripPrefix("CreateTransactionResponse(") + } + + + it should "delete test wallet 2" in { + runCmdLine( + CmdLine.deleteWallet, + CmdLine.walletId, testWallet2Id) + val results = runCmdLine( + CmdLine.getWallet, + CmdLine.walletId, testWallet2Id) + + assert(results.exists(!_.contains(testWalletId)), "Test wallet found after deletion?") + + } + +} diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala index 6238f86..a3822aa 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -6,13 +6,19 @@ import iog.psg.cardano.CardanoApi._ import iog.psg.cardano.CardanoApiCodec.TxState.TxState import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, SyncState, TxState, Units} +import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} +/** + * This script ran successfully in Aug 2020 + * It's purpose was to explore how the API works. + * It is left as a hint/starting point for developers as to how to use the API basics. + */ object CardanoApiTestScript { private implicit val system = ActorSystem("SingleRequest") - private implicit val context = system.dispatcher + private implicit val context = IOExecutionContext(system.dispatcher) def main(args: Array[String]): Unit = { @@ -22,7 +28,7 @@ object CardanoApiTestScript { val walletNameFrom = "alan1" val walletToMnem = GenericMnemonicSentence("enforce chicken cactus pupil wagon brother stuff pumpkin hobby noble genius fish air only sign hour apart fruit market acid beach top subway swear") val walletNameTo = "cardano-api-to" - val walletFromMnem = //GenericMnemonicSentence("sustain noble raise quarter elephant police smile exhibit pass goose acoustic muffin enrich march boy music ostrich maple predict song silk naive trip jump" + val walletFromMnem = GenericMnemonicSentence("reform illegal victory hurry guard bunker add volume bicycle sock dutch couch target portion soap") val walletToPassphrase = "password910" val walletFromPassphrase = "1234567890" @@ -37,9 +43,10 @@ object CardanoApiTestScript { val api = new CardanoApi(baseUri) + import api.Ops._ - def waitForTx(txState: TxState, walletId: String, txId: String):Unit = { - if(txState == TxState.pending) { + def waitForTx(txState: TxState, walletId: String, txId: String): Unit = { + if (txState == TxState.pending) { println(s"$txState") Thread.sleep(5000) val txUpdate = unwrap(api.getTransaction(walletId, txId).toFuture.executeBlocking) @@ -152,6 +159,7 @@ object CardanoApiTestScript { None, ).executeBlocking) + waitForTx(returnTx.status, toWallet.id, returnTx.id) println(s"Successfully transferred value between 2 wallets") @@ -180,9 +188,9 @@ object CardanoApiTestScript { system.terminate() } - private def unwrap[T](apiResult: Try[CardanoApiResponse[T]]): T = unwrapOpt(apiResult).get + private def unwrap[T:ClassTag](apiResult: CardanoApiResponse[T]): T = unwrapOpt(Try(apiResult)).get - private def unwrapOpt[T](apiResult: Try[CardanoApiResponse[T]]): Option[T] = apiResult match { + private def unwrapOpt[T: ClassTag](apiResult: Try[CardanoApiResponse[T]]): Option[T] = apiResult match { case Success(Left(ErrorMessage(message, code))) => println(s"API Error message $message, code $code") None From f6df71708254da82c428bb2cbf953f771c5f82a1 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Thu, 17 Sep 2020 09:18:54 +0100 Subject: [PATCH 06/39] Feature/psgs 38 publish with java programming interface (#2) * Add metadata into create transaction. * Add Java accessible API. * Make publish use jdk 11 * Make new test use shouldBe * Remove unused imports. * Move metamap parser to own class. * Rename flatten to prevent clash with Future.flatten. --- .github/workflows/ci.yml | 10 +- build.sbt | 9 +- cmdline.sh | 6 +- .../iog/psg/cardano/jpi/AddressFilter.java | 5 + .../java/iog/psg/cardano/jpi/CardanoApi.java | 174 ++++++++++++++++++ .../psg/cardano/jpi/CardanoApiBuilder.java | 72 ++++++++ .../jpi/ListTransactionsParamBuilder.java | 78 ++++++++ src/main/java/iog/psg/cardano/jpi/Order.java | 5 + .../scala/iog/psg/cardano/CardanoApi.scala | 82 ++++----- .../iog/psg/cardano/CardanoApiCodec.scala | 72 ++++---- .../iog/psg/cardano/CardanoApiMain.scala | 15 +- .../iog/psg/cardano/jpi/HelpExecute.scala | 37 ++++ .../cardano/util/StringToMetaMapParser.scala | 47 +++++ src/test/java/iog/psg/cardano/TestMain.java | 72 ++++++++ .../psg/cardano/jpi/CardanoApiFixture.java | 24 +++ .../iog/psg/cardano/jpi/JpiResponseCheck.java | 96 ++++++++++ src/test/resources/application.conf | 3 +- .../iog/psg/cardano/CardanoApiMainSpec.scala | 46 ++--- .../psg/cardano/CardanoApiTestScript.scala | 9 +- .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 101 ++++++++++ .../cardano/util/StringToMapParserSpec.scala | 29 +++ 21 files changed, 876 insertions(+), 116 deletions(-) create mode 100644 src/main/java/iog/psg/cardano/jpi/AddressFilter.java create mode 100644 src/main/java/iog/psg/cardano/jpi/CardanoApi.java create mode 100644 src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java create mode 100644 src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java create mode 100644 src/main/java/iog/psg/cardano/jpi/Order.java create mode 100644 src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala create mode 100644 src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala create mode 100644 src/test/java/iog/psg/cardano/TestMain.java create mode 100644 src/test/java/iog/psg/cardano/jpi/CardanoApiFixture.java create mode 100644 src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java create mode 100644 src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0595c0d..92e69c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,10 @@ jobs: steps: - uses: actions/checkout@v2 - name: Run tests - run: sbt coverage test coverageReport + uses: actions/setup-java@v1.4.2 + with: + java-version: '11.0.8' + - run: sbt coverage test coverageReport env: BASE_URL: http://cardano-wallet-testnet.iog.solutions:8090/v2/ - name: Archive code coverage results @@ -32,5 +35,8 @@ jobs: steps: - uses: actions/checkout@v2 - name: Package - run: sbt publish + uses: actions/setup-java@v1.4.2 + with: + java-version: '11.0.8' + - run: sbt publish diff --git a/build.sbt b/build.sbt index b2df00d..c801d82 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbtghpackages.TokenSource.{GitConfig,Or,Environment} name:= "psg-cardano-wallet-api" -version := "0.1.2-SNAPSHOT" +version := "0.1.3-SNAPSHOT" scalaVersion := "2.13.3" @@ -18,6 +18,8 @@ val akkaVersion = "2.6.8" val akkaHttpVersion = "10.2.0" val akkaHttpCirce = "1.31.0" val circeVersion = "0.13.0" +val scalaTestVersion = "3.1.2" + /** * Don't include a logger binding as this is a library for embedding @@ -31,11 +33,14 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-stream" % akkaVersion, "io.circe" %% "circe-generic-extras" % circeVersion, "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, - "org.scalatest" %% "scalatest" % "3.1.2" % Test + "org.scalatest" %% "scalatest" % scalaTestVersion % Test, + ) + javacOptions ++= Seq("-source", "11", "-target", "11") scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations") +parallelExecution in Test := false diff --git a/cmdline.sh b/cmdline.sh index f017f5f..cd28d99 100755 --- a/cmdline.sh +++ b/cmdline.sh @@ -1,8 +1,8 @@ #!/bin/bash -VER=0.1.1-SNAPSHOT -BASE_URL="http://cardano-wallet-testnet.iog.solutions:8090/v2/" - +VER=0.1.3-SNAPSHOT +#BASE_URL="http://cardano-wallet-testnet.iog.solutions:8090/v2/" +BASE_URL="http://localhost:8090/v2/" #run sbt assembly to create this jar exec java -jar target/scala-2.13/psg-cardano-wallet-api-assembly-${VER}.jar -baseUrl ${BASE_URL} "$@" diff --git a/src/main/java/iog/psg/cardano/jpi/AddressFilter.java b/src/main/java/iog/psg/cardano/jpi/AddressFilter.java new file mode 100644 index 0000000..cb126a7 --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/AddressFilter.java @@ -0,0 +1,5 @@ +package iog.psg.cardano.jpi; + +public enum AddressFilter { + USED, UNUSED +} diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java new file mode 100644 index 0000000..4f25191 --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java @@ -0,0 +1,174 @@ +package iog.psg.cardano.jpi; + +import iog.psg.cardano.CardanoApiCodec; + +import scala.Some; +import scala.jdk.javaapi.CollectionConverters; + +import scala.Enumeration; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + + +public class CardanoApi { + + private final iog.psg.cardano.CardanoApi api; + private final HelpExecute helpExecute; + + private CardanoApi() { + helpExecute = null; + api = null; + } + + public CardanoApi(iog.psg.cardano.CardanoApi api, HelpExecute helpExecute) { + this.helpExecute = helpExecute; + this.api = api; + Objects.requireNonNull(api, "Api cannot be null"); + Objects.requireNonNull(helpExecute, "HelpExecute cannot be null"); + } + + public CompletionStage createRestore( + String name, + String passphrase, + List mnemonicWordList, + int addressPoolGap) throws CardanoApiException { + CardanoApiCodec.MnemonicSentence mnem = createMnemonic(mnemonicWordList); + return helpExecute.execute( + api.createRestoreWallet(name, passphrase, mnem, option(addressPoolGap)) + ); + } + + public CompletionStage createTransaction( + String fromWalletId, + String passphrase, + List payments, + Map metadata, + String withdrawal + ) throws CardanoApiException { + + scala.Option> massaged = + metadata.isEmpty() ? + option(Optional.empty()): + option(helpExecute.toScalaImmutable(metadata) + ); + + return helpExecute.execute(api.createTransaction(fromWalletId, passphrase, + new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), + massaged, + option(withdrawal))); + } + + public CompletionStage createTransaction( + String fromWalletId, + String passphrase, + List payments, + Map metadata) throws CardanoApiException { + + return createTransaction(fromWalletId, passphrase, payments, metadata, "self"); + } + + public CompletionStage getWallet( + String fromWalletId) throws CardanoApiException { + + return helpExecute.execute( + api.getWallet(fromWalletId)); + } + + public CompletionStage deleteWallet( + String fromWalletId) throws CardanoApiException { + + return helpExecute.execute( + api.deleteWallet(fromWalletId)).thenApply(x -> null); + } + + public CompletionStage getTransaction( + String walletId, String transactionId) throws CardanoApiException { + + return helpExecute.execute( + api.getTransaction(walletId, transactionId)); + } + + public CompletionStage estimateFee( + String walletId, List payments) throws CardanoApiException { + return estimateFee(walletId, payments, "self"); + } + + public CompletionStage estimateFee( + String walletId, List payments, String withdrawal) throws CardanoApiException { + return helpExecute.execute( + api.estimateFee(walletId, + new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), + withdrawal)); + } + public CompletionStage fundPayments( + String walletId, List payments) throws CardanoApiException { + return helpExecute.execute( + api.fundPayments(walletId, + new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()))); + } + + public CompletionStage> listAddresses( + String walletId, AddressFilter addressFilter) throws CardanoApiException { + Enumeration.Value v = CardanoApiCodec.AddressFilter$.MODULE$.Value(addressFilter.name().toLowerCase()); + return helpExecute.execute( + api.listAddresses(walletId, scala.Option.apply(v))).thenApply(CollectionConverters::asJava); + } + + public CompletionStage> listAddresses( + String walletId) throws CardanoApiException { + return helpExecute.execute( + api.listAddresses(walletId, scala.Option.empty())).thenApply(CollectionConverters::asJava); + } + + + public CompletionStage> listTransactions( + ListTransactionsParamBuilder builder) throws CardanoApiException { + return helpExecute.execute( + api.listTransactions( + builder.getWalletId(), + option(builder.getStartTime()), + option(builder.getEndTime()), + builder.getOrder(), + option(builder.getMinwithdrawal()))) + .thenApply(CollectionConverters::asJava); + } + + + public CompletionStage> listWallets() throws CardanoApiException { + return helpExecute.execute( + api.listWallets()) + .thenApply(CollectionConverters::asJava); + } + + public CompletionStage updatePassphrase( + String walletId, + String oldPassphrase, + String newPassphrase) throws CardanoApiException { + + return helpExecute.execute(api.updatePassphrase(walletId, oldPassphrase, newPassphrase)).thenApply(x -> null); + } + + public CompletionStage networkInfo() throws CardanoApiException { + return helpExecute.execute(api.networkInfo()); + } + + + private static scala.Option option(final T value) { + return (value != null)?new Some(value):scala.Option.apply((T) null); + } + + private static scala.Option option(final Optional value) { + return value.map(CardanoApi::option).orElse(scala.Option.apply((T) null)); + } + + private static CardanoApiCodec.GenericMnemonicSentence createMnemonic(List wordList) { + return new CardanoApiCodec.GenericMnemonicSentence( + CollectionConverters.asScala(wordList).toIndexedSeq() + ); + } + +} diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java new file mode 100644 index 0000000..57dc818 --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java @@ -0,0 +1,72 @@ +package iog.psg.cardano.jpi; + +import akka.actor.ActorSystem; +import scala.concurrent.ExecutionContext; + +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class CardanoApiBuilder { + + final private String url; + private ExecutorService executorService; + private ExecutorService ioExecutorService; + private ActorSystem actorSystem; + + private CardanoApiBuilder() { + url = null; + } + + private CardanoApiBuilder(String url) { + this.url = url; + Objects.requireNonNull(url, + "Provide the url to a cardano wallet instance e.g. http://127.0.0.1:8090/v2/"); + } + + public static CardanoApiBuilder create(String url) { + return new CardanoApiBuilder(url); + } + + public CardanoApiBuilder withExecutorService(ExecutorService executorService) { + this.executorService = executorService; + Objects.requireNonNull(executorService, "ExecutorService is 'null'"); + return this; + } + + public CardanoApiBuilder withIOExecutorService(ExecutorService ioExecutorService) { + this.ioExecutorService = ioExecutorService; + Objects.requireNonNull(ioExecutorService, "IO ExecutorService is 'null'"); + return this; + } + + + public CardanoApiBuilder withActorSystem(ActorSystem actorSystem) { + this.actorSystem = actorSystem; + Objects.requireNonNull(actorSystem, "ActorSystem is 'null'"); + return this; + } + + public CardanoApi build() { + + if (actorSystem == null) { + actorSystem = ActorSystem.create("Cardano JPI ActorSystem"); + } + + if (ioExecutorService == null && executorService == null) { + ioExecutorService = Executors.newCachedThreadPool(); + executorService = ioExecutorService; + } else if(ioExecutorService == null) { + ioExecutorService = executorService; + } else if (executorService == null){ + executorService = ioExecutorService; + } + + ExecutionContext ec = ExecutionContext.fromExecutorService(executorService); + ExecutionContext ioEc = ExecutionContext.fromExecutorService(ioExecutorService); + iog.psg.cardano.CardanoApi api = new iog.psg.cardano.CardanoApi(url, ec, actorSystem); + HelpExecute helpExecute = new HelpExecute(ec, actorSystem); + return new CardanoApi(api, helpExecute); + } + +} diff --git a/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java b/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java new file mode 100644 index 0000000..b5535cd --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java @@ -0,0 +1,78 @@ +package iog.psg.cardano.jpi; + +import iog.psg.cardano.CardanoApi; +import scala.Enumeration; + +import java.time.ZonedDateTime; +import java.util.Objects; + +public class ListTransactionsParamBuilder { + + private final String walletId; + private Integer minwithdrawal = null; + private ZonedDateTime startTime = null; + private Order order = null; + + public String getWalletId() { + return walletId; + } + + public Enumeration.Value getOrder() { + + Enumeration.Value result = CardanoApi.Order$.MODULE$.Value(Order.DESCENDING.name().toLowerCase()); + if(order != null) { + result = CardanoApi.Order$.MODULE$.Value(order.name().toLowerCase()); + } + return result; + } + + public Integer getMinwithdrawal() { + return minwithdrawal; + } + + public ZonedDateTime getStartTime() { + return startTime; + } + + public ZonedDateTime getEndTime() { + return endTime; + } + + private ZonedDateTime endTime = null; + + private ListTransactionsParamBuilder() { + walletId = null; + } + + private ListTransactionsParamBuilder(String walletId) { + this.walletId = walletId; + Objects.requireNonNull(walletId, "WalletId cannot be null"); + } + + static ListTransactionsParamBuilder create(String walletId) { + return new ListTransactionsParamBuilder(walletId); + } + + + public ListTransactionsParamBuilder withEndTime(ZonedDateTime endTime) { + this.endTime = endTime; + return this; + } + + public ListTransactionsParamBuilder withStartTime(ZonedDateTime startTime) { + this.startTime = startTime; + return this; + } + + public ListTransactionsParamBuilder withOrder(Order order) { + this.order = order; + return this; + } + + public ListTransactionsParamBuilder withMinwithdrawal(int minwithdrawal) { + this.minwithdrawal = minwithdrawal; + return this; + } + + +} diff --git a/src/main/java/iog/psg/cardano/jpi/Order.java b/src/main/java/iog/psg/cardano/jpi/Order.java new file mode 100644 index 0000000..09e9a2d --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/Order.java @@ -0,0 +1,5 @@ +package iog.psg.cardano.jpi; + +public enum Order { + ASCENDING, DESCENDING +} diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 334d261..56871a3 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -12,7 +12,6 @@ import akka.http.scaladsl.model._ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ import io.circe.generic.extras.Configuration -import iog.psg.cardano.CardanoApi.IOExecutionContext import iog.psg.cardano.CardanoApi.Order.Order import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} @@ -24,15 +23,12 @@ import scala.concurrent.{Await, ExecutionContext, Future} */ object CardanoApi { - //Marker class, differenciate between multiple ec's in a client - case class IOExecutionContext(ec: ExecutionContext) { - implicit val ioEc: ExecutionContext = ec - } - case class ErrorMessage(message: String, code: String) type CardanoApiResponse[T] = Either[ErrorMessage, T] + type TxMetadata = Map[Long, String] + case class CardanoApiRequest[T](request: HttpRequest, mapper: HttpResponse => Future[CardanoApiResponse[T]]) object Order extends Enumeration { @@ -45,42 +41,23 @@ object CardanoApi { object CardanoApiOps { - implicit class TransformOp[T](val knot: Future[CardanoApiResponse[Future[CardanoApiResponse[T]]]]) extends AnyVal { - //Cannot call this transform as it clashes with futire - def unknot(implicit ec: ExecutionContext): Future[CardanoApiResponse[T]] = knot.flatMap { + implicit class FlattenOp[T](val knot: Future[CardanoApiResponse[Future[CardanoApiResponse[T]]]]) extends AnyVal { + + def flattenCardanoApiResponse(implicit ec: ExecutionContext): Future[CardanoApiResponse[T]] = knot.flatMap { case Left(errorMessage) => Future.successful(Left(errorMessage)) - case Right(vaue) => vaue + case Right(value) => value } } implicit class FutOp[T](val request: CardanoApiRequest[T]) extends AnyVal { def toFuture: Future[CardanoApiRequest[T]] = Future.successful(request) } - - - } - -} - -class CardanoApi(baseUriWithPort: String)(implicit ec: IOExecutionContext, as: ActorSystem) { - - import iog.psg.cardano.CardanoApi._ - import iog.psg.cardano.CardanoApiCodec._ - import AddressFilter.AddressFilter - import ec.ioEc - - private val wallets = s"${baseUriWithPort}wallets" - private val network = s"${baseUriWithPort}network" - - implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames - - - - object Ops { //tie execute to ioEc - implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]]) { + implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]])(implicit ec: ExecutionContext, as: ActorSystem) { def execute: Future[CardanoApiResponse[T]] = { - requestF.flatMap(_.execute) + requestF + .flatMap(r => new CardanoApiRequestOps(r) + .execute) } def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = @@ -88,18 +65,34 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: IOExecutionContext, as: A } - implicit class CardanoApiRequestOps[T](request: CardanoApiRequest[T]) { + implicit class CardanoApiRequestOps[T](request: CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem) { + def execute: Future[CardanoApiResponse[T]] = { - Http() - .singleRequest(request.request) - .flatMap(request.mapper) - } + + Http() + .singleRequest(request.request) + .flatMap(request.mapper) + } def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = Await.result(execute, maxWaitTime) } } + +} + +class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: ActorSystem) { + + import iog.psg.cardano.CardanoApi._ + import iog.psg.cardano.CardanoApiCodec._ + import AddressFilter.AddressFilter + + private val wallets = s"${baseUriWithPort}wallets" + private val network = s"${baseUriWithPort}network" + + implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + def listWallets: CardanoApiRequest[Seq[Wallet]] = CardanoApiRequest( HttpRequest( uri = wallets, @@ -176,11 +169,11 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: IOExecutionContext, as: A start: Option[ZonedDateTime] = None, end: Option[ZonedDateTime] = None, order: Order = Order.descendingOrder, - minWithdrawal: Int = 1): CardanoApiRequest[Seq[CreateTransactionResponse]] = { + minWithdrawal: Option[Int] = None): CardanoApiRequest[Seq[CreateTransactionResponse]] = { val baseUri = Uri(s"${wallets}/${walletId}/transactions") val queries = - Seq("start", "end", "order", "minWithdrawal").zip(Seq(start, end, order, Some(minWithdrawal))) + Seq("start", "end", "order", "minWithdrawal").zip(Seq(start, end, order, minWithdrawal)) .collect { case (queryParamName, Some(o: Order)) => queryParamName -> o.toString case (queryParamName, Some(dt: ZonedDateTime)) => queryParamName -> zonedDateToString(dt) @@ -199,13 +192,18 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: IOExecutionContext, as: A ) } - def createTransaction(fromWalletId: String, + def createTransaction[K <: Long](fromWalletId: String, passphrase: String, payments: Payments, + metadata: Option[Map[K, String]], withdrawal: Option[String] ): Future[CardanoApiRequest[CreateTransactionResponse]] = { - val createTx = CreateTransaction(passphrase, payments.payments, withdrawal) + val keyAsLongs = metadata.map(_.map { + case (k, v) => (k.longValue(), v) + }) + + val createTx = CreateTransaction(passphrase, payments.payments, keyAsLongs, withdrawal) Marshal(createTx).to[RequestEntity] map { marshalled => CardanoApiRequest( diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 031f278..9be9713 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -1,9 +1,10 @@ package iog.psg.cardano +import java.nio.charset.StandardCharsets import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import akka.http.scaladsl.model.ContentType.{Binary, WithFixedCharset} +import akka.http.scaladsl.model.ContentType.WithFixedCharset import akka.http.scaladsl.model.{ContentType, HttpEntity, HttpResponse, MediaTypes, StatusCodes} import akka.http.scaladsl.unmarshalling.Unmarshal import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller @@ -14,7 +15,7 @@ import io.circe.{Decoder, Encoder, Json} import io.circe.generic.auto._ import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder -import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, TxMetadata} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter import iog.psg.cardano.CardanoApiCodec.SyncState.SyncState import iog.psg.cardano.CardanoApiCodec.TxDirection.TxDirection @@ -27,35 +28,35 @@ import scala.util.{Failure, Success, Try} object CardanoApiCodec { - implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + private implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames - def dropNulls[A](encoder: Encoder[A]): Encoder[A] = + private[cardano] def dropNulls[A](encoder: Encoder[A]): Encoder[A] = encoder.mapJson(_.dropNullValues) - implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) - implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) + private[cardano] implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) + private[cardano] implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) - implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => + private[cardano] implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => stringToZonedDate(s) match { case Success(goodDateTime) => Right(goodDateTime) case Failure(exception) => Left(exception.toString) } } - implicit val decodeUnits: Decoder[Units] = Decoder.decodeString.map(Units.withName) - implicit val encodeUnits: Encoder[Units] = (a: Units) => Json.fromString(a.toString) + private[cardano] implicit val decodeUnits: Decoder[Units] = Decoder.decodeString.map(Units.withName) + private[cardano] implicit val encodeUnits: Encoder[Units] = (a: Units) => Json.fromString(a.toString) - implicit val decodeSyncState: Decoder[SyncState] = Decoder.decodeString.map(SyncState.withName) - implicit val encodeSyncState: Encoder[SyncState] = (a: SyncState) => Json.fromString(a.toString) + private[cardano] implicit val decodeSyncState: Decoder[SyncState] = Decoder.decodeString.map(SyncState.withName) + private[cardano] implicit val encodeSyncState: Encoder[SyncState] = (a: SyncState) => Json.fromString(a.toString) - implicit val decodeAddressFilter: Decoder[AddressFilter] = Decoder.decodeString.map(AddressFilter.withName) - implicit val encodeAddressFilter: Encoder[AddressFilter] = (a: AddressFilter) => Json.fromString(a.toString) + private[cardano] implicit val decodeAddressFilter: Decoder[AddressFilter] = Decoder.decodeString.map(AddressFilter.withName) + private[cardano] implicit val encodeAddressFilter: Encoder[AddressFilter] = (a: AddressFilter) => Json.fromString(a.toString) - implicit val decodeTxState: Decoder[TxState] = Decoder.decodeString.map(TxState.withName) - implicit val encodeTxState: Encoder[TxState] = (a: TxState) => Json.fromString(a.toString) + private[cardano] implicit val decodeTxState: Decoder[TxState] = Decoder.decodeString.map(TxState.withName) + private[cardano] implicit val encodeTxState: Encoder[TxState] = (a: TxState) => Json.fromString(a.toString) - implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) - implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) + private[cardano] implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) + private[cardano] implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) object AddressFilter extends Enumeration { type AddressFilter = Value @@ -85,7 +86,11 @@ object CardanoApiCodec { case class WalletAddressId(id: String, state: Option[AddressFilter]) - private[cardano] case class CreateTransaction(passphrase: String, payments: Seq[Payment], withdrawal: Option[String]) + private[cardano] case class CreateTransaction( + passphrase: String, + payments: Seq[Payment], + metadata: Option[TxMetadata], + withdrawal: Option[String]) private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String) @@ -116,19 +121,19 @@ object CardanoApiCodec { @ConfiguredJsonCodec case class NetworkInfo( - syncProgress: SyncStatus, - networkTip: NetworkTip, - nodeTip: NodeTip, - nextEpoch: NextEpoch - ) + syncProgress: SyncStatus, + networkTip: NetworkTip, + nodeTip: NodeTip, + nextEpoch: NextEpoch + ) @ConfiguredJsonCodec case class CreateRestore( - name: String, - passphrase: String, - mnemonicSentence: IndexedSeq[String], - addressPoolGap: Option[Int] = None - ) { + name: String, + passphrase: String, + mnemonicSentence: IndexedSeq[String], + addressPoolGap: Option[Int] = None + ) { require( mnemonicSentence.length == 15 || mnemonicSentence.length == 21 || @@ -219,7 +224,8 @@ object CardanoApiCodec { inputs: Seq[InAddress], outputs: Seq[OutAddress], withdrawals: Seq[StakeAddress], - status: TxState + status: TxState, + metadata: Option[TxMetadata] ) @ConfiguredJsonCodec @@ -255,7 +261,8 @@ object CardanoApiCodec { private def extractErrorResponse[T](strictEntity: Future[HttpEntity.Strict]): Future[CardanoApiResponse[T]] = { strictEntity.map(e => toErrorMessage(e.data) match { - case Left(err) => Left(ErrorMessage(err.getMessage, "UNPARSEABLE RESULT")) + case Left(err) => Left(ErrorMessage(err.getMessage, + Try(new String(e.data.toArray, StandardCharsets.UTF_8)).getOrElse("UNPARSEABLE"))) case Right(v) => Left(v) }) @@ -274,7 +281,10 @@ object CardanoApiCodec { // b. the Either unmarshaller requires it strictEntityF.flatMap(f) - case Binary(MediaTypes.`application/octet-stream`) => + case c: ContentType + if c.mediaType == MediaTypes.`text/plain` || + c.mediaType == MediaTypes.`application/octet-stream`=> + extractErrorResponse[T](strictEntityF) case c: ContentType => diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 465b43f..605d817 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -4,8 +4,10 @@ import java.io.File import java.time.ZonedDateTime import akka.actor.ActorSystem -import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, IOExecutionContext, Order, defaultMaxWaitTime} +import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, Order, TxMetadata, defaultMaxWaitTime} import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} +import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap import iog.psg.cardano.util._ import scala.reflect.ClassTag @@ -29,6 +31,7 @@ object CardanoApiMain { val updatePassphrase = "-updatePassphrase" val oldPassphrase = "-oldPassphrase" val passphrase = "-passphrase" + val metadata = "-metadata" val mnemonic = "-mnemonic" val addressPoolGap = "-addressPoolGap" val listWalletAddresses = "-listAddresses" @@ -67,6 +70,8 @@ object CardanoApiMain { } + + private[cardano] def run(arguments: ArgumentParser)(implicit trace: Trace): Unit = { @@ -81,7 +86,7 @@ object CardanoApiMain { } implicit val system: ActorSystem = ActorSystem("SingleRequest") - implicit val ioEc: IOExecutionContext = IOExecutionContext(system.dispatcher) + import system.dispatcher //the Try { @@ -90,7 +95,7 @@ object CardanoApiMain { trace(s"baseurl:$url") val api = new CardanoApi(url) - import api.Ops._ + if (hasArgument(CmdLine.netInfo)) { val result = unwrap(api.networkInfo.executeBlocking) @@ -143,12 +148,14 @@ object CardanoApiMain { val amount = arguments.get(CmdLine.amount).toLong val addr = arguments.get(CmdLine.address) val pass = arguments.get(CmdLine.passphrase) + val metadata = toMetaMap(arguments(CmdLine.metadata)) val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) val payments = Payments(Seq(singlePayment)) val result = unwrap(api.createTransaction( walletId, pass, payments, + metadata, None ).executeBlocking) trace(result) @@ -171,7 +178,7 @@ object CardanoApiMain { val startDate = arguments(CmdLine.start).map(strToZonedDateTime) val endDate = arguments(CmdLine.end).map(strToZonedDateTime) val orderOf = arguments(CmdLine.order).flatMap(s => Try(Order.withName(s)).toOption).getOrElse(Order.descendingOrder) - val minWithdrawalTx = arguments(CmdLine.minWithdrawal).map(_.toInt).getOrElse(1) + val minWithdrawalTx = arguments(CmdLine.minWithdrawal).map(_.toInt) val result = unwrap(api.listTransactions( walletId, diff --git a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala new file mode 100644 index 0000000..c02a6d4 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala @@ -0,0 +1,37 @@ +package iog.psg.cardano.jpi + +import java.util.concurrent.CompletionStage + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} + +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.MapHasAsScala +import scala.jdk.javaapi.FutureConverters + + +class CardanoApiException(message: String, code: String) extends Exception(s"Message: $message, Code: $code") + +class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) { + + @throws(classOf[CardanoApiException]) + private def unwrapResponse[T](resp: CardanoApiResponse[T]): T = resp match { + case Right(t) => t + case Left(ErrorMessage(message, code)) => + throw new CardanoApiException(message, code) + } + + @throws(classOf[CardanoApiException]) + def execute[T](request: iog.psg.cardano.CardanoApi.CardanoApiRequest[T]): CompletionStage[T] = { + FutureConverters.asJava(request.execute.map(unwrapResponse)) + } + + @throws(classOf[CardanoApiException]) + def execute[T](request: Future[iog.psg.cardano.CardanoApi.CardanoApiRequest[T]]): CompletionStage[T] = { + FutureConverters.asJava(request.execute.map(unwrapResponse)) + } + + def toScalaImmutable[B](in: java.util.Map[java.lang.Long,String]): Map[java.lang.Long, String] = in.asScala.toMap + +} diff --git a/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala b/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala new file mode 100644 index 0000000..0c4e4c0 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala @@ -0,0 +1,47 @@ +package iog.psg.cardano.util + +import iog.psg.cardano.CardanoApi.TxMetadata +import iog.psg.cardano.CardanoApiMain.fail + +import scala.util.{Failure, Success, Try} + +object StringToMetaMapParser { + + def toMetaMap(mapAsStringOpt: Option[String]): Option[TxMetadata] = mapAsStringOpt.flatMap { mapAsStr => + + if (mapAsStr.nonEmpty) { + + val parsedMap = mapAsStr + .split(":") + .grouped(2) + .map { + case Array(k, v) => k.toLongOption.toRight(k) -> v + } + + val (invalidKeys, goodMap) = Try { + parsedMap + .foldLeft((Seq.empty[String], Seq.empty[(Long, String)])) { + + case ((errors, goodTuples), (Right(k), v)) => + (errors, goodTuples :+ (k -> v)) + + case ((errors, goodTuples), (Left(k), _)) => + (errors :+ k, goodTuples) + } + + } match { + case Success(m) => m + case Failure(_) => + fail(s"Map failed to parse into key value pairs, use format 'k:v:k1:v1:k2:v2' " + + s"where all keys are numbers $mapAsStr") + } + if (invalidKeys.nonEmpty) { + fail(s"I can't parse '${invalidKeys.mkString(", ")}' to map, use format 'k:v:k1:v1:k2:v2' where all keys are numbers") + } else { + Some(goodMap.toMap) + } + } else None + + } + +} diff --git a/src/test/java/iog/psg/cardano/TestMain.java b/src/test/java/iog/psg/cardano/TestMain.java new file mode 100644 index 0000000..f32b438 --- /dev/null +++ b/src/test/java/iog/psg/cardano/TestMain.java @@ -0,0 +1,72 @@ +package iog.psg.cardano; + +import akka.actor.ActorSystem; +import iog.psg.cardano.jpi.*; +import iog.psg.cardano.jpi.CardanoApi; +import scala.Enumeration; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TestMain { + + public static void main(String[] args) throws CardanoApiException, ExecutionException, InterruptedException { + + try { + ActorSystem as = ActorSystem.create(); + ExecutorService es = Executors.newFixedThreadPool(10); + CardanoApiBuilder builder = + CardanoApiBuilder.create("http://localhost:8090/v2/") + .withActorSystem(as) + .withExecutorService(es); + + CardanoApi api = builder.build(); + String passphrase = "password10"; + String menmString = "receive post siren monkey mistake morning teach section mention rural idea say offer number ribbon toward rigid pluck begin ticket auto"; + List menmLst = Arrays.asList(menmString.split(" ")); + String walletId = "b63eacb4c89bd942cacfe0d3ed47459bbf0ce5c9"; + + + CardanoApiCodec.Wallet wallet = null; + try { + wallet = + api.getWallet(walletId).toCompletableFuture().get(); + } catch(Exception e) { + wallet = api.createRestore("cardanoapimainspec", passphrase, menmLst, 10).toCompletableFuture().get(); + } + + CardanoApiCodec.WalletAddressId unusedAddr = api.listAddresses(wallet.id(), AddressFilter.UNUSED).toCompletableFuture().get().get(0); + + Enumeration.Value lovelace = CardanoApiCodec.Units$.MODULE$.lovelace(); + Map meta = new HashMap(); + String l = Long.toString(Long.MAX_VALUE); + meta.put(Long.MAX_VALUE, "hello world"); + //9223372036854775807 + //meta.put(l, "0123456789012345678901234567890123456789012345678901234567890123"); + + List pays = List.of(new CardanoApiCodec.Payment(unusedAddr.id(), new CardanoApiCodec.QuantityUnit(1000000, lovelace))); + CardanoApiCodec.CreateTransactionResponse resp = + api.createTransaction( + wallet.id(), + passphrase, + pays, + meta, + "self").toCompletableFuture().get(); + System.out.println(resp.status().toString()); + System.out.println(resp.id()); + System.out.println(resp.metadata()); + + //executeHelper.execute(req); + } catch (Exception e) { + System.out.println(e.toString()); + } finally { + System.exit(9); + } + + } +} diff --git a/src/test/java/iog/psg/cardano/jpi/CardanoApiFixture.java b/src/test/java/iog/psg/cardano/jpi/CardanoApiFixture.java new file mode 100644 index 0000000..a23a348 --- /dev/null +++ b/src/test/java/iog/psg/cardano/jpi/CardanoApiFixture.java @@ -0,0 +1,24 @@ +package iog.psg.cardano.jpi; + +import akka.actor.ActorSystem; + +import java.util.Objects; + +public class CardanoApiFixture { + + public CardanoApi getJpi() { + return jpi; + } + + private final CardanoApi jpi; + + private CardanoApiFixture() { + jpi = null; + } + + public CardanoApiFixture(String url) { + Objects.requireNonNull(url); + ActorSystem as = ActorSystem.create("TESTING_CARDANO_JPI"); + jpi = CardanoApiBuilder.create(url).withActorSystem(as).build(); + } +} diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java new file mode 100644 index 0000000..d753065 --- /dev/null +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -0,0 +1,96 @@ +package iog.psg.cardano.jpi; + +import iog.psg.cardano.CardanoApiCodec; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class JpiResponseCheck { + + public final CardanoApi jpi; + private final long timeout; + private final TimeUnit timeoutUnit; + + private JpiResponseCheck() { + jpi = null; + timeout = 0; + timeoutUnit = null; + + } + + public JpiResponseCheck(CardanoApi jpi, long timeout, TimeUnit timeoutUnit) { + this.jpi = jpi; + this.timeoutUnit = timeoutUnit; + this.timeout = timeout; + } + + static String get(CardanoApiCodec.NetworkInfo info) { + return info.syncProgress().status().toString(); + } + + public void createBadWallet() throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { + List mnem = List.of("", "sdfa", "dfd"); + jpi.createRestore("some name", "password99", mnem, 4).toCompletableFuture().get(timeout, timeoutUnit); + } + + public boolean findOrCreateTestWallet(String ourWalletId, String ourWalletName, String walletPassphrase, List wordList, int addressPoolGap) throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { + List wallets = jpi.listWallets().toCompletableFuture().get(timeout, timeoutUnit); + for(CardanoApiCodec.Wallet w: wallets) { + if(w.id().contentEquals(ourWalletId)) { + return true; + } + } + CardanoApiCodec.Wallet created = jpi.createRestore(ourWalletName, walletPassphrase, wordList,addressPoolGap).toCompletableFuture().get(timeout, timeoutUnit); + return created.id().contentEquals(ourWalletId); + } + + public boolean getWallet(String walletId) throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { + CardanoApiCodec.Wallet w = jpi.getWallet(walletId).toCompletableFuture().get(timeout, timeoutUnit); + return w.id().contentEquals(walletId); + } + + public void passwordChange(String walletId, String passphrase, String newPassphrase) throws CardanoApiException, InterruptedException, ExecutionException, TimeoutException { + jpi.updatePassphrase(walletId, passphrase, newPassphrase).toCompletableFuture().get(timeout, timeoutUnit); + + } + + + public CardanoApiCodec.FundPaymentsResponse fundPayments(String walletId, long amountToTransfer) throws Exception { + List unused = jpi.listAddresses(walletId, AddressFilter.UNUSED).toCompletableFuture().get(timeout, timeoutUnit); + String unusedAddrId = unused.get(0).id(); + CardanoApiCodec.QuantityUnit amount = new CardanoApiCodec.QuantityUnit(amountToTransfer, CardanoApiCodec.Units$.MODULE$.lovelace()); + CardanoApiCodec.Payment p = new CardanoApiCodec.Payment(unusedAddrId, amount); + CardanoApiCodec.FundPaymentsResponse response = jpi.fundPayments(walletId, List.of(p)).toCompletableFuture().get(timeout, timeoutUnit); + return response; + } + + public void deleteWallet(String walletId) throws Exception { + jpi.deleteWallet(walletId).toCompletableFuture().get(timeout, timeoutUnit); + + } + + public CardanoApiCodec.CreateTransactionResponse paymentToSelf(String wallet1Id, String passphrase, int amountToTransfer, Map metadata) throws Exception { + + Map metadataLongKey = new HashMap(); + metadata.forEach((k,v) -> { + metadataLongKey.put(Long.parseLong(k), v); + }); + + List unused = jpi.listAddresses(wallet1Id, AddressFilter.UNUSED).toCompletableFuture().get(timeout, timeoutUnit); + String unusedAddrIdWallet1 = unused.get(0).id(); + CardanoApiCodec.QuantityUnit amount = new CardanoApiCodec.QuantityUnit(amountToTransfer, CardanoApiCodec.Units$.MODULE$.lovelace()); + List payments = List.of(new CardanoApiCodec.Payment(unusedAddrIdWallet1, amount)); + CardanoApiCodec.EstimateFeeResponse response = jpi.estimateFee(wallet1Id, payments).toCompletableFuture().get(timeout, timeoutUnit); + long max = response.estimatedMax().quantity(); + return jpi.createTransaction(wallet1Id, passphrase, payments, metadataLongKey).toCompletableFuture().get(timeout, timeoutUnit); + + } + + public CardanoApiCodec.CreateTransactionResponse getTx(String walletId, String txId) throws Exception { + return jpi.getTransaction(walletId, txId).toCompletableFuture().get(timeout, timeoutUnit); + } +} diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index 20898a3..e98fa8b 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -1,4 +1,4 @@ -cardano.wallet.baseUrl="http://localhost:8091/v2/" +cardano.wallet.baseUrl="http://localhost:8090/v2/" cardano.wallet.baseUrl=${?BASE_URL} cardano.wallet.passphrase="password10" cardano.wallet.passphrase=${?PASSPHRASE} @@ -6,6 +6,7 @@ cardano.wallet.name="cardanoapimainspec" cardano.wallet.amount=2000000 cardano.wallet.mnemonic="receive post siren monkey mistake morning teach section mention rural idea say offer number ribbon toward rigid pluck begin ticket auto" cardano.wallet.id="b63eacb4c89bd942cacfe0d3ed47459bbf0ce5c9" +cardano.wallet.metadata="0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" cardano.wallet2.mnemonic="later image spider wrestle tunnel bomb ahead glance broken merry still nominee property clever wedding reduce tribe buzz voyage clay sheriff" cardano.wallet2.id="527eac22af137dcd159fade57ab0686931feed7c" diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 620a408..6a6e6bd 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -6,15 +6,19 @@ import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi._ import iog.psg.cardano.CardanoApiMain.CmdLine import iog.psg.cardano.util.{ArgumentParser, Configure, Trace} +import org.scalatest.Ignore +import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { +import scala.concurrent.{Future, blocking} + + +class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures { private implicit val system = ActorSystem("SingleRequest") private implicit val context = system.dispatcher - private implicit val ioEc = IOExecutionContext(context) private val baseUrl = config.getString("cardano.wallet.baseUrl") private val testWalletName = config.getString("cardano.wallet.name") private val testWallet2Name = config.getString("cardano.wallet2.name") @@ -25,6 +29,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { private val testWalletPassphrase = config.getString("cardano.wallet.passphrase") private val testWallet2Passphrase = config.getString("cardano.wallet2.passphrase") private val testAmountToTransfer = config.getString("cardano.wallet.amount") + private val testMetadata = config.getString("cardano.wallet.metadata") private val defaultArgs = Array(CmdLine.baseUrl, baseUrl) @@ -48,7 +53,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { "The Cmd line Main" should "support retrieving netInfo" in { val results = runCmdLine(CmdLine.netInfo) - assert(results.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl'") + assert(results.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl' \n $results") } it should "not create a wallet with a bad mnemonic" in { @@ -122,7 +127,8 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { CmdLine.address, getUnusedAddressWallet2, CmdLine.walletId, testWalletId) - assert(results.last.contains("FundPaymentsResponse"), "FundPaymentsResponse failed?") + assert(results.last.contains("FundPaymentsResponse") || + results.mkString("").contains("cannot_cover_fee"), s"$results") //results.foreach(println) } @@ -148,7 +154,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { cleanedUp.head } - it should "transact from a to a" in { + it should "transact from a to a with metadata" in { val unusedAddr = getUnusedAddressWallet1 @@ -161,12 +167,13 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { //estimateResults.foreach(println) - val preTxTime = ZonedDateTime.now().minusMinutes(10) + val preTxTime = ZonedDateTime.now().minusMinutes(1) val resultsCreateTx = runCmdLine( CmdLine.createTx, CmdLine.passphrase, testWalletPassphrase, CmdLine.amount, testAmountToTransfer, + CmdLine.metadata, testMetadata, CmdLine.address, unusedAddr, CmdLine.walletId, testWalletId) @@ -182,34 +189,18 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { assert(resultsGetTx.last.contains(txId), "The getTx result didn't contain the id") //list Txs - val postTxTime = ZonedDateTime.now().plusMinutes(50) + val postTxTime = ZonedDateTime.now().plusMinutes(5) def listWalletTxs: Seq[String] = runCmdLine( CmdLine.listWalletTransactions, - CmdLine.minWithdrawal, "1", CmdLine.start, preTxTime.toString, + CmdLine.`end`, postTxTime.toString, CmdLine.walletId, testWalletId) - var resultsListTxs = listWalletTxs - // Disable the retry for CI, there is a question mark around whether the - // wallet will always return the txId, hence the fudge. - var retryCount = 0 - if(retryCount > 0) { - val sleepInterval: Long = 1000 * 60 - println(s"Looking for $txId, this could take $retryCount * $sleepInterval ms...") - - while (!resultsListTxs.exists(_.contains(txId)) && retryCount > 0) { - resultsListTxs = listWalletTxs - retryCount -= 1 - resultsListTxs.foreach(println) - //It seems the wallet can be slow to react sometimes. - Thread.sleep(sleepInterval) - } - assert(resultsListTxs.exists(_.contains(txId)), "TxId never shows up in list?") - } else if (!resultsListTxs.exists(_.contains(txId))) { - println(s"Warning: $txId NOT found in listTransactions.") - } + val foundTx = listWalletTxs.exists(_.contains(txId)) + assert(foundTx, s"Couldn't find txId $txId in transactions ") + } def extractTxId(toStringCreateTransactionResult: String): String = { @@ -229,4 +220,5 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure { } + } diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala index a3822aa..d10d0a8 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -18,7 +18,6 @@ object CardanoApiTestScript { private implicit val system = ActorSystem("SingleRequest") - private implicit val context = IOExecutionContext(system.dispatcher) def main(args: Array[String]): Unit = { @@ -41,9 +40,9 @@ object CardanoApiTestScript { println(s"Using base url '$baseUri''") println(s"Using wallet name '$walletNameFrom''") + import system.dispatcher val api = new CardanoApi(baseUri) - import api.Ops._ def waitForTx(txState: TxState, walletId: String, txId: String): Unit = { if (txState == TxState.pending) { @@ -128,6 +127,7 @@ object CardanoApiTestScript { fromWallet.id, walletFromPassphrase, payments, + None, //TODO add metadata None, ).executeBlocking) @@ -156,7 +156,8 @@ object CardanoApiTestScript { toWallet.id, walletToPassphrase, returnPayments2, - None, + Some(Map(1L -> "")), //todo ADD metadata + None ).executeBlocking) @@ -188,7 +189,7 @@ object CardanoApiTestScript { system.terminate() } - private def unwrap[T:ClassTag](apiResult: CardanoApiResponse[T]): T = unwrapOpt(Try(apiResult)).get + private def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T]): T = unwrapOpt(Try(apiResult)).get private def unwrapOpt[T: ClassTag](apiResult: Try[CardanoApiResponse[T]]): Option[T] = apiResult match { case Success(Left(ErrorMessage(message, code))) => diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala new file mode 100644 index 0000000..1f2c861 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -0,0 +1,101 @@ +package iog.psg.cardano.jpi + +import java.util.concurrent.TimeUnit + +import iog.psg.cardano.CardanoApiCodec.GenericMnemonicSentence +import iog.psg.cardano.util.Configure +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} + + +class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { + + private val baseUrl = config.getString("cardano.wallet.baseUrl") + private val testWalletName = config.getString("cardano.wallet.name") + private val testWallet2Name = config.getString("cardano.wallet2.name") + private val testWalletMnemonic = config.getString("cardano.wallet.mnemonic") + private val testWallet2Mnemonic = config.getString("cardano.wallet2.mnemonic") + private val testWalletId = config.getString("cardano.wallet.id") + private val testWallet2Id = config.getString("cardano.wallet2.id") + private val testWalletPassphrase = config.getString("cardano.wallet.passphrase") + private val testWallet2Passphrase = config.getString("cardano.wallet2.passphrase") + private val testAmountToTransfer = config.getString("cardano.wallet.amount") + private val timeoutValue: Long = 10 + private val timeoutUnits = TimeUnit.SECONDS + private lazy val sut = new JpiResponseCheck(new CardanoApiFixture(baseUrl).getJpi,timeoutValue, timeoutUnits) + + "NetworkInfo status" should "be 'ready'" in { + val info = sut.jpi.networkInfo().toCompletableFuture.get(timeoutValue, timeoutUnits) + val networkState = JpiResponseCheck.get(info) + networkState shouldBe "ready" + } + + "Bad wallet creation" should "be prevented" in { + an [IllegalArgumentException] shouldBe thrownBy (sut.createBadWallet()) + } + + "Test wallet" should "exist or be created" in { + val mnem = GenericMnemonicSentence(testWalletMnemonic) + sut + .findOrCreateTestWallet( + testWalletId, + testWalletName, + testWalletPassphrase, + mnem.mnemonicSentence.asJava, 10) shouldBe true + } + + it should "get our wallet" in { + sut.getWallet(testWalletId) shouldBe true + } + + it should "create r find wallet 2" in { + val mnem = GenericMnemonicSentence(testWallet2Mnemonic) + sut + .findOrCreateTestWallet( + testWallet2Id, + testWallet2Name, + testWallet2Passphrase, + mnem.mnemonicSentence.asJava, 10) shouldBe true + } + + it should "allow password change in wallet 2" in { + sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase) + //now this is the wrong password + an [Exception] shouldBe thrownBy(sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase)) + + sut.passwordChange(testWallet2Id, testWalletPassphrase, testWallet2Passphrase) + } + + it should "fund payments" in { + val response = sut.fundPayments(testWalletId, testAmountToTransfer.toInt) + + } + + it should "transact from a to a with metadata" in { + + val metadata: Map[String, String] = Map( + Long.box(Long.MaxValue).toString -> "0" * 64, + Long.box(Long.MaxValue - 1).toString -> "1" * 64 + ) + + val createTxResponse = + sut.paymentToSelf(testWalletId, testWalletPassphrase, testAmountToTransfer.toInt, metadata.asJava) + val id = createTxResponse.id + val getTxResponse = sut.getTx(testWalletId, createTxResponse.id) + + createTxResponse.id shouldBe getTxResponse.id + createTxResponse.amount shouldBe getTxResponse.amount + createTxResponse.metadata.get.size shouldBe 2 + createTxResponse.metadata.get.apply(Long.MaxValue) shouldBe "0" * 64 + createTxResponse.metadata.get.apply(Long.MaxValue - 1) shouldBe "1" * 64 + } + + + it should "delete wallet 2" in { + sut.deleteWallet(testWallet2Id) + an [Exception] shouldBe thrownBy (sut.getWallet(testWallet2Id), "Wallet should not be retrieved") + } +} + diff --git a/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala b/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala new file mode 100644 index 0000000..b2bf034 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala @@ -0,0 +1,29 @@ +package iog.psg.cardano.util + +import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +/** + */ +class StringToMapParserSpec extends AnyFlatSpec with Matchers { + + "MetaMapParser" should "parse simple map" in { + toMetaMap(Some("1:a:2:b")) shouldBe Some(Map((1 -> "a"), 2 -> "b")) + } + + it should "correctly parse an empty string" in { + toMetaMap(Some("")) shouldBe None + } + + it should "fail on unbalanced input (no value for key)" in { + an [RuntimeException] shouldBe thrownBy( toMetaMap(Some("1:a:2"))) + } + + + it should "fail and show all bad keys if key not type 'long'" in { + val ex = the [RuntimeException] thrownBy( toMetaMap(Some("1:a:a:b:g:r"))) + ex.toString should include ("a, g") + } +} + From 17d588a2473a6a264c22c29fda3d4d1d45f9f0d9 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Wed, 23 Sep 2020 11:56:44 +0100 Subject: [PATCH 07/39] Feature/psgs 38 publish with java programming interface (#3) * Add Java API * Add metadata into create transaction. * Post review changes. --- .github/workflows/ci.yml | 5 + build.sbt | 6 +- .../java/iog/psg/cardano/jpi/CardanoApi.java | 26 ++-- .../psg/cardano/jpi/CardanoApiBuilder.java | 17 +-- .../iog/psg/cardano/jpi/MetadataBuilder.java | 25 ++++ .../scala/iog/psg/cardano/CardanoApi.scala | 60 ++++----- .../iog/psg/cardano/CardanoApiCodec.scala | 76 ++++++++++-- .../iog/psg/cardano/CardanoApiMain.scala | 5 +- .../iog/psg/cardano/jpi/HelpExecute.scala | 15 ++- .../cardano/util/StringToMetaMapParser.scala | 10 +- src/test/java/iog/psg/cardano/TestMain.java | 15 ++- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 13 +- .../psg/cardano/CardanoApiTestScript.scala | 117 ++++++------------ .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 21 ++-- .../cardano/util/StringToMapParserSpec.scala | 7 +- 15 files changed, 231 insertions(+), 187 deletions(-) create mode 100644 src/main/java/iog/psg/cardano/jpi/MetadataBuilder.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92e69c0..77ce6f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,11 @@ jobs: - run: sbt coverage test coverageReport env: BASE_URL: http://cardano-wallet-testnet.iog.solutions:8090/v2/ + WALLET_1_MNEM: ${{ secrets.WALLET_1_MNEM }} + WALLET_2_MNEM: ${{ secrets.WALLET_2_MNEM }} + WALLET_1_PASS: ${{ secrets.WALLET_1_PASS }} + WALLET_2_PASS: ${{ secrets.WALLET_2_PASS }} + - name: Archive code coverage results uses: actions/upload-artifact@v2 with: diff --git a/build.sbt b/build.sbt index c801d82..c142e59 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ val akkaHttpVersion = "10.2.0" val akkaHttpCirce = "1.31.0" val circeVersion = "0.13.0" val scalaTestVersion = "3.1.2" - +val commonsCodecVersion = "1.15" /** * Don't include a logger binding as this is a library for embedding @@ -33,12 +33,12 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-stream" % akkaVersion, "io.circe" %% "circe-generic-extras" % circeVersion, "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, + "commons-codec" % "commons-codec" % commonsCodecVersion, "org.scalatest" %% "scalatest" % scalaTestVersion % Test, - ) -javacOptions ++= Seq("-source", "11", "-target", "11") +javacOptions ++= Seq("-source", "1.8", "-target", "1.8") scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations") diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java index 4f25191..edb0813 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java @@ -1,14 +1,11 @@ package iog.psg.cardano.jpi; import iog.psg.cardano.CardanoApiCodec; - +import scala.Enumeration; import scala.Some; import scala.jdk.javaapi.CollectionConverters; -import scala.Enumeration; - import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletionStage; @@ -42,33 +39,27 @@ public CompletionStage createRestore( ); } - public CompletionStage createTransaction( + public CompletionStage createTransaction( String fromWalletId, String passphrase, List payments, - Map metadata, + CardanoApiCodec.TxMetadataIn metadata, String withdrawal ) throws CardanoApiException { - scala.Option> massaged = - metadata.isEmpty() ? - option(Optional.empty()): - option(helpExecute.toScalaImmutable(metadata) - ); - return helpExecute.execute(api.createTransaction(fromWalletId, passphrase, new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), - massaged, + option(metadata), option(withdrawal))); } public CompletionStage createTransaction( String fromWalletId, String passphrase, - List payments, - Map metadata) throws CardanoApiException { + List payments + ) throws CardanoApiException { - return createTransaction(fromWalletId, passphrase, payments, metadata, "self"); + return createTransaction(fromWalletId, passphrase, payments, null, "self"); } public CompletionStage getWallet( @@ -104,6 +95,7 @@ public CompletionStage estimateFee( new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), withdrawal)); } + public CompletionStage fundPayments( String walletId, List payments) throws CardanoApiException { return helpExecute.execute( @@ -158,7 +150,7 @@ public CompletionStage networkInfo() throws Cardano private static scala.Option option(final T value) { - return (value != null)?new Some(value):scala.Option.apply((T) null); + return (value != null) ? new Some(value) : scala.Option.apply((T) null); } private static scala.Option option(final Optional value) { diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java index 57dc818..8a13a23 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java @@ -11,7 +11,6 @@ public class CardanoApiBuilder { final private String url; private ExecutorService executorService; - private ExecutorService ioExecutorService; private ActorSystem actorSystem; private CardanoApiBuilder() { @@ -34,12 +33,6 @@ public CardanoApiBuilder withExecutorService(ExecutorService executorService) { return this; } - public CardanoApiBuilder withIOExecutorService(ExecutorService ioExecutorService) { - this.ioExecutorService = ioExecutorService; - Objects.requireNonNull(ioExecutorService, "IO ExecutorService is 'null'"); - return this; - } - public CardanoApiBuilder withActorSystem(ActorSystem actorSystem) { this.actorSystem = actorSystem; @@ -53,17 +46,11 @@ public CardanoApi build() { actorSystem = ActorSystem.create("Cardano JPI ActorSystem"); } - if (ioExecutorService == null && executorService == null) { - ioExecutorService = Executors.newCachedThreadPool(); - executorService = ioExecutorService; - } else if(ioExecutorService == null) { - ioExecutorService = executorService; - } else if (executorService == null){ - executorService = ioExecutorService; + if (executorService == null) { + executorService = Executors.newCachedThreadPool(); } ExecutionContext ec = ExecutionContext.fromExecutorService(executorService); - ExecutionContext ioEc = ExecutionContext.fromExecutorService(ioExecutorService); iog.psg.cardano.CardanoApi api = new iog.psg.cardano.CardanoApi(url, ec, actorSystem); HelpExecute helpExecute = new HelpExecute(ec, actorSystem); return new CardanoApi(api, helpExecute); diff --git a/src/main/java/iog/psg/cardano/jpi/MetadataBuilder.java b/src/main/java/iog/psg/cardano/jpi/MetadataBuilder.java new file mode 100644 index 0000000..b7f8ca4 --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/MetadataBuilder.java @@ -0,0 +1,25 @@ +package iog.psg.cardano.jpi; + +import io.circe.Json; +import iog.psg.cardano.CardanoApiCodec; + +import java.util.Map; + +public class MetadataBuilder { + + private MetadataBuilder() { } + + public static CardanoApiCodec.JsonMetadata withJson(Json metadataCompliantJson) { + return new CardanoApiCodec.JsonMetadata(metadataCompliantJson); + } + + public static CardanoApiCodec.JsonMetadata withJsonString(String metadataCompliantJson) { + return CardanoApiCodec.JsonMetadata$.MODULE$.apply(metadataCompliantJson); + } + + public static CardanoApiCodec.TxMetadataMapIn withMap(Map metadataMap) { + return new CardanoApiCodec.TxMetadataMapIn( + HelpExecute$.MODULE$.toMetadataMap(metadataMap)); + } + +} diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 56871a3..64fb0b8 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -13,7 +13,6 @@ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ import io.circe.generic.extras.Configuration import iog.psg.cardano.CardanoApi.Order.Order - import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} import scala.concurrent.{Await, ExecutionContext, Future} @@ -27,8 +26,6 @@ object CardanoApi { type CardanoApiResponse[T] = Either[ErrorMessage, T] - type TxMetadata = Map[Long, String] - case class CardanoApiRequest[T](request: HttpRequest, mapper: HttpResponse => Future[CardanoApiResponse[T]]) object Order extends Enumeration { @@ -42,22 +39,21 @@ object CardanoApi { object CardanoApiOps { implicit class FlattenOp[T](val knot: Future[CardanoApiResponse[Future[CardanoApiResponse[T]]]]) extends AnyVal { - + def flattenCardanoApiResponse(implicit ec: ExecutionContext): Future[CardanoApiResponse[T]] = knot.flatMap { case Left(errorMessage) => Future.successful(Left(errorMessage)) - case Right(value) => value + case Right(vaue) => vaue } } implicit class FutOp[T](val request: CardanoApiRequest[T]) extends AnyVal { def toFuture: Future[CardanoApiRequest[T]] = Future.successful(request) } + //tie execute to ioEc implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]])(implicit ec: ExecutionContext, as: ActorSystem) { def execute: Future[CardanoApiResponse[T]] = { - requestF - .flatMap(r => new CardanoApiRequestOps(r) - .execute) + requestF.flatMap(_.execute) } def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = @@ -192,18 +188,16 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act ) } - def createTransaction[K <: Long](fromWalletId: String, + + def createTransaction(fromWalletId: String, passphrase: String, payments: Payments, - metadata: Option[Map[K, String]], + metadata: Option[TxMetadataIn], withdrawal: Option[String] ): Future[CardanoApiRequest[CreateTransactionResponse]] = { - val keyAsLongs = metadata.map(_.map { - case (k, v) => (k.longValue(), v) - }) - val createTx = CreateTransaction(passphrase, payments.payments, keyAsLongs, withdrawal) + val createTx = CreateTransaction(passphrase, payments.payments, metadata, withdrawal) Marshal(createTx).to[RequestEntity] map { marshalled => CardanoApiRequest( @@ -218,9 +212,9 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } def estimateFee(fromWalletId: String, - payments: Payments, - withdrawal: String = "self" - ): Future[CardanoApiRequest[EstimateFeeResponse]] = { + payments: Payments, + withdrawal: String = "self" + ): Future[CardanoApiRequest[EstimateFeeResponse]] = { val estimateFees = EstimateFee(payments.payments, withdrawal) @@ -250,9 +244,9 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } - def getTransaction( - walletId: String, - transactionId: String): CardanoApiRequest[CreateTransactionResponse] = { + def getTransaction[T <: TxMetadataIn]( + walletId: String, + transactionId: String): CardanoApiRequest[CreateTransactionResponse] = { val uri = Uri(s"${wallets}/${walletId}/transactions/${transactionId}") @@ -270,19 +264,19 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act oldPassphrase: String, newPassphrase: String): Future[CardanoApiRequest[Unit]] = { - val uri = Uri(s"${wallets}/${walletId}/passphrase") - val updater = UpdatePassphrase(oldPassphrase, newPassphrase) - - Marshal(updater).to[RequestEntity] map { marshalled => { - CardanoApiRequest( - HttpRequest( - uri = uri, - method = PUT, - entity = marshalled - ), - _.toUnit - ) - } + val uri = Uri(s"${wallets}/${walletId}/passphrase") + val updater = UpdatePassphrase(oldPassphrase, newPassphrase) + + Marshal(updater).to[RequestEntity] map { marshalled => { + CardanoApiRequest( + HttpRequest( + uri = uri, + method = PUT, + entity = marshalled + ), + _.toUnit + ) + } } } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 9be9713..2c78160 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -5,25 +5,27 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import akka.http.scaladsl.model.ContentType.WithFixedCharset -import akka.http.scaladsl.model.{ContentType, HttpEntity, HttpResponse, MediaTypes, StatusCodes} +import akka.http.scaladsl.model._ import akka.http.scaladsl.unmarshalling.Unmarshal import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer import akka.util.ByteString import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ -import io.circe.{Decoder, Encoder, Json} import io.circe.generic.auto._ import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder -import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, TxMetadata} +import io.circe.syntax.EncoderOps +import io.circe._ +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter import iog.psg.cardano.CardanoApiCodec.SyncState.SyncState import iog.psg.cardano.CardanoApiCodec.TxDirection.TxDirection import iog.psg.cardano.CardanoApiCodec.TxState.TxState import iog.psg.cardano.CardanoApiCodec.Units.Units +import org.apache.commons.codec.binary.Hex -import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.FiniteDuration +import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} object CardanoApiCodec { @@ -58,6 +60,63 @@ object CardanoApiCodec { private[cardano] implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) private[cardano] implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) + + final case class TxMetadataOut(json: Json) { + def toMapMetadataStr: Decoder.Result[Map[Long, String]] = json.as[Map[Long, String]] + } + + private[cardano] implicit val decodeTxMetadataOut: Decoder[TxMetadataOut] = Decoder.decodeJson.map(TxMetadataOut) + private[cardano] implicit val decodeKeyMetadata: KeyDecoder[MetadataKey] = (key: String) => Some(MetadataValueStr(key)) + + sealed trait MetadataValue + + sealed trait MetadataKey extends MetadataValue + + sealed trait TxMetadataIn + + final case class TxMetadataMapIn[K <: Long](m: Map[K, MetadataValue]) extends TxMetadataIn + + object JsonMetadata { + def apply(str: String): JsonMetadata = JsonMetadata(str.asJson) + } + + final case class JsonMetadata(metadataCompliantJson: Json) extends TxMetadataIn + + final case class MetadataValueLong(l: Long) extends MetadataKey + + final case class MetadataValueStr(s: String) extends MetadataKey + + final case class MetadataValueArray(ary: Seq[MetadataValue]) extends MetadataValue + + final case class MetadataValueByteArray(ary: ByteString) extends MetadataValue + + final case class MetadataValueObject(s: Map[MetadataKey, MetadataValue]) extends MetadataValue + + implicit val metadataKeyDecoder: KeyEncoder[MetadataKey] = { + case MetadataValueLong(l) => l.toString + case MetadataValueStr(s) => s + } + + def toMetadataHex(bs: ByteString): Json = { + val asHex = Hex.encodeHex(bs.toArray[Byte]) + asHex.asJson + } + + implicit val encodeTxMeta: Encoder[MetadataValue] = Encoder.instance { + case MetadataValueLong(s) => s.asJson + case MetadataValueStr(s) => s.asJson + case MetadataValueArray(s) => s.asJson + case MetadataValueObject(s) => s.asJson + case MetadataValueByteArray(ary: ByteString) => toMetadataHex(ary) + } + + implicit val encodeTxMetadata: Encoder[TxMetadataIn] = Encoder.instance { + case JsonMetadata(metadataCompliantJson) => metadataCompliantJson + case TxMetadataMapIn(s) => s.asJson + + } + + object AddressFilter extends Enumeration { type AddressFilter = Value @@ -89,7 +148,7 @@ object CardanoApiCodec { private[cardano] case class CreateTransaction( passphrase: String, payments: Seq[Payment], - metadata: Option[TxMetadata], + metadata: Option[TxMetadataIn], withdrawal: Option[String]) private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String) @@ -225,7 +284,7 @@ object CardanoApiCodec { outputs: Seq[OutAddress], withdrawals: Seq[StakeAddress], status: TxState, - metadata: Option[TxMetadata] + metadata: Option[TxMetadataOut] ) @ConfiguredJsonCodec @@ -261,8 +320,7 @@ object CardanoApiCodec { private def extractErrorResponse[T](strictEntity: Future[HttpEntity.Strict]): Future[CardanoApiResponse[T]] = { strictEntity.map(e => toErrorMessage(e.data) match { - case Left(err) => Left(ErrorMessage(err.getMessage, - Try(new String(e.data.toArray, StandardCharsets.UTF_8)).getOrElse("UNPARSEABLE"))) + case Left(err) => Left(ErrorMessage(err.getMessage, "UNPARSEABLE RESULT")) case Right(v) => Left(v) }) @@ -283,7 +341,7 @@ object CardanoApiCodec { case c: ContentType if c.mediaType == MediaTypes.`text/plain` || - c.mediaType == MediaTypes.`application/octet-stream`=> + c.mediaType == MediaTypes.`application/octet-stream` => extractErrorResponse[T](strictEntityF) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 605d817..d9990c9 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -5,7 +5,7 @@ import java.time.ZonedDateTime import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} -import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, Order, TxMetadata, defaultMaxWaitTime} +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, Order, defaultMaxWaitTime} import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap import iog.psg.cardano.util._ @@ -71,7 +71,6 @@ object CardanoApiMain { } - private[cardano] def run(arguments: ArgumentParser)(implicit trace: Trace): Unit = { @@ -238,7 +237,7 @@ object CardanoApiMain { def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T])(implicit t: Trace): T = unwrapOpt(Try(apiResult)).get - def unwrapOpt[T:ClassTag](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { + def unwrapOpt[T: ClassTag](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { case Success(Left(ErrorMessage(message, code))) => trace(s"API Error message $message, code $code") None diff --git a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala index c02a6d4..2f01fae 100644 --- a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala +++ b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala @@ -5,6 +5,7 @@ import java.util.concurrent.CompletionStage import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} +import iog.psg.cardano.CardanoApiCodec.{MetadataValue, MetadataValueStr, TxMetadataMapIn} import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.MapHasAsScala @@ -13,6 +14,17 @@ import scala.jdk.javaapi.FutureConverters class CardanoApiException(message: String, code: String) extends Exception(s"Message: $message, Code: $code") +object HelpExecute { + + def toScalaImmutable[B](in: java.util.Map[java.lang.Long, String]): Map[java.lang.Long, String] = in.asScala.toMap + + def toMetadataMap(in: java.util.Map[java.lang.Long, String]): Map[Long, MetadataValue] = { + in.asScala.map { + case (k, v) => k.toLong -> MetadataValueStr (v) + } + }.toMap +} + class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) { @throws(classOf[CardanoApiException]) @@ -32,6 +44,7 @@ class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) { FutureConverters.asJava(request.execute.map(unwrapResponse)) } - def toScalaImmutable[B](in: java.util.Map[java.lang.Long,String]): Map[java.lang.Long, String] = in.asScala.toMap + def toScalaImmutable[B](in: java.util.Map[java.lang.Long, String]): Map[java.lang.Long, String] = + HelpExecute.toScalaImmutable(in) } diff --git a/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala b/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala index 0c4e4c0..74904e9 100644 --- a/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala +++ b/src/main/scala/iog/psg/cardano/util/StringToMetaMapParser.scala @@ -1,13 +1,13 @@ package iog.psg.cardano.util -import iog.psg.cardano.CardanoApi.TxMetadata +import iog.psg.cardano.CardanoApiCodec.{MetadataValueStr, TxMetadataMapIn} import iog.psg.cardano.CardanoApiMain.fail import scala.util.{Failure, Success, Try} object StringToMetaMapParser { - def toMetaMap(mapAsStringOpt: Option[String]): Option[TxMetadata] = mapAsStringOpt.flatMap { mapAsStr => + def toMetaMap(mapAsStringOpt: Option[String]): Option[TxMetadataMapIn[Long]] = mapAsStringOpt.flatMap { mapAsStr => if (mapAsStr.nonEmpty) { @@ -15,12 +15,12 @@ object StringToMetaMapParser { .split(":") .grouped(2) .map { - case Array(k, v) => k.toLongOption.toRight(k) -> v + case Array(k, v) => k.toLongOption.toRight(k) -> MetadataValueStr(v) } val (invalidKeys, goodMap) = Try { parsedMap - .foldLeft((Seq.empty[String], Seq.empty[(Long, String)])) { + .foldLeft((Seq.empty[String], Seq.empty[(Long, MetadataValueStr)])) { case ((errors, goodTuples), (Right(k), v)) => (errors, goodTuples :+ (k -> v)) @@ -38,7 +38,7 @@ object StringToMetaMapParser { if (invalidKeys.nonEmpty) { fail(s"I can't parse '${invalidKeys.mkString(", ")}' to map, use format 'k:v:k1:v1:k2:v2' where all keys are numbers") } else { - Some(goodMap.toMap) + Some(TxMetadataMapIn(goodMap.toMap)) } } else None diff --git a/src/test/java/iog/psg/cardano/TestMain.java b/src/test/java/iog/psg/cardano/TestMain.java index f32b438..c60f8c1 100644 --- a/src/test/java/iog/psg/cardano/TestMain.java +++ b/src/test/java/iog/psg/cardano/TestMain.java @@ -5,10 +5,7 @@ import iog.psg.cardano.jpi.CardanoApi; import scala.Enumeration; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -46,16 +43,22 @@ public static void main(String[] args) throws CardanoApiException, ExecutionExce Map meta = new HashMap(); String l = Long.toString(Long.MAX_VALUE); meta.put(Long.MAX_VALUE, "hello world"); + //9223372036854775807 //meta.put(l, "0123456789012345678901234567890123456789012345678901234567890123"); - List pays = List.of(new CardanoApiCodec.Payment(unusedAddr.id(), new CardanoApiCodec.QuantityUnit(1000000, lovelace))); + List pays = + Arrays.asList( + new CardanoApiCodec.Payment(unusedAddr.id(), + new CardanoApiCodec.QuantityUnit(1000000, lovelace) + ) + ); CardanoApiCodec.CreateTransactionResponse resp = api.createTransaction( wallet.id(), passphrase, pays, - meta, + MetadataBuilder.withMap(meta), "self").toCompletableFuture().get(); System.out.println(resp.status().toString()); System.out.println(resp.id()); diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index d753065..7911a68 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -2,9 +2,7 @@ import iog.psg.cardano.CardanoApiCodec; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -33,7 +31,7 @@ static String get(CardanoApiCodec.NetworkInfo info) { } public void createBadWallet() throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { - List mnem = List.of("", "sdfa", "dfd"); + List mnem = Arrays.asList("", "sdfa", "dfd"); jpi.createRestore("some name", "password99", mnem, 4).toCompletableFuture().get(timeout, timeoutUnit); } @@ -64,7 +62,7 @@ public CardanoApiCodec.FundPaymentsResponse fundPayments(String walletId, long a String unusedAddrId = unused.get(0).id(); CardanoApiCodec.QuantityUnit amount = new CardanoApiCodec.QuantityUnit(amountToTransfer, CardanoApiCodec.Units$.MODULE$.lovelace()); CardanoApiCodec.Payment p = new CardanoApiCodec.Payment(unusedAddrId, amount); - CardanoApiCodec.FundPaymentsResponse response = jpi.fundPayments(walletId, List.of(p)).toCompletableFuture().get(timeout, timeoutUnit); + CardanoApiCodec.FundPaymentsResponse response = jpi.fundPayments(walletId, Collections.singletonList(p)).toCompletableFuture().get(timeout, timeoutUnit); return response; } @@ -80,13 +78,14 @@ public CardanoApiCodec.CreateTransactionResponse paymentToSelf(String wallet1Id, metadataLongKey.put(Long.parseLong(k), v); }); + CardanoApiCodec.TxMetadataMapIn in = MetadataBuilder.withMap(metadataLongKey); List unused = jpi.listAddresses(wallet1Id, AddressFilter.UNUSED).toCompletableFuture().get(timeout, timeoutUnit); String unusedAddrIdWallet1 = unused.get(0).id(); CardanoApiCodec.QuantityUnit amount = new CardanoApiCodec.QuantityUnit(amountToTransfer, CardanoApiCodec.Units$.MODULE$.lovelace()); - List payments = List.of(new CardanoApiCodec.Payment(unusedAddrIdWallet1, amount)); + List payments = Collections.singletonList(new CardanoApiCodec.Payment(unusedAddrIdWallet1, amount)); CardanoApiCodec.EstimateFeeResponse response = jpi.estimateFee(wallet1Id, payments).toCompletableFuture().get(timeout, timeoutUnit); long max = response.estimatedMax().quantity(); - return jpi.createTransaction(wallet1Id, passphrase, payments, metadataLongKey).toCompletableFuture().get(timeout, timeoutUnit); + return jpi.createTransaction(wallet1Id, passphrase, payments, in, null).toCompletableFuture().get(timeout, timeoutUnit); } diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala index d10d0a8..b97f097 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -1,13 +1,18 @@ package iog.psg.cardano +import java.util + import akka.actor.ActorSystem +import akka.util.ByteString import iog.psg.cardano.CardanoApi.CardanoApiOps._ import iog.psg.cardano.CardanoApi._ import iog.psg.cardano.CardanoApiCodec.TxState.TxState -import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, SyncState, TxState, Units} +import iog.psg.cardano.CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueByteArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} +import org.apache.commons.codec.binary.Hex +import scala.annotation.tailrec import scala.reflect.ClassTag -import scala.util.{Failure, Success, Try} +import scala.util.{Failure, Random, Success, Try} /** * This script ran successfully in Aug 2020 @@ -19,6 +24,7 @@ object CardanoApiTestScript { private implicit val system = ActorSystem("SingleRequest") + def main(args: Array[String]): Unit = { Try { @@ -44,14 +50,16 @@ object CardanoApiTestScript { val api = new CardanoApi(baseUri) - def waitForTx(txState: TxState, walletId: String, txId: String): Unit = { - if (txState == TxState.pending) { - println(s"$txState") + @tailrec + def waitForTx(txCreateResponse: CreateTransactionResponse, walletId: String, txId: String): CreateTransactionResponse = { + if (txCreateResponse.status == TxState.pending) { + println(s"Wait for ${TxState.inLedger} ${txCreateResponse.status}") Thread.sleep(5000) val txUpdate = unwrap(api.getTransaction(walletId, txId).toFuture.executeBlocking) - waitForTx(txUpdate.status, walletId, txId) + waitForTx(txUpdate, walletId, txId) + } else { + txCreateResponse } - println(s"$txState !!") } @@ -62,10 +70,6 @@ object CardanoApiTestScript { if (netInfo.syncProgress.status == SyncState.ready) { val walletAddresses = unwrap(api.listWallets.toFuture.executeBlocking) - walletAddresses.foreach(addr => { - println(s"Name: ${addr.name} balance: ${addr.balance}") - println(s"Id: ${addr.id} pool gap: ${addr.addressPoolGap}") - }) val walletsOfInterest = walletAddresses.filter(w => walletsNamesOfInterest.contains(w.name)) val fromWallet = walletsOfInterest.find(_.name == walletNameFrom).getOrElse { @@ -73,46 +77,12 @@ object CardanoApiTestScript { unwrap(api.createRestoreWallet(walletNameFrom, walletFromPassphrase, walletFromMnem).executeBlocking) } - val unitResult = unwrap( - api. - updatePassphrase( - fromWallet.id, - walletFromPassphrase, - newWalletPassword) - .executeBlocking - ) - - /*val fromWalletFromNewPassword = unwrap( - api. - createRestoreWallet( - walletNameFrom, - newWalletPassword, - walletFromMnem) - .executeBlocking - )*/ - - val unitResultPutItBack = unwrap( - api. - updatePassphrase( - fromWallet.id, - newWalletPassword, - walletFromPassphrase) - .executeBlocking - ) - println(s"From wallet name, id, balance ${fromWallet.name}, ${fromWallet.id}, ${fromWallet.balance}") - val toWallet = walletsOfInterest.find(_.name == walletNameTo).getOrElse { - println("Generating 'to' wallet...") - unwrap(api.createRestoreWallet(walletNameTo, walletToPassphrase, walletToMnem).executeBlocking) - } - - println(s"To wallet name, id, balance ${toWallet.name}, ${toWallet.id}, ${toWallet.balance}") if (fromWallet.balance.available.quantity > 2) { val toWalletAddresses = - unwrap(api.listAddresses(toWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking) - + unwrap(api.listAddresses(fromWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking) val paymentTo = toWalletAddresses.headOption.getOrElse(fail("No unused addresses in the To wallet?")) @@ -122,54 +92,41 @@ object CardanoApiTestScript { ) ) + val hexAry = Hex.encodeHex(("" + ("1" * 12)).getBytes()) + val h = new String(hexAry) + val inAry = Array.fill(1024 * 5)(Random.nextBytes(1).head) + val ha = MetadataValueArray(inAry.toSeq.map(MetadataValueLong(_))) + MetadataValueStr(h) + val meta = TxMetadataMapIn( + Map(6L -> + ha + ) + ) val tx = unwrap(api.createTransaction( fromWallet.id, walletFromPassphrase, payments, - None, //TODO add metadata + Some(meta), None, ).executeBlocking) - waitForTx(tx.status, fromWallet.id, tx.id) - - - val fromWalletAddresses = - unwrap(api.listAddresses(fromWallet.id, Some(AddressFilter.unUsed)).toFuture.executeBlocking) - - val paymentBack = fromWalletAddresses.headOption.getOrElse(fail("No unused addresses in the From wallet?")) - //transfer back - val returnPayments = Payments( - Seq( - Payment(paymentBack.id, QuantityUnit(lovelaceToTransfer, Units.lovelace)) - ) - ) - - val estimate = unwrap(api.estimateFee(toWallet.id, returnPayments).executeBlocking) - val returnPayments2 = Payments( - Seq( - Payment(paymentBack.id, QuantityUnit(lovelaceToTransfer - estimate.estimatedMax.quantity, Units.lovelace)) - ) - ) - - val returnTx = unwrap(api.createTransaction( - toWallet.id, - walletToPassphrase, - returnPayments2, - Some(Map(1L -> "")), //todo ADD metadata - None - ).executeBlocking) + val finalTx = waitForTx(tx, fromWallet.id, tx.id) + println(s"Last Tx in stream ${finalTx.metadata}") + val back = finalTx.metadata.get.json.as[Map[Long, Array[Byte]]] + back match { + case Left(err) => + case Right(s) => + val good = util.Arrays.equals(s(6),inAry) + println(good) + } - waitForTx(returnTx.status, toWallet.id, returnTx.id) + println(s"Successfully transferred value from wallet to wallet") - println(s"Successfully transferred value between 2 wallets") - val refreshedToWallet = unwrap(api.getWallet(toWallet.id).toFuture.executeBlocking) val refreshedFromWallet = unwrap(api.getWallet(fromWallet.id).toFuture.executeBlocking) val fromDiffBalance = refreshedFromWallet.balance.available.quantity - fromWallet.balance.available.quantity - val toDiffBalance = refreshedToWallet.balance.available.quantity - toWallet.balance.available.quantity println(s"Balance of 'from' wallet is now ${refreshedFromWallet.balance} diff: $fromDiffBalance") - println(s"Balance of 'to' wallet is now ${refreshedToWallet.balance} diff $toDiffBalance") } else { println(s"From wallet ${fromWallet.name} balance ${fromWallet.balance} is too low, cannot continue") diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 1f2c861..845d637 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -24,7 +24,7 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { private val testAmountToTransfer = config.getString("cardano.wallet.amount") private val timeoutValue: Long = 10 private val timeoutUnits = TimeUnit.SECONDS - private lazy val sut = new JpiResponseCheck(new CardanoApiFixture(baseUrl).getJpi,timeoutValue, timeoutUnits) + private lazy val sut = new JpiResponseCheck(new CardanoApiFixture(baseUrl).getJpi, timeoutValue, timeoutUnits) "NetworkInfo status" should "be 'ready'" in { val info = sut.jpi.networkInfo().toCompletableFuture.get(timeoutValue, timeoutUnits) @@ -33,10 +33,17 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { } "Bad wallet creation" should "be prevented" in { - an [IllegalArgumentException] shouldBe thrownBy (sut.createBadWallet()) + an[IllegalArgumentException] shouldBe thrownBy(sut.createBadWallet()) } "Test wallet" should "exist or be created" in { + + println(s"WALLET $baseUrl") + val aryLen = testWalletMnemonic.split(" ").length + val aryLen2 = testWallet2Mnemonic.split(" ").length + println(s"WALLET 1 words ${aryLen} <-") + println(s"WALLET 2 words ${aryLen2} <-") + val mnem = GenericMnemonicSentence(testWalletMnemonic) sut .findOrCreateTestWallet( @@ -63,7 +70,7 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { it should "allow password change in wallet 2" in { sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase) //now this is the wrong password - an [Exception] shouldBe thrownBy(sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase)) + an[Exception] shouldBe thrownBy(sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase)) sut.passwordChange(testWallet2Id, testWalletPassphrase, testWallet2Passphrase) } @@ -87,15 +94,15 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { createTxResponse.id shouldBe getTxResponse.id createTxResponse.amount shouldBe getTxResponse.amount - createTxResponse.metadata.get.size shouldBe 2 - createTxResponse.metadata.get.apply(Long.MaxValue) shouldBe "0" * 64 - createTxResponse.metadata.get.apply(Long.MaxValue - 1) shouldBe "1" * 64 + val Right(mapOut) = createTxResponse.metadata.get.json.as[Map[Long, String]] + mapOut(Long.MaxValue) shouldBe "0" * 64 + mapOut(Long.MaxValue - 1) shouldBe "1" * 64 } it should "delete wallet 2" in { sut.deleteWallet(testWallet2Id) - an [Exception] shouldBe thrownBy (sut.getWallet(testWallet2Id), "Wallet should not be retrieved") + an[Exception] shouldBe thrownBy(sut.getWallet(testWallet2Id), "Wallet should not be retrieved") } } diff --git a/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala b/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala index b2bf034..f82d175 100644 --- a/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala +++ b/src/test/scala/iog/psg/cardano/util/StringToMapParserSpec.scala @@ -1,5 +1,6 @@ package iog.psg.cardano.util +import iog.psg.cardano.CardanoApiCodec.{MetadataValueStr, TxMetadataMapIn} import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -9,7 +10,11 @@ import org.scalatest.matchers.should.Matchers class StringToMapParserSpec extends AnyFlatSpec with Matchers { "MetaMapParser" should "parse simple map" in { - toMetaMap(Some("1:a:2:b")) shouldBe Some(Map((1 -> "a"), 2 -> "b")) + toMetaMap(Some("1:a:2:b")) shouldBe Some( + TxMetadataMapIn[Long]( + Map(1L -> MetadataValueStr("a"), 2L -> MetadataValueStr("b")) + ) + ) } it should "correctly parse an empty string" in { From 9ab706e9c449c2e110b605c67770eb0b0847baf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Mon, 28 Sep 2020 15:31:15 +0100 Subject: [PATCH 08/39] Remove secret data #PSGS-49 (#5) * Remove secret data #PSGS-49 * Pass secrets to CI * CI package only after merge * Moved envs to test phase --- .github/workflows/ci.yml | 10 +++++----- src/test/resources/application.conf | 16 +++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77ce6f7..c33a229 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,11 +24,10 @@ jobs: java-version: '11.0.8' - run: sbt coverage test coverageReport env: - BASE_URL: http://cardano-wallet-testnet.iog.solutions:8090/v2/ - WALLET_1_MNEM: ${{ secrets.WALLET_1_MNEM }} - WALLET_2_MNEM: ${{ secrets.WALLET_2_MNEM }} - WALLET_1_PASS: ${{ secrets.WALLET_1_PASS }} - WALLET_2_PASS: ${{ secrets.WALLET_2_PASS }} + BASE_URL: ${{ secrets.BASE_URL }} + CARDANO_API_WALLET_1_PASSPHRASE: ${{ secrets.CARDANO_API_WALLET_1_PASSPHRASE }} + CARDANO_API_WALLET_1_MNEMONIC: ${{ secrets.CARDANO_API_WALLET_1_MNEMONIC }} + CARDANO_API_WALLET_2_MNEMONIC: ${{ secrets.CARDANO_API_WALLET_2_MNEMONIC }} - name: Archive code coverage results uses: actions/upload-artifact@v2 @@ -36,6 +35,7 @@ jobs: name: code-coverage-report path: target/scala-2.13/scoverage-report package: + if: contains(github.ref, 'release') runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index e98fa8b..8584057 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -1,14 +1,12 @@ -cardano.wallet.baseUrl="http://localhost:8090/v2/" -cardano.wallet.baseUrl=${?BASE_URL} -cardano.wallet.passphrase="password10" -cardano.wallet.passphrase=${?PASSPHRASE} -cardano.wallet.name="cardanoapimainspec" +cardano.wallet.baseUrl=${BASE_URL} +cardano.wallet.passphrase=${CARDANO_API_WALLET_1_PASSPHRASE} +cardano.wallet.name="cardano_api_wallet_1" cardano.wallet.amount=2000000 -cardano.wallet.mnemonic="receive post siren monkey mistake morning teach section mention rural idea say offer number ribbon toward rigid pluck begin ticket auto" -cardano.wallet.id="b63eacb4c89bd942cacfe0d3ed47459bbf0ce5c9" +cardano.wallet.mnemonic=${CARDANO_API_WALLET_1_MNEMONIC} +cardano.wallet.id="6cd6d11a489b7ea82a4624d18b93bdf9b77f0620" cardano.wallet.metadata="0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" -cardano.wallet2.mnemonic="later image spider wrestle tunnel bomb ahead glance broken merry still nominee property clever wedding reduce tribe buzz voyage clay sheriff" -cardano.wallet2.id="527eac22af137dcd159fade57ab0686931feed7c" +cardano.wallet2.mnemonic=${CARDANO_API_WALLET_2_MNEMONIC} +cardano.wallet2.id="bfa9530c4ecfee6e5561e950bd7a7a332e4e7497" cardano.wallet2.name="somethrowawayname" cardano.wallet2.passphrase="somethrowawayname" \ No newline at end of file From 24580e17e2ec31524a5828ea0fa428b10c7a1880 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Mon, 28 Sep 2020 15:34:58 +0100 Subject: [PATCH 09/39] Prepare README (#4) * First draft. --- README.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8d6cf6d..0015b55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,124 @@ -# psg-cardano-wallet-api -Scala client to the Cardano wallet REST API +# PSG Cardano Wallet API -Note - not every field in the wallet API is represented, some of those used in delagation are ignored. \ No newline at end of file +_For consultancy services email [enterprise.solutions@iohk.io](mailto:enterprise.solutions@iohk.io)_ +### Scala and Java client for the Cardano Wallet API + +The Cardano node exposes a [REST like API](https://github.com/input-output-hk/cardano-wallet) +allowing clients to perform a variety of tasks including + - creating or restoring a wallet + - submitting a transaction with or without [metadata](https://github.com/input-output-hk/cardano-wallet/wiki/TxMetadata) + - checking on the status of the node + - listing transactions + - listing wallets + +The full list of capabilities can be found [here](https://input-output-hk.github.io/cardano-wallet/api/edge/). + +This artefact wraps calls to that API to make them easily accessible to Java or Scala developers. + +It also provides an executable jar to provide rudimentary command line access. + + +- [Building](#building) +- [Usage](#usage) + - [scala](#usagescala) + - [java](#usagejava) +- [Command line executable jar](#cmdline) +- [Examples](#examples) + + +### Building + +This is an `sbt` project, so the usual `sbt` commands apply. + +Clone the [repository](https://github.com/input-output-hk/psg-cardano-wallet-api) + +To build and publish the project to your local repository use + +`sbt publish` + +To build the command line executable jar use + +`sbt assembly` + +To build the command line executable jar skipping tests, use + +`sbt 'set test in assembly := {}' assembly` + +This will create a jar in the `target/scala-2.13` folder. + +#### Implementation + +The jar is part of an Akka streaming ecosystem and unsurprisingly uses [Akka Http](https://doc.akka.io/docs/akka-http/current/introduction.html) to make the http requests, +it also uses [circe](https://circe.github.io/circe/) to marshal and unmarshal the json. + +### Usage + +The jar is published in Maven Central, the command line executable jar can be downloaded from the releases section +of the [github repository](https://github.com/input-output-hk/psg-cardano-wallet-api) + +#### Scala + +Add the library to your dependencies + +`libraryDependencies += "iog.psg" %% "psg-cardano-wallet-api" % "0.4.1"` + +The api calls return a HttpRequest set up to the correct url and a mapper to take the entity result and +map it from Json to the corresponding case classes. Using `networkInfo` as an example... + +``` +import iog.psg.cardano.CardanoApi.CardanoApiOps._ +import iog.psg.cardano.CardanoApi._ + +implicit val as = ActorSystem("MyActorSystem") +val baseUri = "http://localhost:8090/v2/" +import as.dispatcher + +val api = new CardanoApi(baseUri) + +val networkInfoF: Future[CardanoApiResponse[NetworkInfo]] = + api.networkInfo.toFuture.execute + +val networkInfo: CardanoApiResponse[NetworkInfo] = + api.networkInfo.toFuture.executeBlocking + +networkInfo match { + case Left(ErrorMessage(message, code)) => //do something + case Right(netInfo: NetworkInfo) => // good! +} +``` + +#### Java + +First, add the library to your dependencies, then using `getWallet` as an example... + +``` +import iog.psg.cardano.jpi.*; + +ActorSystem as = ActorSystem.create(); +ExecutorService es = Executors.newFixedThreadPool(10); +CardanoApiBuilder builder = + CardanoApiBuilder.create("http://localhost:8090/v2/") + .withActorSystem(as) + .withExecutorService(es); + +CardanoApi api = builder.build(); + +String walletId = ""; +CardanoApiCodec.Wallet wallet = + api.getWallet(walletId).toCompletableFuture().get(); + +``` + +#### Command Line + +To see the usage instructions, use + +`java -jar psg-cardano-wallet-api-assembly-x.x.x-SNAPSHOT.jar` + +For example, to see the [network information](https://input-output-hk.github.io/cardano-wallet/api/edge/#tag/Network) use + +`java -jar psg-cardano-wallet-api-assembly-x.x.x-SNAPSHOT.jar -baseUrl http://localhost:8090/v2/ -netInfo` + +#### Examples + +The best place to find working examples is in the [test](https://github.com/input-output-hk/psg-cardano-wallet-api/tree/develop/src/test) folder From fa2dc7f96b822f3c6da902b913700b3dcf6494e6 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Tue, 29 Sep 2020 11:01:53 +0100 Subject: [PATCH 10/39] -help #PSGS-45 --- .../iog/psg/cardano/CardanoApiMain.scala | 158 +++++++++++++++++- .../iog/psg/cardano/CardanoApiMainSpec.scala | 135 ++++++++++++++- 2 files changed, 288 insertions(+), 5 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index d9990c9..05adc1a 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -48,7 +48,6 @@ object CardanoApiMain { val txId = "-txId" val amount = "-amount" val address = "-address" - } val defaultBaseUrl = "http://127.0.0.1:8090/v2/" @@ -230,10 +229,161 @@ object CardanoApiMain { ZonedDateTime.parse(dtStr) } - private def showHelp(): Unit = { - println("Enter commands, and so on...") - } + private def showHelp()(implicit trace: Trace): Unit = { + val exampleWalletId = "1234567890123456789012345678901234567890" + val exampleTxd = "ABCDEF1234567890" + val exampleAddress = "addr12345678901234567890123456789012345678901234567890123456789012345678901234567890" + val exampleMetadata = "0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" + val exampleMnemonic = "ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad" + + trace("This super simple tool allows developers to access a cardano wallet backend from the command line\n") + trace("Usage:") + trace("export CMDLINE='java -jar psg-cardano-wallet-api-assembly-.jar'") + trace("$CMDLINE [command] [arguments]\n") + + def beautifyTrace(commandRunDesc: String, description: String, examples: List[String], apiDocOperation: String = ""): Unit = { + val docsUrl = if (apiDocOperation.nonEmpty) s" [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/$apiDocOperation ]\n" else "" + val examplesStr = s" Examples:\n ${examples.map("$CMDLINE "+_).mkString("\n ")}" + trace(s"$commandRunDesc\n $description\n$docsUrl\n$examplesStr\n") + } + trace("Optional commands:") + beautifyTrace( + commandRunDesc = s"${CmdLine.traceToFile} [filename] [command]", + description = s"write logs into a defined file ( default file name: ${CardanoApiMain.defaultTraceFile} )", + examples = List( + s"${CmdLine.traceToFile} wallets.log ${CmdLine.listWallets}" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.baseUrl} [url] [command]", + description = s"define different api url ( default : ${CardanoApiMain.defaultBaseUrl} )", + examples = List( + s"${CmdLine.baseUrl} http://cardano-wallet-testnet.mydomain:8090/v2/ ${CmdLine.listWallets}" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.noConsole} [command]", + description = "run a command without any logging", + examples = List( + s"${CmdLine.noConsole} ${CmdLine.deleteWallet} ${CmdLine.walletId} $exampleWalletId" + ) + ) + + trace("Commands:") + beautifyTrace( + commandRunDesc = s"${CmdLine.netInfo}", + description = "Show network information", + apiDocOperation = "getNetworkInformation", + examples = List( + s"${CmdLine.netInfo}" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.listWallets}", + description = "Return a list of known wallets, ordered from oldest to newest", + apiDocOperation = "listWallets", + examples = List( + s"${CmdLine.listWallets}" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.estimateFee} ${CmdLine.walletId} [walletId] ${CmdLine.amount} [amount] ${CmdLine.address} [address]", + description = "Estimate fee for the transaction", + apiDocOperation = "postTransactionFee", + examples = List( + s"${CmdLine.estimateFee} ${CmdLine.walletId} $exampleWalletId ${CmdLine.amount} 20000 ${CmdLine.address} $exampleAddress" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.getWallet} ${CmdLine.walletId} [walletId]", + description = "Get wallet by id", + apiDocOperation = "getWallet", + examples = List( + s"${CmdLine.getWallet} ${CmdLine.walletId} $exampleWalletId" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.updatePassphrase} ${CmdLine.walletId} [walletId] ${CmdLine.oldPassphrase} [oldPassphrase] ${CmdLine.passphrase} [newPassphrase]", + description = "Update passphrase", + apiDocOperation = "putWalletPassphrase", + examples = List( + s"${CmdLine.updatePassphrase} ${CmdLine.walletId} $exampleWalletId ${CmdLine.oldPassphrase} OldPassword12345! ${CmdLine.passphrase} NewPassword12345!]" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.deleteWallet} ${CmdLine.walletId} [walletId]", + description = "Delete wallet by id", + apiDocOperation = "deleteWallet", + examples = List( + s"${CmdLine.deleteWallet} ${CmdLine.walletId} $exampleWalletId" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.listWalletAddresses} ${CmdLine.walletId} [walletId] ${CmdLine.state} [state]", + description = "Return a list of known addresses, ordered from newest to oldest, state: used, unused", + apiDocOperation = "listAddresses", + examples = List( + s"${CmdLine.listWalletAddresses} ${CmdLine.walletId} $exampleWalletId ${CmdLine.state} ${AddressFilter.used}", + s"${CmdLine.listWalletAddresses} ${CmdLine.walletId} $exampleWalletId ${CmdLine.state} ${AddressFilter.unUsed}" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.getTx} ${CmdLine.walletId} [walletId] ${CmdLine.txId} [txId]", + description = "Get transaction by id", + apiDocOperation = "getTransaction", + examples = List( + s"${CmdLine.getTx} ${CmdLine.walletId} $exampleWalletId ${CmdLine.txId} $exampleTxd" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.createTx} ${CmdLine.walletId} [walletId] ${CmdLine.amount} [amount] ${CmdLine.address} [address] ${CmdLine.passphrase} [passphrase] ${CmdLine.metadata} [metadata](optional)", + description = "Create and send transaction from the wallet", + apiDocOperation = "postTransaction", + examples = List( + s"${CmdLine.createTx} ${CmdLine.walletId} $exampleWalletId ${CmdLine.amount} 20000 ${CmdLine.address} $exampleAddress ${CmdLine.passphrase} Password12345!", + s"${CmdLine.createTx} ${CmdLine.walletId} $exampleWalletId ${CmdLine.amount} 20000 ${CmdLine.address} $exampleAddress ${CmdLine.passphrase} Password12345! ${CmdLine.metadata} $exampleMetadata", + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.fundTx} ${CmdLine.walletId} [walletId] ${CmdLine.amount} [amount] ${CmdLine.address} [address]", + description = "Select coins to cover the given set of payments", + apiDocOperation = "selectCoins", + examples = List( + s"${CmdLine.fundTx} ${CmdLine.walletId} $exampleWalletId ${CmdLine.amount} 20000 ${CmdLine.address} $exampleAddress" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} [walletId] ${CmdLine.start} [start_date](optional) ${CmdLine.end} [end_date](optional) ${CmdLine.order} [order](optional) ${CmdLine.minWithdrawal} [minWithdrawal](optional)", + description = "Lists all incoming and outgoing wallet's transactions, dates in ISO_ZONED_DATE_TIME format, order: ascending, descending ( default )", + apiDocOperation = "listTransactions", + examples = List( + s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} $exampleWalletId", + s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} $exampleWalletId ${CmdLine.start} 2020-01-02T10:15:30+01:00", + s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} $exampleWalletId ${CmdLine.start} 2020-01-02T10:15:30+01:00 ${CmdLine.end} 2020-09-30T12:00:00+01:00", + s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} $exampleWalletId ${CmdLine.order} ${Order.ascendingOrder}", + s"${CmdLine.listWalletTransactions} ${CmdLine.walletId} $exampleWalletId ${CmdLine.minWithdrawal} 1" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.createWallet} ${CmdLine.name} [walletName] ${CmdLine.passphrase} [passphrase] ${CmdLine.mnemonic} [mnemonic] ${CmdLine.addressPoolGap} [address_pool_gap](optional)", + description = "Create new wallet", + apiDocOperation = "postWallet", + examples = List( + s"${CmdLine.createWallet} ${CmdLine.name} new_wallet_1 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic'", + s"${CmdLine.createWallet} ${CmdLine.name} new_wallet_2 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic' ${CmdLine.addressPoolGap} 10" + ) + ) + beautifyTrace( + commandRunDesc = s"${CmdLine.restoreWallet} ${CmdLine.name} [walletName] ${CmdLine.passphrase} [passphrase] ${CmdLine.mnemonic} [mnemonic] ${CmdLine.addressPoolGap} [address_pool_gap](optional)", + description = "Restore wallet", + apiDocOperation = "postWallet", + examples = List( + s"${CmdLine.restoreWallet} ${CmdLine.name} new_wallet_1 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic''", + s"${CmdLine.restoreWallet} ${CmdLine.name} new_wallet_2 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic' ${CmdLine.addressPoolGap} 10" + ) + ) + } def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T])(implicit t: Trace): T = unwrapOpt(Try(apiResult)).get diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 6a6e6bd..4735222 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -41,7 +41,9 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S var results: Seq[String] = Seq.empty implicit val memTrace = new Trace { - override def apply(s: Object): Unit = results = s.toString +: results + override def apply(s: Object): Unit = { + results = s.toString +: results + } override def close(): Unit = () } @@ -220,5 +222,136 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S } + "--help" should "show possible commands" in { + val results = runCmdLine(CmdLine.help) + + results.mkString("\n") shouldBe + """This super simple tool allows developers to access a cardano wallet backend from the command line + | + |Usage: + |export CMDLINE='java -jar psg-cardano-wallet-api-assembly-.jar' + |$CMDLINE [command] [arguments] + | + |Optional commands: + |-trace [filename] [command] + | write logs into a defined file ( default file name: cardano-api.log ) + | + | Examples: + | $CMDLINE -trace wallets.log -wallets + | + |-baseUrl [url] [command] + | define different api url ( default : http://127.0.0.1:8090/v2/ ) + | + | Examples: + | $CMDLINE -baseUrl http://cardano-wallet-testnet.mydomain:8090/v2/ -wallets + | + |-noConsole [command] + | run a command without any logging + | + | Examples: + | $CMDLINE -noConsole -deleteWallet -walletId 1234567890123456789012345678901234567890 + | + |Commands: + |-netInfo + | Show network information + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getNetworkInformation ] + | + | Examples: + | $CMDLINE -netInfo + | + |-wallets + | Return a list of known wallets, ordered from oldest to newest + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listWallets ] + | + | Examples: + | $CMDLINE -wallets + | + |-estimateFee -walletId [walletId] -amount [amount] -address [address] + | Estimate fee for the transaction + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postTransactionFee ] + | + | Examples: + | $CMDLINE -estimateFee -walletId 1234567890123456789012345678901234567890 -amount 20000 -address addr12345678901234567890123456789012345678901234567890123456789012345678901234567890 + | + |-wallet -walletId [walletId] + | Get wallet by id + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getWallet ] + | + | Examples: + | $CMDLINE -wallet -walletId 1234567890123456789012345678901234567890 + | + |-updatePassphrase -walletId [walletId] -oldPassphrase [oldPassphrase] -passphrase [newPassphrase] + | Update passphrase + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/putWalletPassphrase ] + | + | Examples: + | $CMDLINE -updatePassphrase -walletId 1234567890123456789012345678901234567890 -oldPassphrase OldPassword12345! -passphrase NewPassword12345!] + | + |-deleteWallet -walletId [walletId] + | Delete wallet by id + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/deleteWallet ] + | + | Examples: + | $CMDLINE -deleteWallet -walletId 1234567890123456789012345678901234567890 + | + |-listAddresses -walletId [walletId] -state [state] + | Return a list of known addresses, ordered from newest to oldest, state: used, unused + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listAddresses ] + | + | Examples: + | $CMDLINE -listAddresses -walletId 1234567890123456789012345678901234567890 -state used + | $CMDLINE -listAddresses -walletId 1234567890123456789012345678901234567890 -state unused + | + |-getTx -walletId [walletId] -txId [txId] + | Get transaction by id + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getTransaction ] + | + | Examples: + | $CMDLINE -getTx -walletId 1234567890123456789012345678901234567890 -txId ABCDEF1234567890 + | + |-createTx -walletId [walletId] -amount [amount] -address [address] -passphrase [passphrase] -metadata [metadata](optional) + | Create and send transaction from the wallet + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postTransaction ] + | + | Examples: + | $CMDLINE -createTx -walletId 1234567890123456789012345678901234567890 -amount 20000 -address addr12345678901234567890123456789012345678901234567890123456789012345678901234567890 -passphrase Password12345! + | $CMDLINE -createTx -walletId 1234567890123456789012345678901234567890 -amount 20000 -address addr12345678901234567890123456789012345678901234567890123456789012345678901234567890 -passphrase Password12345! -metadata 0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI + | + |-fundTx -walletId [walletId] -amount [amount] -address [address] + | Select coins to cover the given set of payments + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/selectCoins ] + | + | Examples: + | $CMDLINE -fundTx -walletId 1234567890123456789012345678901234567890 -amount 20000 -address addr12345678901234567890123456789012345678901234567890123456789012345678901234567890 + | + |-listTxs -walletId [walletId] -start [start_date](optional) -end [end_date](optional) -order [order](optional) -minWithdrawal [minWithdrawal](optional) + | Lists all incoming and outgoing wallet's transactions, dates in ISO_ZONED_DATE_TIME format, order: ascending, descending ( default ) + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listTransactions ] + | + | Examples: + | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 + | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 -start 2020-01-02T10:15:30+01:00 + | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 -start 2020-01-02T10:15:30+01:00 -end 2020-09-30T12:00:00+01:00 + | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 -order ascending + | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 -minWithdrawal 1 + | + |-createWallet -name [walletName] -passphrase [passphrase] -mnemonic [mnemonic] -addressPoolGap [address_pool_gap](optional) + | Create new wallet + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet ] + | + | Examples: + | $CMDLINE -createWallet -name new_wallet_1 -passphrase Password12345! -mnemonic 'ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad' + | $CMDLINE -createWallet -name new_wallet_2 -passphrase Password12345! -mnemonic 'ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad' -addressPoolGap 10 + | + |-restoreWallet -name [walletName] -passphrase [passphrase] -mnemonic [mnemonic] -addressPoolGap [address_pool_gap](optional) + | Restore wallet + | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet ] + | + | Examples: + | $CMDLINE -restoreWallet -name new_wallet_1 -passphrase Password12345! -mnemonic 'ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad'' + | $CMDLINE -restoreWallet -name new_wallet_2 -passphrase Password12345! -mnemonic 'ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad' -addressPoolGap 10 + |""".stripMargin + } + } From 9e9149675b0952042ac5dd4c7e37e3425ef2d862 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Tue, 29 Sep 2020 11:25:02 +0100 Subject: [PATCH 11/39] rollback those brackets --- src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 4735222..353d625 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -41,9 +41,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S var results: Seq[String] = Seq.empty implicit val memTrace = new Trace { - override def apply(s: Object): Unit = { - results = s.toString +: results - } + override def apply(s: Object): Unit = results = s.toString +: results override def close(): Unit = () } From 29fd2b579908dd08e024b54d9e6be216abd36b06 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Tue, 29 Sep 2020 14:17:50 +0100 Subject: [PATCH 12/39] Added info where to generate mnemonic --- src/main/scala/iog/psg/cardano/CardanoApiMain.scala | 4 ++-- src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 05adc1a..2d1e292 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -367,7 +367,7 @@ object CardanoApiMain { ) beautifyTrace( commandRunDesc = s"${CmdLine.createWallet} ${CmdLine.name} [walletName] ${CmdLine.passphrase} [passphrase] ${CmdLine.mnemonic} [mnemonic] ${CmdLine.addressPoolGap} [address_pool_gap](optional)", - description = "Create new wallet", + description = "Create new wallet ( mnemonic can be generated on: https://iancoleman.io/bip39/ )", apiDocOperation = "postWallet", examples = List( s"${CmdLine.createWallet} ${CmdLine.name} new_wallet_1 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic'", @@ -376,7 +376,7 @@ object CardanoApiMain { ) beautifyTrace( commandRunDesc = s"${CmdLine.restoreWallet} ${CmdLine.name} [walletName] ${CmdLine.passphrase} [passphrase] ${CmdLine.mnemonic} [mnemonic] ${CmdLine.addressPoolGap} [address_pool_gap](optional)", - description = "Restore wallet", + description = "Restore wallet ( mnemonic can be generated on: https://iancoleman.io/bip39/ )", apiDocOperation = "postWallet", examples = List( s"${CmdLine.restoreWallet} ${CmdLine.name} new_wallet_1 ${CmdLine.passphrase} Password12345! ${CmdLine.mnemonic} '$exampleMnemonic''", diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 353d625..a701b34 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -334,7 +334,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S | $CMDLINE -listTxs -walletId 1234567890123456789012345678901234567890 -minWithdrawal 1 | |-createWallet -name [walletName] -passphrase [passphrase] -mnemonic [mnemonic] -addressPoolGap [address_pool_gap](optional) - | Create new wallet + | Create new wallet ( mnemonic can be generated on: https://iancoleman.io/bip39/ ) | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet ] | | Examples: @@ -342,7 +342,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S | $CMDLINE -createWallet -name new_wallet_2 -passphrase Password12345! -mnemonic 'ability make always any pulse swallow marriage media dismiss degree edit spawn distance state dad' -addressPoolGap 10 | |-restoreWallet -name [walletName] -passphrase [passphrase] -mnemonic [mnemonic] -addressPoolGap [address_pool_gap](optional) - | Restore wallet + | Restore wallet ( mnemonic can be generated on: https://iancoleman.io/bip39/ ) | [ https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet ] | | Examples: From 98cc5d49f011c9dcdb24217d691a36930bcfd591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Wed, 30 Sep 2020 12:52:30 +0100 Subject: [PATCH 13/39] ApiRequestExecutor as a trait, for simpler overriding in clients #PSGS-52 (#8) * ApiRequestExecutor as a trait, for simpler overriding in clients #PSGS-52 * Make the execution of a request something that can be overriden by clients. Co-authored-by: mcsherrylabs --- .../psg/cardano/jpi/ApiRequestExecutor.java | 8 ++++ .../psg/cardano/jpi/CardanoApiBuilder.java | 27 +++++++++++- .../iog/psg/cardano/ApiRequestExecutor.scala | 18 ++++++++ .../scala/iog/psg/cardano/CardanoApi.scala | 11 ++--- .../iog/psg/cardano/CardanoApiMain.scala | 1 + .../iog/psg/cardano/jpi/HelpExecute.scala | 10 +++-- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 43 +++++++++++++++++-- .../iog/psg/cardano/CardanoApiMainSpec.scala | 4 -- .../psg/cardano/CardanoApiTestScript.scala | 4 +- .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 15 ++++++- .../util/ApiExecutorOverrideSpec.scala | 37 ++++++++++++++++ 11 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 src/main/java/iog/psg/cardano/jpi/ApiRequestExecutor.java create mode 100644 src/main/scala/iog/psg/cardano/ApiRequestExecutor.scala create mode 100644 src/test/scala/iog/psg/cardano/util/ApiExecutorOverrideSpec.scala diff --git a/src/main/java/iog/psg/cardano/jpi/ApiRequestExecutor.java b/src/main/java/iog/psg/cardano/jpi/ApiRequestExecutor.java new file mode 100644 index 0000000..dae5d40 --- /dev/null +++ b/src/main/java/iog/psg/cardano/jpi/ApiRequestExecutor.java @@ -0,0 +1,8 @@ +package iog.psg.cardano.jpi; + +import java.util.concurrent.CompletionStage; + +public interface ApiRequestExecutor { + CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException; + +} diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java index 8a13a23..be6faa5 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java @@ -1,9 +1,11 @@ package iog.psg.cardano.jpi; import akka.actor.ActorSystem; +import iog.psg.cardano.ApiRequestExecutor$; import scala.concurrent.ExecutionContext; import java.util.Objects; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -12,6 +14,7 @@ public class CardanoApiBuilder { final private String url; private ExecutorService executorService; private ActorSystem actorSystem; + private ApiRequestExecutor apiRequestExecutor; private CardanoApiBuilder() { url = null; @@ -40,10 +43,16 @@ public CardanoApiBuilder withActorSystem(ActorSystem actorSystem) { return this; } + public CardanoApiBuilder withApiExecutor(ApiRequestExecutor apiExecutor) { + this.apiRequestExecutor = apiExecutor; + Objects.requireNonNull(apiExecutor, "apiExecutor is 'null'"); + return this; + } + public CardanoApi build() { if (actorSystem == null) { - actorSystem = ActorSystem.create("Cardano JPI ActorSystem"); + actorSystem = ActorSystem.create("CardanoJPIActorSystem"); } if (executorService == null) { @@ -51,8 +60,22 @@ public CardanoApi build() { } ExecutionContext ec = ExecutionContext.fromExecutorService(executorService); + + HelpExecute helpExecute; + + if(apiRequestExecutor == null) { + helpExecute = new HelpExecute(ApiRequestExecutor$.MODULE$, ec, actorSystem); + } else { + helpExecute = new HelpExecute(ApiRequestExecutor$.MODULE$, ec, actorSystem) { + @Override + public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { + return apiRequestExecutor.execute(request); + } + }; + } + iog.psg.cardano.CardanoApi api = new iog.psg.cardano.CardanoApi(url, ec, actorSystem); - HelpExecute helpExecute = new HelpExecute(ec, actorSystem); + return new CardanoApi(api, helpExecute); } diff --git a/src/main/scala/iog/psg/cardano/ApiRequestExecutor.scala b/src/main/scala/iog/psg/cardano/ApiRequestExecutor.scala new file mode 100644 index 0000000..77c6f3a --- /dev/null +++ b/src/main/scala/iog/psg/cardano/ApiRequestExecutor.scala @@ -0,0 +1,18 @@ +package iog.psg.cardano + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import iog.psg.cardano.CardanoApi.{CardanoApiRequest, CardanoApiResponse} + +import scala.concurrent.{ExecutionContext, Future} + +object ApiRequestExecutor extends ApiRequestExecutor + +trait ApiRequestExecutor { + + def execute[T](request: CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem): Future[CardanoApiResponse[T]] = + Http() + .singleRequest(request.request) + .flatMap(request.mapper) + +} diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 64fb0b8..edfc6e2 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -51,7 +51,7 @@ object CardanoApi { } //tie execute to ioEc - implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]])(implicit ec: ExecutionContext, as: ActorSystem) { + implicit class CardanoApiRequestFOps[T](requestF: Future[CardanoApiRequest[T]])(implicit executor: ApiRequestExecutor, ec: ExecutionContext, as: ActorSystem) { def execute: Future[CardanoApiResponse[T]] = { requestF.flatMap(_.execute) } @@ -61,14 +61,9 @@ object CardanoApi { } - implicit class CardanoApiRequestOps[T](request: CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem) { + implicit class CardanoApiRequestOps[T](request: CardanoApiRequest[T])(implicit executor: ApiRequestExecutor, ec: ExecutionContext, as: ActorSystem) { - def execute: Future[CardanoApiResponse[T]] = { - - Http() - .singleRequest(request.request) - .flatMap(request.mapper) - } + def execute: Future[CardanoApiResponse[T]] = executor.execute(request) def executeBlocking(implicit maxWaitTime: Duration): CardanoApiResponse[T] = Await.result(execute, maxWaitTime) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 2d1e292..2aef7a1 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -85,6 +85,7 @@ object CardanoApiMain { implicit val system: ActorSystem = ActorSystem("SingleRequest") import system.dispatcher //the + implicit val apiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor Try { diff --git a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala index 2f01fae..7e1d78a 100644 --- a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala +++ b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala @@ -2,10 +2,12 @@ package iog.psg.cardano.jpi import java.util.concurrent.CompletionStage +import iog.psg.cardano.ApiRequestExecutor +import iog.psg.cardano.jpi.{ApiRequestExecutor => JApiRequestExecutor} import akka.actor.ActorSystem -import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} +import iog.psg.cardano.CardanoApi.CardanoApiOps.CardanoApiRequestOps import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} -import iog.psg.cardano.CardanoApiCodec.{MetadataValue, MetadataValueStr, TxMetadataMapIn} +import iog.psg.cardano.CardanoApiCodec.{MetadataValue, MetadataValueStr} import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.MapHasAsScala @@ -25,7 +27,7 @@ object HelpExecute { }.toMap } -class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) { +class HelpExecute(implicit executor: ApiRequestExecutor, ec: ExecutionContext, as: ActorSystem) extends JApiRequestExecutor { @throws(classOf[CardanoApiException]) private def unwrapResponse[T](resp: CardanoApiResponse[T]): T = resp match { @@ -41,7 +43,7 @@ class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) { @throws(classOf[CardanoApiException]) def execute[T](request: Future[iog.psg.cardano.CardanoApi.CardanoApiRequest[T]]): CompletionStage[T] = { - FutureConverters.asJava(request.execute.map(unwrapResponse)) + FutureConverters.asJava(request).thenCompose(request => this.execute(request)) } def toScalaImmutable[B](in: java.util.Map[java.lang.Long, String]): Map[java.lang.Long, String] = diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 7911a68..8b18b3e 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -1,11 +1,11 @@ package iog.psg.cardano.jpi; import iog.psg.cardano.CardanoApiCodec; +import scala.Enumeration; +import scala.Option; import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; public class JpiResponseCheck { @@ -92,4 +92,41 @@ public CardanoApiCodec.CreateTransactionResponse paymentToSelf(String wallet1Id, public CardanoApiCodec.CreateTransactionResponse getTx(String walletId, String txId) throws Exception { return jpi.getTransaction(walletId, txId).toCompletableFuture().get(timeout, timeoutUnit); } + + public static CardanoApi buildWithDummyApiExecutor() { + CardanoApiBuilder builder = CardanoApiBuilder.create("http://fake/").withApiExecutor(new ApiRequestExecutor() { + @Override + public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { + CompletableFuture result = new CompletableFuture<>(); + + System.out.println(request.request().uri().path()); + System.out.println(request.request().uri().fragment()); + System.out.println(request.request().uri()); + + if(request.request().uri().path().endsWith("wallets", true)) { + Enumeration.Value lovelace = CardanoApiCodec.Units$.MODULE$.Value(CardanoApiCodec.Units$.MODULE$.lovelace().toString()); + Enumeration.Value sync = CardanoApiCodec.SyncState$.MODULE$.Value(CardanoApiCodec.SyncState$.MODULE$.ready().toString()); + CardanoApiCodec.QuantityUnit dummy = new CardanoApiCodec.QuantityUnit(1, lovelace); + CardanoApiCodec.SyncStatus state = new CardanoApiCodec.SyncStatus( + sync, + Option.apply(null) + ); + CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null)); + result.complete((T) new CardanoApiCodec.Wallet( + "id", + 10, + new CardanoApiCodec.Balance(dummy, dummy, dummy), + "name", + state, + tip)); + return result.toCompletableFuture(); + } else { + throw new CardanoApiException("Unexpected", "request"); + } + } + + }); + + return builder.build(); + } } diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index a701b34..5856982 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -3,16 +3,12 @@ package iog.psg.cardano import java.time.ZonedDateTime import akka.actor.ActorSystem -import iog.psg.cardano.CardanoApi._ import iog.psg.cardano.CardanoApiMain.CmdLine import iog.psg.cardano.util.{ArgumentParser, Configure, Trace} -import org.scalatest.Ignore import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import scala.concurrent.{Future, blocking} - class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures { diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala index b97f097..698c4e0 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -3,10 +3,8 @@ package iog.psg.cardano import java.util import akka.actor.ActorSystem -import akka.util.ByteString import iog.psg.cardano.CardanoApi.CardanoApiOps._ import iog.psg.cardano.CardanoApi._ -import iog.psg.cardano.CardanoApiCodec.TxState.TxState import iog.psg.cardano.CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueByteArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} import org.apache.commons.codec.binary.Hex @@ -48,7 +46,7 @@ object CardanoApiTestScript { import system.dispatcher val api = new CardanoApi(baseUri) - + implicit val apiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor @tailrec def waitForTx(txCreateResponse: CreateTransactionResponse, walletId: String, txId: String): CreateTransactionResponse = { diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 845d637..218e185 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -2,7 +2,7 @@ package iog.psg.cardano.jpi import java.util.concurrent.TimeUnit -import iog.psg.cardano.CardanoApiCodec.GenericMnemonicSentence +import iog.psg.cardano.CardanoApiCodec.{GenericMnemonicSentence, Wallet} import iog.psg.cardano.util.Configure import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -32,6 +32,19 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { networkState shouldBe "ready" } + "Jpi CardanoAPI" should "allow override of execute" in { + val api = JpiResponseCheck.buildWithDummyApiExecutor() + val mnem = GenericMnemonicSentence(testWalletMnemonic) + val wallet = api + .createRestore( + testWalletName, + testWalletPassphrase, + mnem.mnemonicSentence.asJava, + 10 + ).toCompletableFuture.get(); + wallet.id shouldBe "id" + } + "Bad wallet creation" should "be prevented" in { an[IllegalArgumentException] shouldBe thrownBy(sut.createBadWallet()) } diff --git a/src/test/scala/iog/psg/cardano/util/ApiExecutorOverrideSpec.scala b/src/test/scala/iog/psg/cardano/util/ApiExecutorOverrideSpec.scala new file mode 100644 index 0000000..dd46e8e --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/ApiExecutorOverrideSpec.scala @@ -0,0 +1,37 @@ +package iog.psg.cardano.util + +import akka.actor.ActorSystem +import akka.http.scaladsl.model.HttpRequest +import iog.psg.cardano.CardanoApi.{CardanoApiRequest, CardanoApiResponse, ErrorMessage} +import iog.psg.cardano.{ApiRequestExecutor, CardanoApi} +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.{ExecutionContext, Future} + +class ApiExecutorOverrideSpec extends AnyFlatSpec with Matchers with ScalaFutures { + + private implicit val system = ActorSystem("SingleRequest") + import system.dispatcher + + "A client in different package" should "be able to override the APIExecutor" in { + //This will not compile if the trait is sealed. + val testUri = "http://localhost:9999/" + val response = Left(ErrorMessage("TESTAPIOVERRIDE", "TESTAPIOVERRIDE")) + val sut = new ApiRequestExecutor { + override def execute[T](request: CardanoApi.CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem): Future[CardanoApiResponse[T]] = { + request.request.uri.toString() shouldBe testUri + Future.successful(response) + } + } + + + val result = sut.execute(CardanoApiRequest( + HttpRequest(uri = testUri), + _ => Future.successful(response) + )).futureValue + + result shouldBe response + } +} From a9ad6aa76006a6bd77c7145d473121d2dd0c6648 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Wed, 30 Sep 2020 16:35:26 +0100 Subject: [PATCH 14/39] We don't need to expose the scala request executor to the java API, (#10) the executor functionality is overridable via the java withApiExecutor method. --- src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java | 5 ++--- src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala | 4 +++- src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java index be6faa5..d2d5652 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApiBuilder.java @@ -1,7 +1,6 @@ package iog.psg.cardano.jpi; import akka.actor.ActorSystem; -import iog.psg.cardano.ApiRequestExecutor$; import scala.concurrent.ExecutionContext; import java.util.Objects; @@ -64,9 +63,9 @@ public CardanoApi build() { HelpExecute helpExecute; if(apiRequestExecutor == null) { - helpExecute = new HelpExecute(ApiRequestExecutor$.MODULE$, ec, actorSystem); + helpExecute = new HelpExecute(ec, actorSystem); } else { - helpExecute = new HelpExecute(ApiRequestExecutor$.MODULE$, ec, actorSystem) { + helpExecute = new HelpExecute(ec, actorSystem) { @Override public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { return apiRequestExecutor.execute(request); diff --git a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala index 7e1d78a..ec94d30 100644 --- a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala +++ b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala @@ -27,7 +27,9 @@ object HelpExecute { }.toMap } -class HelpExecute(implicit executor: ApiRequestExecutor, ec: ExecutionContext, as: ActorSystem) extends JApiRequestExecutor { +class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) extends JApiRequestExecutor { + + implicit val executor: ApiRequestExecutor = ApiRequestExecutor @throws(classOf[CardanoApiException]) private def unwrapResponse[T](resp: CardanoApiResponse[T]): T = resp match { diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 218e185..0c566b1 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -2,7 +2,7 @@ package iog.psg.cardano.jpi import java.util.concurrent.TimeUnit -import iog.psg.cardano.CardanoApiCodec.{GenericMnemonicSentence, Wallet} +import iog.psg.cardano.CardanoApiCodec.GenericMnemonicSentence import iog.psg.cardano.util.Configure import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers From aaa6290881e0e7b7989f0971b668ddc788ae7d6b Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Wed, 30 Sep 2020 09:43:10 +0100 Subject: [PATCH 15/39] Filled up missing fields from API responses #PSGS-54 --- .../scala/iog/psg/cardano/CardanoApi.scala | 1 - .../iog/psg/cardano/CardanoApiCodec.scala | 31 +- src/test/resources/jsons/addresses.json | 6 + .../jsons/coin_selections_random.json | 22 ++ src/test/resources/jsons/estimate_fees.json | 10 + src/test/resources/jsons/netinfo.json | 23 ++ src/test/resources/jsons/transaction.json | 110 +++++++ src/test/resources/jsons/transactions.json | 112 +++++++ src/test/resources/jsons/wallet.json | 49 +++ src/test/resources/jsons/wallets.json | 51 ++++ .../iog/psg/cardano/CardanoApiCodecSpec.scala | 281 ++++++++++++++++++ 11 files changed, 688 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/jsons/addresses.json create mode 100644 src/test/resources/jsons/coin_selections_random.json create mode 100644 src/test/resources/jsons/estimate_fees.json create mode 100644 src/test/resources/jsons/netinfo.json create mode 100644 src/test/resources/jsons/transaction.json create mode 100644 src/test/resources/jsons/transactions.json create mode 100644 src/test/resources/jsons/wallet.json create mode 100644 src/test/resources/jsons/wallets.json create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index edfc6e2..14dd417 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -4,7 +4,6 @@ package iog.psg.cardano import java.time.ZonedDateTime import akka.actor.ActorSystem -import akka.http.scaladsl.Http import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.Uri.Query diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 2c78160..2c856f0 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -1,6 +1,5 @@ package iog.psg.cardano -import java.nio.charset.StandardCharsets import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -11,13 +10,14 @@ import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer import akka.util.ByteString import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ +import io.circe._ import io.circe.generic.auto._ import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder import io.circe.syntax.EncoderOps -import io.circe._ import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter +import iog.psg.cardano.CardanoApiCodec.DelegationStatus.DelegationStatus import iog.psg.cardano.CardanoApiCodec.SyncState.SyncState import iog.psg.cardano.CardanoApiCodec.TxDirection.TxDirection import iog.psg.cardano.CardanoApiCodec.TxState.TxState @@ -36,7 +36,6 @@ object CardanoApiCodec { encoder.mapJson(_.dropNullValues) private[cardano] implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) - private[cardano] implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) private[cardano] implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => stringToZonedDate(s) match { @@ -60,6 +59,8 @@ object CardanoApiCodec { private[cardano] implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) private[cardano] implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) + private[cardano] implicit val decodeDelegationStatus: Decoder[DelegationStatus] = Decoder.decodeString.map(DelegationStatus.withName) + private[cardano] implicit val encodeDelegationStatus: Encoder[DelegationStatus] = (a: DelegationStatus) => Json.fromString(a.toString) final case class TxMetadataOut(json: Json) { def toMapMetadataStr: Decoder.Result[Map[Long, String]] = json.as[Map[Long, String]] @@ -135,15 +136,24 @@ object CardanoApiCodec { } + object DelegationStatus extends Enumeration { + type DelegationStatus = Value + val delegating: DelegationStatus = Value("delegating") + val notDelegating: DelegationStatus = Value("not_delegating") + } + final case class DelegationActive(status: DelegationStatus, target: Option[String]) + @ConfiguredJsonCodec final case class DelegationNext(status: DelegationStatus, changesAt: Option[NextEpoch]) + final case class Delegation(active: DelegationActive, next: List[DelegationNext]) @ConfiguredJsonCodec case class NetworkTip( epochNumber: Long, slotNumber: Long, - height: Option[QuantityUnit]) + height: Option[QuantityUnit], + absoluteSlotNumber: Option[Long]) - case class NodeTip(height: QuantityUnit) + @ConfiguredJsonCodec case class NodeTip(height: QuantityUnit, slotNumber: Long, epochNumber: Long, absoluteSlotNumber: Option[Long]) - case class WalletAddressId(id: String, state: Option[AddressFilter]) + @ConfiguredJsonCodec case class WalletAddressId(id: String, state: Option[AddressFilter]) private[cardano] case class CreateTransaction( passphrase: String, @@ -247,6 +257,7 @@ object CardanoApiCodec { amount: QuantityUnit ) + @ConfiguredJsonCodec case class FundPaymentsResponse( inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress] @@ -256,7 +267,8 @@ object CardanoApiCodec { case class Block( slotNumber: Int, epochNumber: Int, - height: QuantityUnit + height: QuantityUnit, + absoluteSlotNumber: Option[Long] ) @ConfiguredJsonCodec @@ -287,12 +299,17 @@ object CardanoApiCodec { metadata: Option[TxMetadataOut] ) + @ConfiguredJsonCodec + case class Passphrase(lastUpdatedAt: ZonedDateTime) + @ConfiguredJsonCodec case class Wallet( id: String, addressPoolGap: Int, balance: Balance, + delegation: Option[Delegation], name: String, + passphrase: Passphrase, state: SyncStatus, tip: NetworkTip ) diff --git a/src/test/resources/jsons/addresses.json b/src/test/resources/jsons/addresses.json new file mode 100644 index 0000000..ef15cbc --- /dev/null +++ b/src/test/resources/jsons/addresses.json @@ -0,0 +1,6 @@ +[ + { + "id": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "used" + } +] \ No newline at end of file diff --git a/src/test/resources/jsons/coin_selections_random.json b/src/test/resources/jsons/coin_selections_random.json new file mode 100644 index 0000000..8c95baa --- /dev/null +++ b/src/test/resources/jsons/coin_selections_random.json @@ -0,0 +1,22 @@ +{ + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/jsons/estimate_fees.json b/src/test/resources/jsons/estimate_fees.json new file mode 100644 index 0000000..ed2c553 --- /dev/null +++ b/src/test/resources/jsons/estimate_fees.json @@ -0,0 +1,10 @@ +{ + "estimated_min": { + "quantity": 42000000, + "unit": "lovelace" + }, + "estimated_max": { + "quantity": 42000000, + "unit": "lovelace" + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/netinfo.json b/src/test/resources/jsons/netinfo.json new file mode 100644 index 0000000..4bd32d2 --- /dev/null +++ b/src/test/resources/jsons/netinfo.json @@ -0,0 +1,23 @@ +{ + "sync_progress": { + "status": "ready" + }, + "node_tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + }, + "network_tip": { + "slot_number": 1337, + "epoch_number": 14, + "absolute_slot_number": 8086 + }, + "next_epoch": { + "epoch_number": 14, + "epoch_start_time": "2019-02-27T14:46:45.000Z" + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/transaction.json b/src/test/resources/jsons/transaction.json new file mode 100644 index 0000000..aa1692a --- /dev/null +++ b/src/test/resources/jsons/transaction.json @@ -0,0 +1,110 @@ +{ + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "inserted_at": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "pending_since": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "depth": { + "quantity": 1337, + "unit": "block" + }, + "direction": "outgoing", + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "withdrawals": [ + { + "stake_address": "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "status": "pending", + "metadata": { + "0": { + "string": "cardano" + }, + "1": { + "int": 14 + }, + "2": { + "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + }, + "3": { + "list": [ + { + "int": 14 + }, + { + "int": 42 + }, + { + "string": "1337" + } + ] + }, + "4": { + "map": [ + { + "k": { + "string": "key" + }, + "v": { + "string": "value" + } + }, + { + "k": { + "int": 14 + }, + "v": { + "int": 42 + } + } + ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/transactions.json b/src/test/resources/jsons/transactions.json new file mode 100644 index 0000000..3d83f59 --- /dev/null +++ b/src/test/resources/jsons/transactions.json @@ -0,0 +1,112 @@ +[ + { + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "inserted_at": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "pending_since": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "depth": { + "quantity": 1337, + "unit": "block" + }, + "direction": "outgoing", + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "withdrawals": [ + { + "stake_address": "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "status": "pending", + "metadata": { + "0": { + "string": "cardano" + }, + "1": { + "int": 14 + }, + "2": { + "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + }, + "3": { + "list": [ + { + "int": 14 + }, + { + "int": 42 + }, + { + "string": "1337" + } + ] + }, + "4": { + "map": [ + { + "k": { + "string": "key" + }, + "v": { + "string": "value" + } + }, + { + "k": { + "int": 14 + }, + "v": { + "int": 42 + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/src/test/resources/jsons/wallet.json b/src/test/resources/jsons/wallet.json new file mode 100644 index 0000000..1a8b23a --- /dev/null +++ b/src/test/resources/jsons/wallet.json @@ -0,0 +1,49 @@ +{ + "id": "2512a00e9653fe49a44a5886202e24d77eeb998f", + "address_pool_gap": 20, + "balance": { + "available": { + "quantity": 42000000, + "unit": "lovelace" + }, + "reward": { + "quantity": 42000000, + "unit": "lovelace" + }, + "total": { + "quantity": 42000000, + "unit": "lovelace" + } + }, + "delegation": { + "active": { + "status": "delegating", + "target": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1" + }, + "next": [ + { + "status": "not_delegating", + "changes_at": { + "epoch_number": 14, + "epoch_start_time": "2020-01-22T10:06:39.037Z" + } + } + ] + }, + "name": "Alan's Wallet", + "passphrase": { + "last_updated_at": "2019-02-27T14:46:45.000Z" + }, + "state": { + "status": "ready" + }, + "tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/wallets.json b/src/test/resources/jsons/wallets.json new file mode 100644 index 0000000..5a1f37a --- /dev/null +++ b/src/test/resources/jsons/wallets.json @@ -0,0 +1,51 @@ +[ + { + "id": "2512a00e9653fe49a44a5886202e24d77eeb998f", + "address_pool_gap": 20, + "balance": { + "available": { + "quantity": 42000000, + "unit": "lovelace" + }, + "reward": { + "quantity": 42000000, + "unit": "lovelace" + }, + "total": { + "quantity": 42000000, + "unit": "lovelace" + } + }, + "delegation": { + "active": { + "status": "delegating", + "target": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1" + }, + "next": [ + { + "status": "not_delegating", + "changes_at": { + "epoch_number": 14, + "epoch_start_time": "2020-01-22T10:06:39.037Z" + } + } + ] + }, + "name": "Alan's Wallet", + "passphrase": { + "last_updated_at": "2019-02-27T14:46:45.000Z" + }, + "state": { + "status": "ready" + }, + "tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + } +] \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala new file mode 100644 index 0000000..b06d4d2 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -0,0 +1,281 @@ +package iog.psg.cardano + +import java.time.ZonedDateTime + +import io.circe.Decoder +import io.circe.parser._ +import io.circe.syntax._ +import iog.psg.cardano.CardanoApiCodec._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.io.Source + +class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { + + "Wallet" should "be decoded properly" in { + val decoded = decodeJsonFile[Wallet]("wallet.json") + + compareWallets(decoded, wallet) + } + + it should "decode wallet's list" in { + val decodedWallets = decodeJsonFile[Seq[Wallet]]("wallets.json") + + decodedWallets.size shouldBe 1 + compareWallets(decodedWallets.head, wallet) + } + + "network information" should "be decoded properly" in { + val decoded = decodeJsonFile[NetworkInfo]("netinfo.json") + + compareNetworkInformation( + decoded, + NetworkInfo( + syncProgress = SyncStatus(SyncState.ready, None), + networkTip = networkTip.copy(height = None), + nodeTip = nodeTip, + nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) + ) + ) + + } + + "list addresses" should "be decoded properly" in { + val decoded = decodeJsonFile[Seq[WalletAddressId]]("addresses.json") + decoded.size shouldBe 1 + + compareAddress(decoded.head, WalletAddressId(id = addressIdStr, Some(AddressFilter.used))) + } + + "list transactions" should "be decoded properly" in { + val decoded = decodeJsonFile[Seq[CreateTransactionResponse]]("transactions.json") + decoded.size shouldBe 1 + + compareTransaction(decoded.head, createdTransactionResponse) + } + + it should "decode one transaction" in { + val decoded = decodeJsonFile[CreateTransactionResponse]("transaction.json") + + compareTransaction(decoded, createdTransactionResponse) + } + + "estimate fees" should "be decoded properly" in { + val decoded = decodeJsonFile[EstimateFeeResponse]("estimate_fees.json") + + compareEstimateFeeResponse(decoded, estimateFeeResponse) + } + + "fund payments" should "be decoded properly" in { + val decoded = decodeJsonFile[FundPaymentsResponse]("coin_selections_random.json") + + compareFundPaymentsResponse(decoded, fundPaymentsResponse) + } + + private def getJsonFromFile(file: String): String = { + val source = Source.fromURL(getClass.getResource(s"/jsons/$file")) + val jsonStr = source.mkString + source.close() + jsonStr + } + + + private def decodeJsonFile[T](file: String)(implicit dec: Decoder[T]) = { + val jsonStr = getJsonFromFile(file) + decode[T](jsonStr).getOrElse(fail("Could not decode wallet")) + } + + private def compareFundPaymentsResponse(decoded: FundPaymentsResponse, proper: FundPaymentsResponse) = { + decoded.inputs shouldBe proper.inputs + decoded.outputs shouldBe proper.outputs + } + + private def compareEstimateFeeResponse(decoded: EstimateFeeResponse, proper: EstimateFeeResponse) = { + decoded.estimatedMax shouldBe proper.estimatedMax + decoded.estimatedMin shouldBe proper.estimatedMin + } + + private def compareTransaction(decoded: CreateTransactionResponse, proper: CreateTransactionResponse) = { + decoded.id shouldBe proper.id + decoded.amount shouldBe proper.amount + decoded.insertedAt shouldBe proper.insertedAt + decoded.pendingSince shouldBe proper.pendingSince + decoded.depth shouldBe proper.depth + decoded.direction shouldBe proper.direction + decoded.inputs shouldBe proper.inputs + decoded.outputs shouldBe proper.outputs + decoded.withdrawals shouldBe proper.withdrawals + decoded.status shouldBe proper.status + decoded.metadata shouldBe proper.metadata + } + + private def compareAddress(decoded: WalletAddressId, proper: WalletAddressId) = { + decoded.id shouldBe proper.id + decoded.state shouldBe proper.state + } + + private def compareNetworkInformation(decoded: NetworkInfo, proper: NetworkInfo) = { + decoded.nextEpoch shouldBe proper.nextEpoch + decoded.nodeTip shouldBe proper.nodeTip + decoded.networkTip shouldBe proper.networkTip + decoded.syncProgress shouldBe proper.syncProgress + } + + private def compareWallets(decoded: Wallet, proper: Wallet) = { + decoded.id shouldBe proper.id + decoded.addressPoolGap shouldBe proper.addressPoolGap + decoded.balance shouldBe proper.balance + decoded.delegation shouldBe proper.delegation + decoded.name shouldBe proper.name + decoded.passphrase shouldBe proper.passphrase + decoded.state shouldBe proper.state + decoded.tip shouldBe proper.tip + } + + private final lazy val wallet = Wallet( + id = "2512a00e9653fe49a44a5886202e24d77eeb998f", + addressPoolGap = 20, + balance = Balance( + available = QuantityUnit(42000000, Units.lovelace), + reward = QuantityUnit(42000000, Units.lovelace), + total = QuantityUnit(42000000, Units.lovelace) + ), + delegation = Some( + Delegation( + active = DelegationActive( + status = DelegationStatus.delegating, + target = Some("1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1") + ), + next = List( + DelegationNext( + status = DelegationStatus.notDelegating, + changesAt = + Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14)) + ) + ) + ) + ), + name = "Alan's Wallet", + passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2019-02-27T14:46:45.000Z")), + state = SyncStatus(SyncState.ready, None), + tip = networkTip + ) + + private final lazy val nextEpoch = + NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14) + private final lazy val networkTip = NetworkTip( + epochNumber = 14, + slotNumber = 1337, + height = Some(QuantityUnit(1337, Units.block)), + absoluteSlotNumber = Some(8086) + ) + + private final lazy val nodeTip = NodeTip( + epochNumber = 14, + slotNumber = 1337, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + + private final lazy val timedBlock = TimedBlock( + time = ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), + block = Block( + slotNumber = 1337, + epochNumber = 14, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + ) + + private final lazy val createdTransactionResponse = { + val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + + CreateTransactionResponse( + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + amount = commonAmount, + insertedAt = Some(timedBlock), + pendingSince = Some(timedBlock), + depth = Some(QuantityUnit(quantity = 1337, unit = Units.block)), + direction = TxDirection.outgoing, + inputs = Seq(inAddress), + outputs = Seq(outAddress), + withdrawals = Seq( + StakeAddress( + stakeAddress = "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + amount = commonAmount + ) + ), + status = TxState.pending, + metadata = Some(TxMetadataOut(json = parse(""" + |{ + | "0": { + | "string": "cardano" + | }, + | "1": { + | "int": 14 + | }, + | "2": { + | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + | }, + | "3": { + | "list": [ + | { + | "int": 14 + | }, + | { + | "int": 42 + | }, + | { + | "string": "1337" + | } + | ] + | }, + | "4": { + | "map": [ + | { + | "k": { + | "string": "key" + | }, + | "v": { + | "string": "value" + | } + | }, + | { + | "k": { + | "int": 14 + | }, + | "v": { + | "int": 42 + | } + | } + | ] + | } + | } + |""".stripMargin).getOrElse(fail("Invalid metadata json")))) + ) + } + + private final lazy val estimateFeeResponse = { + val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + + EstimateFeeResponse(estimatedMin = commonAmount, estimatedMax = commonAmount) + } + + private final lazy val fundPaymentsResponse = + FundPaymentsResponse(inputs = IndexedSeq(inAddress), outputs = Seq(outAddress)) + + private final lazy val inAddress = InAddress( + address = Some(addressIdStr), + amount = Some(QuantityUnit(quantity = 42000000, unit = Units.lovelace)), + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + index = 0 + ) + + private final lazy val outAddress = + OutAddress(address = addressIdStr, amount = QuantityUnit(quantity = 42000000, unit = Units.lovelace)) + + private final lazy val addressIdStr = + "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" + +} From 579b303233380e28a16f07457a9fcc51771e9431 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Wed, 30 Sep 2020 14:58:04 +0100 Subject: [PATCH 16/39] wip --- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 5 ++++- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 8b18b3e..6fba659 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -4,6 +4,7 @@ import scala.Enumeration; import scala.Option; +import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -111,12 +112,14 @@ public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiReque sync, Option.apply(null) ); - CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null)); + CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null), Option.apply(10)); result.complete((T) new CardanoApiCodec.Wallet( "id", 10, new CardanoApiCodec.Balance(dummy, dummy, dummy), + Option.apply(null), "name", + new CardanoApiCodec.Passphrase(ZonedDateTime.now()), state, tip)); return result.toCompletableFuture(); diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index b06d4d2..2d2494d 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -122,14 +122,30 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { decoded.syncProgress shouldBe proper.syncProgress } + private def compareBalance(decoded: Balance, proper: Balance) = { + decoded.available.quantity shouldBe proper.available.quantity + decoded.available.unit.toString shouldBe proper.available.unit.toString + + decoded.reward.quantity shouldBe proper.reward.quantity + decoded.reward.unit.toString shouldBe proper.reward.unit.toString + + decoded.total.quantity shouldBe proper.total.quantity + decoded.total.unit.toString shouldBe proper.total.unit.toString + } + + private def compareState(decoded: SyncStatus, proper: SyncStatus) = { + decoded.status.toString shouldBe proper.status.toString + decoded.progress shouldBe proper.progress + } + private def compareWallets(decoded: Wallet, proper: Wallet) = { decoded.id shouldBe proper.id decoded.addressPoolGap shouldBe proper.addressPoolGap - decoded.balance shouldBe proper.balance + compareBalance(decoded.balance, proper.balance) decoded.delegation shouldBe proper.delegation decoded.name shouldBe proper.name decoded.passphrase shouldBe proper.passphrase - decoded.state shouldBe proper.state + compareState(decoded.state, proper.state) decoded.tip shouldBe proper.tip } From 6e082a455c7cabd47c7ee850379e4cbecd7db0b5 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Wed, 30 Sep 2020 15:37:41 +0100 Subject: [PATCH 17/39] Finished units for codec decoders --- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 2d2494d..d958eb8 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -86,26 +86,59 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { decode[T](jsonStr).getOrElse(fail("Could not decode wallet")) } + private def compareInAddress(decoded: InAddress, proper: InAddress) = { + decoded.address shouldBe proper.address + compareQuantityUnitOpts(decoded.amount, proper.amount) + decoded.id shouldBe proper.id + decoded.index shouldBe proper.index + } + + private def compareOutAddress(decoded: OutAddress, proper: OutAddress) = { + decoded.address shouldBe proper.address + compareQuantityUnit(decoded.amount, proper.amount) + } + + private def compareInputs(decoded: Seq[InAddress], proper: Seq[InAddress]) = + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareInAddress(decodedAddress, properAddress) + } + + private def compareOutputs(decoded: Seq[OutAddress], proper: Seq[OutAddress]) = + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareOutAddress(decodedAddress, properAddress) + } + private def compareFundPaymentsResponse(decoded: FundPaymentsResponse, proper: FundPaymentsResponse) = { - decoded.inputs shouldBe proper.inputs - decoded.outputs shouldBe proper.outputs + compareInputs(decoded.inputs, proper.inputs) + compareOutputs(decoded.outputs, proper.outputs) } private def compareEstimateFeeResponse(decoded: EstimateFeeResponse, proper: EstimateFeeResponse) = { - decoded.estimatedMax shouldBe proper.estimatedMax - decoded.estimatedMin shouldBe proper.estimatedMin + compareQuantityUnit(decoded.estimatedMax, proper.estimatedMax) + compareQuantityUnit(decoded.estimatedMin, proper.estimatedMin) + } + + private def compareStakeAddress(decoded: StakeAddress, proper: StakeAddress) = { + compareQuantityUnit(decoded.amount, proper.amount) + decoded.stakeAddress shouldBe proper.stakeAddress + } + + private def compareStakeAddresses(decoded: Seq[StakeAddress], proper: Seq[StakeAddress]) = { + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareStakeAddress(decodedAddress, properAddress) + } } private def compareTransaction(decoded: CreateTransactionResponse, proper: CreateTransactionResponse) = { decoded.id shouldBe proper.id - decoded.amount shouldBe proper.amount + compareQuantityUnit(decoded.amount, proper.amount) decoded.insertedAt shouldBe proper.insertedAt decoded.pendingSince shouldBe proper.pendingSince decoded.depth shouldBe proper.depth decoded.direction shouldBe proper.direction - decoded.inputs shouldBe proper.inputs - decoded.outputs shouldBe proper.outputs - decoded.withdrawals shouldBe proper.withdrawals + compareInputs(decoded.inputs, proper.inputs) + compareOutputs(decoded.outputs, proper.outputs) + compareStakeAddresses(decoded.withdrawals, proper.withdrawals) decoded.status shouldBe proper.status decoded.metadata shouldBe proper.metadata } @@ -119,7 +152,22 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { decoded.nextEpoch shouldBe proper.nextEpoch decoded.nodeTip shouldBe proper.nodeTip decoded.networkTip shouldBe proper.networkTip - decoded.syncProgress shouldBe proper.syncProgress + decoded.syncProgress.status.toString shouldBe proper.syncProgress.status.toString + + compareQuantityUnitOpts(decoded.syncProgress.progress, proper.syncProgress.progress) + } + + private def compareQuantityUnitOpts(decoded: Option[QuantityUnit], proper: Option[QuantityUnit]) = { + if (decoded.isEmpty && proper.isEmpty) assert(true) + else for { + decodedQU <- decoded + properQU <- proper + } yield compareQuantityUnit(decodedQU, properQU) + } + + private def compareQuantityUnit(decoded: QuantityUnit, proper: QuantityUnit) = { + decoded.unit.toString shouldBe proper.unit.toString + decoded.quantity shouldBe proper.quantity } private def compareBalance(decoded: Balance, proper: Balance) = { From 4f3c0ab9a3dd7a4014971b314df581a255b4f2d6 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Wed, 30 Sep 2020 16:06:33 +0100 Subject: [PATCH 18/39] Added more check for new model in jpi checks --- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 16 ++- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 115 +-------------- .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 23 ++- .../iog/psg/cardano/util/ModelCompare.scala | 131 ++++++++++++++++++ 4 files changed, 167 insertions(+), 118 deletions(-) create mode 100644 src/test/scala/iog/psg/cardano/util/ModelCompare.scala diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 6fba659..509cd2b 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -3,7 +3,9 @@ import iog.psg.cardano.CardanoApiCodec; import scala.Enumeration; import scala.Option; +import scala.jdk.CollectionConverters; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -113,13 +115,23 @@ public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiReque Option.apply(null) ); CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null), Option.apply(10)); + + ZonedDateTime dummyDate = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"); + Enumeration.Value delegatingStatus = CardanoApiCodec.DelegationStatus$.MODULE$.Value(CardanoApiCodec.DelegationStatus$.MODULE$.delegating().toString()); + Enumeration.Value notDelegatingStatus = CardanoApiCodec.DelegationStatus$.MODULE$.Value(CardanoApiCodec.DelegationStatus$.MODULE$.notDelegating().toString()); + CardanoApiCodec.DelegationActive delegationActive = new CardanoApiCodec.DelegationActive(delegatingStatus, Option.apply("1234567890")); + CardanoApiCodec.DelegationNext delegationNext = new CardanoApiCodec.DelegationNext(notDelegatingStatus, Option.apply(new CardanoApiCodec.NextEpoch(dummyDate, 10))); + List nexts = Arrays.asList(delegationNext); + scala.collection.immutable.List nextsScalaList = CollectionConverters.ListHasAsScala(nexts).asScala().toList(); + CardanoApiCodec.Delegation delegation = new CardanoApiCodec.Delegation(delegationActive, nextsScalaList); + result.complete((T) new CardanoApiCodec.Wallet( "id", 10, new CardanoApiCodec.Balance(dummy, dummy, dummy), - Option.apply(null), + Option.apply(delegation), "name", - new CardanoApiCodec.Passphrase(ZonedDateTime.now()), + new CardanoApiCodec.Passphrase(dummyDate), state, tip)); return result.toCompletableFuture(); diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index d958eb8..05b422f 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -4,14 +4,14 @@ import java.time.ZonedDateTime import io.circe.Decoder import io.circe.parser._ -import io.circe.syntax._ import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.util.ModelCompare import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.io.Source -class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { +class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { "Wallet" should "be decoded properly" in { val decoded = decodeJsonFile[Wallet]("wallet.json") @@ -86,117 +86,6 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers { decode[T](jsonStr).getOrElse(fail("Could not decode wallet")) } - private def compareInAddress(decoded: InAddress, proper: InAddress) = { - decoded.address shouldBe proper.address - compareQuantityUnitOpts(decoded.amount, proper.amount) - decoded.id shouldBe proper.id - decoded.index shouldBe proper.index - } - - private def compareOutAddress(decoded: OutAddress, proper: OutAddress) = { - decoded.address shouldBe proper.address - compareQuantityUnit(decoded.amount, proper.amount) - } - - private def compareInputs(decoded: Seq[InAddress], proper: Seq[InAddress]) = - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareInAddress(decodedAddress, properAddress) - } - - private def compareOutputs(decoded: Seq[OutAddress], proper: Seq[OutAddress]) = - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareOutAddress(decodedAddress, properAddress) - } - - private def compareFundPaymentsResponse(decoded: FundPaymentsResponse, proper: FundPaymentsResponse) = { - compareInputs(decoded.inputs, proper.inputs) - compareOutputs(decoded.outputs, proper.outputs) - } - - private def compareEstimateFeeResponse(decoded: EstimateFeeResponse, proper: EstimateFeeResponse) = { - compareQuantityUnit(decoded.estimatedMax, proper.estimatedMax) - compareQuantityUnit(decoded.estimatedMin, proper.estimatedMin) - } - - private def compareStakeAddress(decoded: StakeAddress, proper: StakeAddress) = { - compareQuantityUnit(decoded.amount, proper.amount) - decoded.stakeAddress shouldBe proper.stakeAddress - } - - private def compareStakeAddresses(decoded: Seq[StakeAddress], proper: Seq[StakeAddress]) = { - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareStakeAddress(decodedAddress, properAddress) - } - } - - private def compareTransaction(decoded: CreateTransactionResponse, proper: CreateTransactionResponse) = { - decoded.id shouldBe proper.id - compareQuantityUnit(decoded.amount, proper.amount) - decoded.insertedAt shouldBe proper.insertedAt - decoded.pendingSince shouldBe proper.pendingSince - decoded.depth shouldBe proper.depth - decoded.direction shouldBe proper.direction - compareInputs(decoded.inputs, proper.inputs) - compareOutputs(decoded.outputs, proper.outputs) - compareStakeAddresses(decoded.withdrawals, proper.withdrawals) - decoded.status shouldBe proper.status - decoded.metadata shouldBe proper.metadata - } - - private def compareAddress(decoded: WalletAddressId, proper: WalletAddressId) = { - decoded.id shouldBe proper.id - decoded.state shouldBe proper.state - } - - private def compareNetworkInformation(decoded: NetworkInfo, proper: NetworkInfo) = { - decoded.nextEpoch shouldBe proper.nextEpoch - decoded.nodeTip shouldBe proper.nodeTip - decoded.networkTip shouldBe proper.networkTip - decoded.syncProgress.status.toString shouldBe proper.syncProgress.status.toString - - compareQuantityUnitOpts(decoded.syncProgress.progress, proper.syncProgress.progress) - } - - private def compareQuantityUnitOpts(decoded: Option[QuantityUnit], proper: Option[QuantityUnit]) = { - if (decoded.isEmpty && proper.isEmpty) assert(true) - else for { - decodedQU <- decoded - properQU <- proper - } yield compareQuantityUnit(decodedQU, properQU) - } - - private def compareQuantityUnit(decoded: QuantityUnit, proper: QuantityUnit) = { - decoded.unit.toString shouldBe proper.unit.toString - decoded.quantity shouldBe proper.quantity - } - - private def compareBalance(decoded: Balance, proper: Balance) = { - decoded.available.quantity shouldBe proper.available.quantity - decoded.available.unit.toString shouldBe proper.available.unit.toString - - decoded.reward.quantity shouldBe proper.reward.quantity - decoded.reward.unit.toString shouldBe proper.reward.unit.toString - - decoded.total.quantity shouldBe proper.total.quantity - decoded.total.unit.toString shouldBe proper.total.unit.toString - } - - private def compareState(decoded: SyncStatus, proper: SyncStatus) = { - decoded.status.toString shouldBe proper.status.toString - decoded.progress shouldBe proper.progress - } - - private def compareWallets(decoded: Wallet, proper: Wallet) = { - decoded.id shouldBe proper.id - decoded.addressPoolGap shouldBe proper.addressPoolGap - compareBalance(decoded.balance, proper.balance) - decoded.delegation shouldBe proper.delegation - decoded.name shouldBe proper.name - decoded.passphrase shouldBe proper.passphrase - compareState(decoded.state, proper.state) - decoded.tip shouldBe proper.tip - } - private final lazy val wallet = Wallet( id = "2512a00e9653fe49a44a5886202e24d77eeb998f", addressPoolGap = 20, diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 0c566b1..3ddc9d0 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -1,16 +1,17 @@ package iog.psg.cardano.jpi +import java.time.ZonedDateTime import java.util.concurrent.TimeUnit -import iog.psg.cardano.CardanoApiCodec.GenericMnemonicSentence -import iog.psg.cardano.util.Configure +import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.util.{Configure, ModelCompare} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} -class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { +class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare { private val baseUrl = config.getString("cardano.wallet.baseUrl") private val testWalletName = config.getString("cardano.wallet.name") @@ -43,6 +44,22 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { 10 ).toCompletableFuture.get(); wallet.id shouldBe "id" + + val delegation = wallet.delegation.getOrElse(fail("Missing delegation")) + val properDelegation = Delegation( + DelegationActive( + DelegationStatus.delegating, + Some("1234567890") + ), + List(DelegationNext( + DelegationStatus.notDelegating, + Some(NextEpoch( + epochStartTime = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"), epochNumber = 10 + )) + )) + ) + + compareDelegation(delegation, properDelegation) } "Bad wallet creation" should "be prevented" in { diff --git a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala new file mode 100644 index 0000000..bf1056b --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala @@ -0,0 +1,131 @@ +package iog.psg.cardano.util + +import iog.psg.cardano.CardanoApiCodec._ +import org.scalatest.Assertion +import org.scalatest.matchers.should.Matchers + +trait ModelCompare extends Matchers { + + final def compareInAddress(decoded: InAddress, proper: InAddress): Assertion = { + decoded.address shouldBe proper.address + compareQuantityUnitOpts(decoded.amount, proper.amount) + decoded.id shouldBe proper.id + decoded.index shouldBe proper.index + } + + final def compareOutAddress(decoded: OutAddress, proper: OutAddress): Assertion = { + decoded.address shouldBe proper.address + compareQuantityUnit(decoded.amount, proper.amount) + } + + final def compareInputs(decoded: Seq[InAddress], proper: Seq[InAddress]): Seq[Assertion] = + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareInAddress(decodedAddress, properAddress) + } + + final def compareOutputs(decoded: Seq[OutAddress], proper: Seq[OutAddress]): Seq[Assertion] = + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareOutAddress(decodedAddress, properAddress) + } + + final def compareFundPaymentsResponse(decoded: FundPaymentsResponse, proper: FundPaymentsResponse): Seq[Assertion] = { + compareInputs(decoded.inputs, proper.inputs) + compareOutputs(decoded.outputs, proper.outputs) + } + + final def compareEstimateFeeResponse(decoded: EstimateFeeResponse, proper: EstimateFeeResponse): Assertion = { + compareQuantityUnit(decoded.estimatedMax, proper.estimatedMax) + compareQuantityUnit(decoded.estimatedMin, proper.estimatedMin) + } + + final def compareStakeAddress(decoded: StakeAddress, proper: StakeAddress): Assertion = { + compareQuantityUnit(decoded.amount, proper.amount) + decoded.stakeAddress shouldBe proper.stakeAddress + } + + final def compareStakeAddresses(decoded: Seq[StakeAddress], proper: Seq[StakeAddress]): Seq[Assertion] = { + decoded.zip(proper).map { + case (decodedAddress, properAddress) => compareStakeAddress(decodedAddress, properAddress) + } + } + + final def compareTransaction(decoded: CreateTransactionResponse, proper: CreateTransactionResponse): Assertion = { + decoded.id shouldBe proper.id + compareQuantityUnit(decoded.amount, proper.amount) + decoded.insertedAt shouldBe proper.insertedAt + decoded.pendingSince shouldBe proper.pendingSince + decoded.depth shouldBe proper.depth + decoded.direction shouldBe proper.direction + compareInputs(decoded.inputs, proper.inputs) + compareOutputs(decoded.outputs, proper.outputs) + compareStakeAddresses(decoded.withdrawals, proper.withdrawals) + decoded.status shouldBe proper.status + decoded.metadata shouldBe proper.metadata + } + + final def compareAddress(decoded: WalletAddressId, proper: WalletAddressId): Assertion = { + decoded.id shouldBe proper.id + decoded.state shouldBe proper.state + } + + final def compareNetworkInformation(decoded: NetworkInfo, proper: NetworkInfo): Assertion = { + decoded.nextEpoch shouldBe proper.nextEpoch + decoded.nodeTip shouldBe proper.nodeTip + decoded.networkTip shouldBe proper.networkTip + decoded.syncProgress.status.toString shouldBe proper.syncProgress.status.toString + + compareQuantityUnitOpts(decoded.syncProgress.progress, proper.syncProgress.progress) + } + + final def compareQuantityUnitOpts(decoded: Option[QuantityUnit], proper: Option[QuantityUnit]): Assertion = { + if (decoded.isEmpty && proper.isEmpty) assert(true) + else (for { + decodedQU <- decoded + properQU <- proper + } yield compareQuantityUnit(decodedQU, properQU)).getOrElse(assert(false, "one of units is none")) + } + + final def compareQuantityUnit(decoded: QuantityUnit, proper: QuantityUnit): Assertion = { + decoded.unit.toString shouldBe proper.unit.toString + decoded.quantity shouldBe proper.quantity + } + + final def compareBalance(decoded: Balance, proper: Balance): Assertion = { + decoded.available.quantity shouldBe proper.available.quantity + decoded.available.unit.toString shouldBe proper.available.unit.toString + + decoded.reward.quantity shouldBe proper.reward.quantity + decoded.reward.unit.toString shouldBe proper.reward.unit.toString + + decoded.total.quantity shouldBe proper.total.quantity + decoded.total.unit.toString shouldBe proper.total.unit.toString + } + + final def compareState(decoded: SyncStatus, proper: SyncStatus): Assertion = { + decoded.status.toString shouldBe proper.status.toString + decoded.progress shouldBe proper.progress + } + + final def compareDelegation(decoded: Delegation, proper: Delegation): Seq[Assertion] = { + decoded.active.status.toString shouldBe proper.active.status.toString + decoded.active.target shouldBe proper.active.target + + decoded.next.zip(proper.next).map { + case (decodedNext, properNext) => + decodedNext.status.toString shouldBe properNext.status.toString + decodedNext.changesAt shouldBe properNext.changesAt + } + } + + final def compareWallets(decoded: Wallet, proper: Wallet): Assertion = { + decoded.id shouldBe proper.id + decoded.addressPoolGap shouldBe proper.addressPoolGap + compareBalance(decoded.balance, proper.balance) + decoded.delegation shouldBe proper.delegation + decoded.name shouldBe proper.name + decoded.passphrase shouldBe proper.passphrase + compareState(decoded.state, proper.state) + decoded.tip shouldBe proper.tip + } + +} From bc3e9893815c505a32115fe42b82923cdf3bcc02 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Wed, 30 Sep 2020 16:17:28 +0100 Subject: [PATCH 19/39] JPI checks all fields now --- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 2 - .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 40 +++++++++++++++++++ .../iog/psg/cardano/util/ModelCompare.scala | 7 +++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 05b422f..8f40f3b 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -115,8 +115,6 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { tip = networkTip ) - private final lazy val nextEpoch = - NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14) private final lazy val networkTip = NetworkTip( epochNumber = 14, slotNumber = 1337, diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 3ddc9d0..ba90cba 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -43,6 +43,8 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model mnem.mnemonicSentence.asJava, 10 ).toCompletableFuture.get(); + + wallet.id shouldBe "id" val delegation = wallet.delegation.getOrElse(fail("Missing delegation")) @@ -59,6 +61,44 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model )) ) + val networkTip = NetworkTip( + epochNumber = 3, + slotNumber = 4, + height = None, + absoluteSlotNumber = Some(10) + ) + + val properWallet = Wallet( + id = "id", + addressPoolGap = 10, + balance = Balance( + available = QuantityUnit(1, Units.lovelace), + reward = QuantityUnit(1, Units.lovelace), + total = QuantityUnit(1, Units.lovelace) + ), + delegation = Some( + Delegation( + active = DelegationActive( + status = DelegationStatus.delegating, + target = Some("1234567890") + ), + next = List( + DelegationNext( + status = DelegationStatus.notDelegating, + changesAt = + Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"), epochNumber = 10)) + ) + ) + ) + ), + name = "name", + passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2000-01-02T10:01:02+01:00")), + state = SyncStatus(SyncState.ready, None), + tip = networkTip + ) + + compareWallets(wallet, properWallet) + compareDelegation(delegation, properDelegation) } diff --git a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala index bf1056b..2a004df 100644 --- a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala +++ b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala @@ -117,11 +117,16 @@ trait ModelCompare extends Matchers { } } + final def compareDelegationOpts(decoded: Option[Delegation], proper: Option[Delegation]): Seq[Assertion] = { + if (decoded.nonEmpty && proper.nonEmpty) compareDelegation(decoded.get, proper.get) + else Seq(assert(false, "one of delegations is none")) + } + final def compareWallets(decoded: Wallet, proper: Wallet): Assertion = { decoded.id shouldBe proper.id decoded.addressPoolGap shouldBe proper.addressPoolGap compareBalance(decoded.balance, proper.balance) - decoded.delegation shouldBe proper.delegation + compareDelegationOpts(decoded.delegation, proper.delegation) decoded.name shouldBe proper.name decoded.passphrase shouldBe proper.passphrase compareState(decoded.state, proper.state) From 2b6d999c9f60b843865546e865aae0fac361487c Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 08:18:04 +0100 Subject: [PATCH 20/39] Imports and dec change --- src/main/scala/iog/psg/cardano/CardanoApiCodec.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 2c856f0..b1f165f 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -13,7 +13,7 @@ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe._ import io.circe.generic.auto._ import io.circe.generic.extras._ -import io.circe.generic.extras.semiauto.deriveConfiguredEncoder +import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} import io.circe.syntax.EncoderOps import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter @@ -36,6 +36,8 @@ object CardanoApiCodec { encoder.mapJson(_.dropNullValues) private[cardano] implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) + private[cardano] implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) + private[cardano] implicit val createListAddrEntityDecoder: Decoder[WalletAddressId] = deriveConfiguredDecoder private[cardano] implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => stringToZonedDate(s) match { @@ -153,7 +155,7 @@ object CardanoApiCodec { @ConfiguredJsonCodec case class NodeTip(height: QuantityUnit, slotNumber: Long, epochNumber: Long, absoluteSlotNumber: Option[Long]) - @ConfiguredJsonCodec case class WalletAddressId(id: String, state: Option[AddressFilter]) + case class WalletAddressId(id: String, state: Option[AddressFilter]) private[cardano] case class CreateTransaction( passphrase: String, From 19a547c7355f64006295712c50f98c2dbf69bc7e Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 08:22:01 +0100 Subject: [PATCH 21/39] Use codecs only when needed ( snake case ) --- src/main/scala/iog/psg/cardano/CardanoApiCodec.scala | 2 -- src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index b1f165f..c914877 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -37,7 +37,6 @@ object CardanoApiCodec { private[cardano] implicit val createRestoreEntityEncoder: Encoder[CreateRestore] = dropNulls(deriveConfiguredEncoder) private[cardano] implicit val createListAddrEntityEncoder: Encoder[WalletAddressId] = dropNulls(deriveConfiguredEncoder) - private[cardano] implicit val createListAddrEntityDecoder: Decoder[WalletAddressId] = deriveConfiguredDecoder private[cardano] implicit val decodeDateTime: Decoder[ZonedDateTime] = Decoder.decodeString.emap { s => stringToZonedDate(s) match { @@ -259,7 +258,6 @@ object CardanoApiCodec { amount: QuantityUnit ) - @ConfiguredJsonCodec case class FundPaymentsResponse( inputs: IndexedSeq[InAddress], outputs: Seq[OutAddress] diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 8f40f3b..4cf45df 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -4,8 +4,11 @@ import java.time.ZonedDateTime import io.circe.Decoder import io.circe.parser._ +import io.circe.generic.auto._ + import iog.psg.cardano.CardanoApiCodec._ import iog.psg.cardano.util.ModelCompare + import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers From 7fe203e80b93b89a72b29101f178bcfccfdc8daa Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 08:26:10 +0100 Subject: [PATCH 22/39] Imports optimized --- src/main/scala/iog/psg/cardano/CardanoApiCodec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index c914877..d21d173 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -13,7 +13,7 @@ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe._ import io.circe.generic.auto._ import io.circe.generic.extras._ -import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} +import io.circe.generic.extras.semiauto.deriveConfiguredEncoder import io.circe.syntax.EncoderOps import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter From e38efd77b6fecde24dfab2d80fb5429ad1118a93 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 08:28:17 +0100 Subject: [PATCH 23/39] Last polishing --- src/main/scala/iog/psg/cardano/CardanoApiCodec.scala | 2 +- src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index d21d173..3bb8e83 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -300,7 +300,7 @@ object CardanoApiCodec { ) @ConfiguredJsonCodec - case class Passphrase(lastUpdatedAt: ZonedDateTime) + final case class Passphrase(lastUpdatedAt: ZonedDateTime) @ConfiguredJsonCodec case class Wallet( diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index ba90cba..1b5c40c 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -98,8 +98,6 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model ) compareWallets(wallet, properWallet) - - compareDelegation(delegation, properDelegation) } "Bad wallet creation" should "be prevented" in { From d66a598933d6152d7cb9c997bbe088546857f3ce Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 08:43:37 +0100 Subject: [PATCH 24/39] Fix update pass message --- src/main/scala/iog/psg/cardano/CardanoApiMain.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 2aef7a1..e370047 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -123,7 +123,7 @@ object CardanoApiMain { val newPassphrase = arguments.get(CmdLine.passphrase) val result: Unit = unwrap(api.updatePassphrase(walletId, oldPassphrase, newPassphrase).executeBlocking) - trace("Unit result from delete wallet") + trace("Unit result from update passphrase") } else if (hasArgument(CmdLine.deleteWallet)) { val walletId = arguments.get(CmdLine.walletId) From 8fd1db85dac0d6dc4f1142c962bf54149b5aafc1 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 10:55:10 +0100 Subject: [PATCH 25/39] IT WIP --- .../iog/psg/cardano/CardanoApiMain.scala | 4 - .../iog/psg/cardano/CardanoApiCodecSpec.scala | 1 + .../iog/psg/cardano/CardanoApiMainSpec.scala | 177 ++++++++++-------- 3 files changed, 97 insertions(+), 85 deletions(-) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index e370047..d8a37e2 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -193,10 +193,6 @@ object CardanoApiMain { result.foreach(trace.apply) } - } else if (hasArgument(CmdLine.listWallets)) { - val result = unwrap(api.listWallets.executeBlocking) - result.foreach(trace.apply) - } else if (hasArgument(CmdLine.createWallet) || hasArgument(CmdLine.restoreWallet)) { val name = arguments.get(CmdLine.name) val passphrase = arguments.get(CmdLine.passphrase) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 4cf45df..0cc9f22 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -161,6 +161,7 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { ) ), status = TxState.pending, + //TODO fix toMapMetadataStr metadata = Some(TxMetadataOut(json = parse(""" |{ | "0": { diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 5856982..1aaf3ab 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -47,122 +47,128 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S results.reverse } - "The Cmd line Main" should "support retrieving netInfo" in { - val results = runCmdLine(CmdLine.netInfo) - assert(results.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl' \n $results") + "The Cmd line -netInfo" should "support retrieving netInfo" in { + val cmdLineResults = runCmdLine(CmdLine.netInfo) + assert(cmdLineResults.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl' \n $cmdLineResults") } - it should "not create a wallet with a bad mnemonic" in { - val badMnem = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21" - val results = runCmdLine( - CmdLine.createWallet, - CmdLine.passphrase, "password", - CmdLine.name, "some name", - CmdLine.mnemonic, badMnem) - assert(results.exists(_.contains("Found an unknown word")), "Bad menmonic not stopped") - } - - it should "find our test wallet" in { - val wallets = runCmdLine( + "The Cmd Line -wallets" should "show our test wallet in the list" in { + val cmdLineResults = runCmdLine( CmdLine.listWallets) - wallets.find(w => w.contains(testWalletName) && + cmdLineResults.find(w => w.contains(testWalletName) && w.contains(testWalletId)) .getOrElse { - val results = runCmdLine( + val cmdLineResults = runCmdLine( CmdLine.createWallet, CmdLine.passphrase, testWalletPassphrase, CmdLine.name, testWalletName, CmdLine.mnemonic, testWalletMnemonic) - assert(results.exists(_.contains(testWalletId)), "Test Wallet not created") + assert(cmdLineResults.exists(_.contains(testWalletId)), "Test Wallet not created") } } - it should "get our wallet" in { - val results = runCmdLine( - CmdLine.getWallet, + "The Cmd Line -estimateFee" should "estimate transaction costs" in { + val unusedAddr = getUnusedAddressWallet1 + + val cmdLineResults = runCmdLine( + CmdLine.estimateFee, + CmdLine.amount, testAmountToTransfer, + CmdLine.address, unusedAddr, CmdLine.walletId, testWalletId) - assert(results.exists(_.contains(testWalletId)), "Test wallet not found.") + assert(cmdLineResults.exists(_.contains("EstimateFeeResponse(QuantityUnit("))) + } + "The Cmd Line -wallet [walletId]" should "get our wallet" in { + val cmdLineResults = runCmdLine( + CmdLine.getWallet, + CmdLine.walletId, testWalletId) + + assert(cmdLineResults.exists(_.contains(testWalletId)), "Test wallet not found.") } - it should "create or find wallet 2" in { + "The Cmd Line -createWallet" should "create wallet 2" in { + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, testWallet2Passphrase, + CmdLine.name, testWallet2Name, + CmdLine.mnemonic, testWallet2Mnemonic) - val wallets = runCmdLine(CmdLine.listWallets) + println(results) - wallets.find(w => w.contains(testWallet2Name) && - w.contains(testWallet2Id)) - .getOrElse { - val results = runCmdLine( - CmdLine.createWallet, - CmdLine.passphrase, testWallet2Passphrase, - CmdLine.name, testWallet2Name, - CmdLine.mnemonic, testWallet2Mnemonic) + assert(results.last.contains(testWallet2Id), "Test wallet 2 not found.") + } - assert(results.last.contains(testWallet2Id), "Test wallet 2 not found.") - } + it should "not create a wallet with a bad mnemonic" in { + val badMnem = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21" + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, "password", + CmdLine.name, "some name", + CmdLine.mnemonic, badMnem) + assert(results.exists(_.contains("Found an unknown word")), "Bad menmonic not stopped") } - it should "allow password change in test wallet 2" in { - runCmdLine( + "The Cmd Line -updatePassphrase" should "allow password change in test wallet 2" in { + val cmdLineResults = runCmdLine( CmdLine.updatePassphrase, CmdLine.oldPassphrase, testWallet2Passphrase, CmdLine.passphrase, testWalletPassphrase, CmdLine.walletId, testWallet2Id) + assert(cmdLineResults.exists(_.contains("Unit result from update passphrase"))) } - it should "fund payments" in { + "The Cmd Line -deleteWallet [walletId]" should "delete test wallet 2" in { + val cmdLineResults = runCmdLine( + CmdLine.deleteWallet, + CmdLine.walletId, testWallet2Id) + + assert(cmdLineResults.exists(_.contains("Unit result from delete wallet"))) val results = runCmdLine( - CmdLine.fundTx, - CmdLine.amount, testAmountToTransfer, - CmdLine.address, getUnusedAddressWallet2, - CmdLine.walletId, testWalletId) + CmdLine.getWallet, + CmdLine.walletId, testWallet2Id) - assert(results.last.contains("FundPaymentsResponse") || - results.mkString("").contains("cannot_cover_fee"), s"$results") - //results.foreach(println) + assert(results.exists(!_.contains(testWalletId)), "Test wallet found after deletion?") } - private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) - - private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) - - def getUnusedAddress(walletId: String): String = { - val results = runCmdLine( + "The Cmd Line -listAddresses -walletId [walletId] -state [state]" should "list unused wallet addresses" in { + val cmdLineResults = runCmdLine( CmdLine.listWalletAddresses, CmdLine.state, "unused", - CmdLine.walletId, walletId) - + CmdLine.walletId, testWalletId) - val all = results.last.split(",") - val cleanedUp = all.map(s => { - if (s.indexOf("addr") > 0) - Some(s.substring(s.indexOf("addr"))) - else None - }) collect { - case Some(goodAddr) => goodAddr - } - cleanedUp.head + assert(cmdLineResults.exists(_.contains("Some(unused)"))) + cmdLineResults.count(_.contains("Some(used)")) shouldBe 0 } - it should "transact from a to a with metadata" in { + it should "list used wallet addresses" in { + val cmdLineResults = runCmdLine( + CmdLine.listWalletAddresses, + CmdLine.state, "used", + CmdLine.walletId, testWalletId) - val unusedAddr = getUnusedAddressWallet1 + assert(cmdLineResults.exists(_.contains("Some(used)"))) + cmdLineResults.count(_.contains("Some(unused)")) shouldBe 0 + } - // estimate fee - val estimateResults = runCmdLine( - CmdLine.estimateFee, + "The Cmd Line -fundTx" should "fund payments" in { + val cmdLineResults = runCmdLine( + CmdLine.fundTx, CmdLine.amount, testAmountToTransfer, - CmdLine.address, unusedAddr, + CmdLine.address, getUnusedAddressWallet2, CmdLine.walletId, testWalletId) - //estimateResults.foreach(println) + assert(cmdLineResults.last.contains("FundPaymentsResponse") || + cmdLineResults.mkString("").contains("cannot_cover_fee"), s"$cmdLineResults") + } + "The Cmd Lines -createTx, -getTx, -listTxs" should "transact from A to B with metadata, txId should be visible in get and list" in { + val unusedAddr = getUnusedAddressWallet1 val preTxTime = ZonedDateTime.now().minusMinutes(1) val resultsCreateTx = runCmdLine( @@ -183,7 +189,6 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S CmdLine.walletId, testWalletId) assert(resultsGetTx.last.contains(txId), "The getTx result didn't contain the id") - //list Txs val postTxTime = ZonedDateTime.now().plusMinutes(5) @@ -193,27 +198,37 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S CmdLine.`end`, postTxTime.toString, CmdLine.walletId, testWalletId) - val foundTx = listWalletTxs.exists(_.contains(txId)) assert(foundTx, s"Couldn't find txId $txId in transactions ") - } - def extractTxId(toStringCreateTransactionResult: String): String = { - toStringCreateTransactionResult.split(",").head.stripPrefix("CreateTransactionResponse(") - } + //=--------------> - it should "delete test wallet 2" in { - runCmdLine( - CmdLine.deleteWallet, - CmdLine.walletId, testWallet2Id) + private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) + + private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) + + def getUnusedAddress(walletId: String): String = { val results = runCmdLine( - CmdLine.getWallet, - CmdLine.walletId, testWallet2Id) + CmdLine.listWalletAddresses, + CmdLine.state, "unused", + CmdLine.walletId, walletId) - assert(results.exists(!_.contains(testWalletId)), "Test wallet found after deletion?") + val all = results.last.split(",") + val cleanedUp = all.map(s => { + if (s.indexOf("addr") > 0) + Some(s.substring(s.indexOf("addr"))) + else None + }) collect { + case Some(goodAddr) => goodAddr + } + cleanedUp.head + } + + def extractTxId(toStringCreateTransactionResult: String): String = { + toStringCreateTransactionResult.split(",").head.stripPrefix("CreateTransactionResponse(") } "--help" should "show possible commands" in { From c06e132c217b5895f03d64e1bbc822ee4cb94ad5 Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 10:56:13 +0100 Subject: [PATCH 26/39] Removed printlns + rename to expected --- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 4 - .../iog/psg/cardano/util/ModelCompare.scala | 164 +++++++++--------- 2 files changed, 82 insertions(+), 86 deletions(-) diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 509cd2b..5274cf4 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -102,10 +102,6 @@ public static CardanoApi buildWithDummyApiExecutor() { public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { CompletableFuture result = new CompletableFuture<>(); - System.out.println(request.request().uri().path()); - System.out.println(request.request().uri().fragment()); - System.out.println(request.request().uri()); - if(request.request().uri().path().endsWith("wallets", true)) { Enumeration.Value lovelace = CardanoApiCodec.Units$.MODULE$.Value(CardanoApiCodec.Units$.MODULE$.lovelace().toString()); Enumeration.Value sync = CardanoApiCodec.SyncState$.MODULE$.Value(CardanoApiCodec.SyncState$.MODULE$.ready().toString()); diff --git a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala index 2a004df..65bd7df 100644 --- a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala +++ b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala @@ -6,131 +6,131 @@ import org.scalatest.matchers.should.Matchers trait ModelCompare extends Matchers { - final def compareInAddress(decoded: InAddress, proper: InAddress): Assertion = { - decoded.address shouldBe proper.address - compareQuantityUnitOpts(decoded.amount, proper.amount) - decoded.id shouldBe proper.id - decoded.index shouldBe proper.index + final def compareInAddress(decoded: InAddress, expected: InAddress): Assertion = { + decoded.address shouldBe expected.address + compareQuantityUnitOpts(decoded.amount, expected.amount) + decoded.id shouldBe expected.id + decoded.index shouldBe expected.index } - final def compareOutAddress(decoded: OutAddress, proper: OutAddress): Assertion = { - decoded.address shouldBe proper.address - compareQuantityUnit(decoded.amount, proper.amount) + final def compareOutAddress(decoded: OutAddress, expected: OutAddress): Assertion = { + decoded.address shouldBe expected.address + compareQuantityUnit(decoded.amount, expected.amount) } - final def compareInputs(decoded: Seq[InAddress], proper: Seq[InAddress]): Seq[Assertion] = - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareInAddress(decodedAddress, properAddress) + final def compareInputs(decoded: Seq[InAddress], expected: Seq[InAddress]): Seq[Assertion] = + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareInAddress(decodedAddress, expectedAddress) } - final def compareOutputs(decoded: Seq[OutAddress], proper: Seq[OutAddress]): Seq[Assertion] = - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareOutAddress(decodedAddress, properAddress) + final def compareOutputs(decoded: Seq[OutAddress], expected: Seq[OutAddress]): Seq[Assertion] = + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareOutAddress(decodedAddress, expectedAddress) } - final def compareFundPaymentsResponse(decoded: FundPaymentsResponse, proper: FundPaymentsResponse): Seq[Assertion] = { - compareInputs(decoded.inputs, proper.inputs) - compareOutputs(decoded.outputs, proper.outputs) + final def compareFundPaymentsResponse(decoded: FundPaymentsResponse, expected: FundPaymentsResponse): Seq[Assertion] = { + compareInputs(decoded.inputs, expected.inputs) + compareOutputs(decoded.outputs, expected.outputs) } - final def compareEstimateFeeResponse(decoded: EstimateFeeResponse, proper: EstimateFeeResponse): Assertion = { - compareQuantityUnit(decoded.estimatedMax, proper.estimatedMax) - compareQuantityUnit(decoded.estimatedMin, proper.estimatedMin) + final def compareEstimateFeeResponse(decoded: EstimateFeeResponse, expected: EstimateFeeResponse): Assertion = { + compareQuantityUnit(decoded.estimatedMax, expected.estimatedMax) + compareQuantityUnit(decoded.estimatedMin, expected.estimatedMin) } - final def compareStakeAddress(decoded: StakeAddress, proper: StakeAddress): Assertion = { - compareQuantityUnit(decoded.amount, proper.amount) - decoded.stakeAddress shouldBe proper.stakeAddress + final def compareStakeAddress(decoded: StakeAddress, expected: StakeAddress): Assertion = { + compareQuantityUnit(decoded.amount, expected.amount) + decoded.stakeAddress shouldBe expected.stakeAddress } - final def compareStakeAddresses(decoded: Seq[StakeAddress], proper: Seq[StakeAddress]): Seq[Assertion] = { - decoded.zip(proper).map { - case (decodedAddress, properAddress) => compareStakeAddress(decodedAddress, properAddress) + final def compareStakeAddresses(decoded: Seq[StakeAddress], expected: Seq[StakeAddress]): Seq[Assertion] = { + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareStakeAddress(decodedAddress, expectedAddress) } } - final def compareTransaction(decoded: CreateTransactionResponse, proper: CreateTransactionResponse): Assertion = { - decoded.id shouldBe proper.id - compareQuantityUnit(decoded.amount, proper.amount) - decoded.insertedAt shouldBe proper.insertedAt - decoded.pendingSince shouldBe proper.pendingSince - decoded.depth shouldBe proper.depth - decoded.direction shouldBe proper.direction - compareInputs(decoded.inputs, proper.inputs) - compareOutputs(decoded.outputs, proper.outputs) - compareStakeAddresses(decoded.withdrawals, proper.withdrawals) - decoded.status shouldBe proper.status - decoded.metadata shouldBe proper.metadata + final def compareTransaction(decoded: CreateTransactionResponse, expected: CreateTransactionResponse): Assertion = { + decoded.id shouldBe expected.id + compareQuantityUnit(decoded.amount, expected.amount) + decoded.insertedAt shouldBe expected.insertedAt + decoded.pendingSince shouldBe expected.pendingSince + decoded.depth shouldBe expected.depth + decoded.direction shouldBe expected.direction + compareInputs(decoded.inputs, expected.inputs) + compareOutputs(decoded.outputs, expected.outputs) + compareStakeAddresses(decoded.withdrawals, expected.withdrawals) + decoded.status shouldBe expected.status + decoded.metadata shouldBe expected.metadata } - final def compareAddress(decoded: WalletAddressId, proper: WalletAddressId): Assertion = { - decoded.id shouldBe proper.id - decoded.state shouldBe proper.state + final def compareAddress(decoded: WalletAddressId, expected: WalletAddressId): Assertion = { + decoded.id shouldBe expected.id + decoded.state shouldBe expected.state } - final def compareNetworkInformation(decoded: NetworkInfo, proper: NetworkInfo): Assertion = { - decoded.nextEpoch shouldBe proper.nextEpoch - decoded.nodeTip shouldBe proper.nodeTip - decoded.networkTip shouldBe proper.networkTip - decoded.syncProgress.status.toString shouldBe proper.syncProgress.status.toString + final def compareNetworkInformation(decoded: NetworkInfo, expected: NetworkInfo): Assertion = { + decoded.nextEpoch shouldBe expected.nextEpoch + decoded.nodeTip shouldBe expected.nodeTip + decoded.networkTip shouldBe expected.networkTip + decoded.syncProgress.status.toString shouldBe expected.syncProgress.status.toString - compareQuantityUnitOpts(decoded.syncProgress.progress, proper.syncProgress.progress) + compareQuantityUnitOpts(decoded.syncProgress.progress, expected.syncProgress.progress) } - final def compareQuantityUnitOpts(decoded: Option[QuantityUnit], proper: Option[QuantityUnit]): Assertion = { - if (decoded.isEmpty && proper.isEmpty) assert(true) + final def compareQuantityUnitOpts(decoded: Option[QuantityUnit], expected: Option[QuantityUnit]): Assertion = { + if (decoded.isEmpty && expected.isEmpty) assert(true) else (for { decodedQU <- decoded - properQU <- proper - } yield compareQuantityUnit(decodedQU, properQU)).getOrElse(assert(false, "one of units is none")) + expectedQU <- expected + } yield compareQuantityUnit(decodedQU, expectedQU)).getOrElse(assert(false, "one of units is none")) } - final def compareQuantityUnit(decoded: QuantityUnit, proper: QuantityUnit): Assertion = { - decoded.unit.toString shouldBe proper.unit.toString - decoded.quantity shouldBe proper.quantity + final def compareQuantityUnit(decoded: QuantityUnit, expected: QuantityUnit): Assertion = { + decoded.unit.toString shouldBe expected.unit.toString + decoded.quantity shouldBe expected.quantity } - final def compareBalance(decoded: Balance, proper: Balance): Assertion = { - decoded.available.quantity shouldBe proper.available.quantity - decoded.available.unit.toString shouldBe proper.available.unit.toString + final def compareBalance(decoded: Balance, expected: Balance): Assertion = { + decoded.available.quantity shouldBe expected.available.quantity + decoded.available.unit.toString shouldBe expected.available.unit.toString - decoded.reward.quantity shouldBe proper.reward.quantity - decoded.reward.unit.toString shouldBe proper.reward.unit.toString + decoded.reward.quantity shouldBe expected.reward.quantity + decoded.reward.unit.toString shouldBe expected.reward.unit.toString - decoded.total.quantity shouldBe proper.total.quantity - decoded.total.unit.toString shouldBe proper.total.unit.toString + decoded.total.quantity shouldBe expected.total.quantity + decoded.total.unit.toString shouldBe expected.total.unit.toString } - final def compareState(decoded: SyncStatus, proper: SyncStatus): Assertion = { - decoded.status.toString shouldBe proper.status.toString - decoded.progress shouldBe proper.progress + final def compareState(decoded: SyncStatus, expected: SyncStatus): Assertion = { + decoded.status.toString shouldBe expected.status.toString + decoded.progress shouldBe expected.progress } - final def compareDelegation(decoded: Delegation, proper: Delegation): Seq[Assertion] = { - decoded.active.status.toString shouldBe proper.active.status.toString - decoded.active.target shouldBe proper.active.target + final def compareDelegation(decoded: Delegation, expected: Delegation): Seq[Assertion] = { + decoded.active.status.toString shouldBe expected.active.status.toString + decoded.active.target shouldBe expected.active.target - decoded.next.zip(proper.next).map { - case (decodedNext, properNext) => - decodedNext.status.toString shouldBe properNext.status.toString - decodedNext.changesAt shouldBe properNext.changesAt + decoded.next.zip(expected.next).map { + case (decodedNext, expectedNext) => + decodedNext.status.toString shouldBe expectedNext.status.toString + decodedNext.changesAt shouldBe expectedNext.changesAt } } - final def compareDelegationOpts(decoded: Option[Delegation], proper: Option[Delegation]): Seq[Assertion] = { - if (decoded.nonEmpty && proper.nonEmpty) compareDelegation(decoded.get, proper.get) + final def compareDelegationOpts(decoded: Option[Delegation], expected: Option[Delegation]): Seq[Assertion] = { + if (decoded.nonEmpty && expected.nonEmpty) compareDelegation(decoded.get, expected.get) else Seq(assert(false, "one of delegations is none")) } - final def compareWallets(decoded: Wallet, proper: Wallet): Assertion = { - decoded.id shouldBe proper.id - decoded.addressPoolGap shouldBe proper.addressPoolGap - compareBalance(decoded.balance, proper.balance) - compareDelegationOpts(decoded.delegation, proper.delegation) - decoded.name shouldBe proper.name - decoded.passphrase shouldBe proper.passphrase - compareState(decoded.state, proper.state) - decoded.tip shouldBe proper.tip + final def compareWallets(decoded: Wallet, expected: Wallet): Assertion = { + decoded.id shouldBe expected.id + decoded.addressPoolGap shouldBe expected.addressPoolGap + compareBalance(decoded.balance, expected.balance) + compareDelegationOpts(decoded.delegation, expected.delegation) + decoded.name shouldBe expected.name + decoded.passphrase shouldBe expected.passphrase + compareState(decoded.state, expected.state) + decoded.tip shouldBe expected.tip } } From 2175a412a799d0d13d14404120705e10b2b81155 Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Thu, 1 Oct 2020 11:01:15 +0100 Subject: [PATCH 27/39] Task/psgs 38 publish to maven (#9) * Prepare to publish to Maven. * Tidy up, add sonatypeRelease --- .github/workflows/publish.yml | 31 +++++++++++++++++++++++ .github/workflows/{ci.yml => test.yml} | 12 +-------- build.sbt | 34 ++++++++++++++++++++------ cmdline.sh | 2 +- project/plugins.sbt | 10 +++++--- 5 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/publish.yml rename .github/workflows/{ci.yml => test.yml} (75%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4f9734a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish to Maven + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONA_USER: ${{ secrets.SONA_USER }} + SONA_PASS: ${{ secrets.SONA_PASS }} + +on: + push: + tags: + - 'v*' + +jobs: + package: + if: contains(github.ref, 'master') || contains(github.ref, 'develop') + runs-on: ubuntu-latest + steps: + - name: Configure GPG Key + run: | + mkdir -p ~/.gnupg/ + printf "$GPG_SIGNING_KEY" | base64 --decode > ~/.gnupg/private.key + gpg --import --no-tty --batch --yes ~/.gnupg/private.key + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + - uses: actions/checkout@v2 + - name: Package + uses: actions/setup-java@v1.4.2 + with: + java-version: '11.0.8' + - run: sbt publishSigned sonatypeRelease + diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 75% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index c33a229..b6a0501 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Clean, build, test and package +name: Clean, build, test, coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,14 +34,4 @@ jobs: with: name: code-coverage-report path: target/scala-2.13/scoverage-report - package: - if: contains(github.ref, 'release') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Package - uses: actions/setup-java@v1.4.2 - with: - java-version: '11.0.8' - - run: sbt publish diff --git a/build.sbt b/build.sbt index c142e59..7658164 100644 --- a/build.sbt +++ b/build.sbt @@ -1,18 +1,38 @@ -import sbtghpackages.TokenSource.{GitConfig,Or,Environment} name:= "psg-cardano-wallet-api" -version := "0.1.3-SNAPSHOT" - scalaVersion := "2.13.3" -organization := "iog.psg" +organization := "solutions.iog" + +homepage := Some(url("https://github.com/input-output-hk/psg-cardano-wallet-api")) +scmInfo := Some(ScmInfo(url("https://github.com/input-output-hk/psg-cardano-wallet-api"), "scm:git@github.com:input-output-hk/psg-cardano-wallet-api.git")) +developers := List( + Developer("mcsherrylabs", "Alan McSherry", "alan.mcsherry@iohk.io", url("https://github.com/mcsherrylabs")), + Developer("maciejbak85", "Maciej Bak", "maciej.bak@iohk.io", url("https://github.com/maciejbak85")) +) +publishMavenStyle := true +licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")) +description := "A java/scala wrapper for the cardano wallet backend API" +usePgpKeyHex("75E12F006A3F08C757EE8343927AE95EEEF4A02F") + + -githubOwner := "input-output-hk" +publishTo := Some { + // publish to the sonatype repository + val sonaUrl = "https://oss.sonatype.org/" + if (isSnapshot.value) + "snapshots" at sonaUrl + "content/repositories/snapshots" + else + "releases" at sonaUrl + "service/local/staging/deploy/maven2" +} -githubRepository := "psg-cardano-wallet-api" +credentials += Credentials("Sonatype Nexus Repository Manager", + "oss.sonatype.org", + sys.env.getOrElse("SONA_USER", ""), + sys.env.getOrElse("SONA_PASS", "")) -githubTokenSource := Or(GitConfig("github.token"), Environment("GITHUB_TOKEN")) +dynverSonatypeSnapshots in ThisBuild := true val akkaVersion = "2.6.8" val akkaHttpVersion = "10.2.0" diff --git a/cmdline.sh b/cmdline.sh index cd28d99..0234905 100755 --- a/cmdline.sh +++ b/cmdline.sh @@ -2,7 +2,7 @@ VER=0.1.3-SNAPSHOT #BASE_URL="http://cardano-wallet-testnet.iog.solutions:8090/v2/" -BASE_URL="http://localhost:8090/v2/" +#BASE_URL="http://localhost:8090/v2/" #run sbt assembly to create this jar exec java -jar target/scala-2.13/psg-cardano-wallet-api-assembly-${VER}.jar -baseUrl ${BASE_URL} "$@" diff --git a/project/plugins.sbt b/project/plugins.sbt index 32411f6..e98e26c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,9 @@ -addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.2") - addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") + +// sbt-sonatype plugin used to publish artifact to maven central via sonatype nexus +// sbt-pgp plugin used to sign the artifcat with pgp keys +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.4") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") +addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") From e70baaedb6ba5fb82a6fbd2690e5d7abb01f736c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Thu, 1 Oct 2020 11:08:25 +0100 Subject: [PATCH 28/39] Task/psgs 44 sub 54 jsons (#11) * Filled up missing fields from API responses #PSGS-54 * Finished units for codec decoders * Added more check for new model in jpi checks * JPI checks all fields now * Use codecs only when needed ( snake case ) * Imports optimized --- .../scala/iog/psg/cardano/CardanoApi.scala | 1 - .../iog/psg/cardano/CardanoApiCodec.scala | 27 +- .../iog/psg/cardano/CardanoApiMain.scala | 2 +- .../iog/psg/cardano/jpi/JpiResponseCheck.java | 21 +- src/test/resources/jsons/addresses.json | 6 + .../jsons/coin_selections_random.json | 22 ++ src/test/resources/jsons/estimate_fees.json | 10 + src/test/resources/jsons/netinfo.json | 23 ++ src/test/resources/jsons/transaction.json | 110 ++++++++ src/test/resources/jsons/transactions.json | 112 +++++++++ src/test/resources/jsons/wallet.json | 49 ++++ src/test/resources/jsons/wallets.json | 51 ++++ .../iog/psg/cardano/CardanoApiCodecSpec.scala | 235 ++++++++++++++++++ .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 61 ++++- .../iog/psg/cardano/util/ModelCompare.scala | 136 ++++++++++ 15 files changed, 851 insertions(+), 15 deletions(-) create mode 100644 src/test/resources/jsons/addresses.json create mode 100644 src/test/resources/jsons/coin_selections_random.json create mode 100644 src/test/resources/jsons/estimate_fees.json create mode 100644 src/test/resources/jsons/netinfo.json create mode 100644 src/test/resources/jsons/transaction.json create mode 100644 src/test/resources/jsons/transactions.json create mode 100644 src/test/resources/jsons/wallet.json create mode 100644 src/test/resources/jsons/wallets.json create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/util/ModelCompare.scala diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index edfc6e2..14dd417 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -4,7 +4,6 @@ package iog.psg.cardano import java.time.ZonedDateTime import akka.actor.ActorSystem -import akka.http.scaladsl.Http import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.Uri.Query diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 2c78160..3bb8e83 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -1,6 +1,5 @@ package iog.psg.cardano -import java.nio.charset.StandardCharsets import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -11,13 +10,14 @@ import akka.http.scaladsl.unmarshalling.Unmarshaller.eitherUnmarshaller import akka.stream.Materializer import akka.util.ByteString import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ +import io.circe._ import io.circe.generic.auto._ import io.circe.generic.extras._ import io.circe.generic.extras.semiauto.deriveConfiguredEncoder import io.circe.syntax.EncoderOps -import io.circe._ import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} import iog.psg.cardano.CardanoApiCodec.AddressFilter.AddressFilter +import iog.psg.cardano.CardanoApiCodec.DelegationStatus.DelegationStatus import iog.psg.cardano.CardanoApiCodec.SyncState.SyncState import iog.psg.cardano.CardanoApiCodec.TxDirection.TxDirection import iog.psg.cardano.CardanoApiCodec.TxState.TxState @@ -60,6 +60,8 @@ object CardanoApiCodec { private[cardano] implicit val decodeTxDirection: Decoder[TxDirection] = Decoder.decodeString.map(TxDirection.withName) private[cardano] implicit val encodeTxDirection: Encoder[TxDirection] = (a: TxDirection) => Json.fromString(a.toString) + private[cardano] implicit val decodeDelegationStatus: Decoder[DelegationStatus] = Decoder.decodeString.map(DelegationStatus.withName) + private[cardano] implicit val encodeDelegationStatus: Encoder[DelegationStatus] = (a: DelegationStatus) => Json.fromString(a.toString) final case class TxMetadataOut(json: Json) { def toMapMetadataStr: Decoder.Result[Map[Long, String]] = json.as[Map[Long, String]] @@ -135,13 +137,22 @@ object CardanoApiCodec { } + object DelegationStatus extends Enumeration { + type DelegationStatus = Value + val delegating: DelegationStatus = Value("delegating") + val notDelegating: DelegationStatus = Value("not_delegating") + } + final case class DelegationActive(status: DelegationStatus, target: Option[String]) + @ConfiguredJsonCodec final case class DelegationNext(status: DelegationStatus, changesAt: Option[NextEpoch]) + final case class Delegation(active: DelegationActive, next: List[DelegationNext]) @ConfiguredJsonCodec case class NetworkTip( epochNumber: Long, slotNumber: Long, - height: Option[QuantityUnit]) + height: Option[QuantityUnit], + absoluteSlotNumber: Option[Long]) - case class NodeTip(height: QuantityUnit) + @ConfiguredJsonCodec case class NodeTip(height: QuantityUnit, slotNumber: Long, epochNumber: Long, absoluteSlotNumber: Option[Long]) case class WalletAddressId(id: String, state: Option[AddressFilter]) @@ -256,7 +267,8 @@ object CardanoApiCodec { case class Block( slotNumber: Int, epochNumber: Int, - height: QuantityUnit + height: QuantityUnit, + absoluteSlotNumber: Option[Long] ) @ConfiguredJsonCodec @@ -287,12 +299,17 @@ object CardanoApiCodec { metadata: Option[TxMetadataOut] ) + @ConfiguredJsonCodec + final case class Passphrase(lastUpdatedAt: ZonedDateTime) + @ConfiguredJsonCodec case class Wallet( id: String, addressPoolGap: Int, balance: Balance, + delegation: Option[Delegation], name: String, + passphrase: Passphrase, state: SyncStatus, tip: NetworkTip ) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 2aef7a1..e370047 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -123,7 +123,7 @@ object CardanoApiMain { val newPassphrase = arguments.get(CmdLine.passphrase) val result: Unit = unwrap(api.updatePassphrase(walletId, oldPassphrase, newPassphrase).executeBlocking) - trace("Unit result from delete wallet") + trace("Unit result from update passphrase") } else if (hasArgument(CmdLine.deleteWallet)) { val walletId = arguments.get(CmdLine.walletId) diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 8b18b3e..5274cf4 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -3,7 +3,10 @@ import iog.psg.cardano.CardanoApiCodec; import scala.Enumeration; import scala.Option; +import scala.jdk.CollectionConverters; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -99,10 +102,6 @@ public static CardanoApi buildWithDummyApiExecutor() { public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { CompletableFuture result = new CompletableFuture<>(); - System.out.println(request.request().uri().path()); - System.out.println(request.request().uri().fragment()); - System.out.println(request.request().uri()); - if(request.request().uri().path().endsWith("wallets", true)) { Enumeration.Value lovelace = CardanoApiCodec.Units$.MODULE$.Value(CardanoApiCodec.Units$.MODULE$.lovelace().toString()); Enumeration.Value sync = CardanoApiCodec.SyncState$.MODULE$.Value(CardanoApiCodec.SyncState$.MODULE$.ready().toString()); @@ -111,12 +110,24 @@ public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiReque sync, Option.apply(null) ); - CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null)); + CardanoApiCodec.NetworkTip tip = new CardanoApiCodec.NetworkTip(3,4,Option.apply(null), Option.apply(10)); + + ZonedDateTime dummyDate = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"); + Enumeration.Value delegatingStatus = CardanoApiCodec.DelegationStatus$.MODULE$.Value(CardanoApiCodec.DelegationStatus$.MODULE$.delegating().toString()); + Enumeration.Value notDelegatingStatus = CardanoApiCodec.DelegationStatus$.MODULE$.Value(CardanoApiCodec.DelegationStatus$.MODULE$.notDelegating().toString()); + CardanoApiCodec.DelegationActive delegationActive = new CardanoApiCodec.DelegationActive(delegatingStatus, Option.apply("1234567890")); + CardanoApiCodec.DelegationNext delegationNext = new CardanoApiCodec.DelegationNext(notDelegatingStatus, Option.apply(new CardanoApiCodec.NextEpoch(dummyDate, 10))); + List nexts = Arrays.asList(delegationNext); + scala.collection.immutable.List nextsScalaList = CollectionConverters.ListHasAsScala(nexts).asScala().toList(); + CardanoApiCodec.Delegation delegation = new CardanoApiCodec.Delegation(delegationActive, nextsScalaList); + result.complete((T) new CardanoApiCodec.Wallet( "id", 10, new CardanoApiCodec.Balance(dummy, dummy, dummy), + Option.apply(delegation), "name", + new CardanoApiCodec.Passphrase(dummyDate), state, tip)); return result.toCompletableFuture(); diff --git a/src/test/resources/jsons/addresses.json b/src/test/resources/jsons/addresses.json new file mode 100644 index 0000000..ef15cbc --- /dev/null +++ b/src/test/resources/jsons/addresses.json @@ -0,0 +1,6 @@ +[ + { + "id": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "used" + } +] \ No newline at end of file diff --git a/src/test/resources/jsons/coin_selections_random.json b/src/test/resources/jsons/coin_selections_random.json new file mode 100644 index 0000000..8c95baa --- /dev/null +++ b/src/test/resources/jsons/coin_selections_random.json @@ -0,0 +1,22 @@ +{ + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/jsons/estimate_fees.json b/src/test/resources/jsons/estimate_fees.json new file mode 100644 index 0000000..ed2c553 --- /dev/null +++ b/src/test/resources/jsons/estimate_fees.json @@ -0,0 +1,10 @@ +{ + "estimated_min": { + "quantity": 42000000, + "unit": "lovelace" + }, + "estimated_max": { + "quantity": 42000000, + "unit": "lovelace" + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/netinfo.json b/src/test/resources/jsons/netinfo.json new file mode 100644 index 0000000..4bd32d2 --- /dev/null +++ b/src/test/resources/jsons/netinfo.json @@ -0,0 +1,23 @@ +{ + "sync_progress": { + "status": "ready" + }, + "node_tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + }, + "network_tip": { + "slot_number": 1337, + "epoch_number": 14, + "absolute_slot_number": 8086 + }, + "next_epoch": { + "epoch_number": 14, + "epoch_start_time": "2019-02-27T14:46:45.000Z" + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/transaction.json b/src/test/resources/jsons/transaction.json new file mode 100644 index 0000000..aa1692a --- /dev/null +++ b/src/test/resources/jsons/transaction.json @@ -0,0 +1,110 @@ +{ + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "inserted_at": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "pending_since": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "depth": { + "quantity": 1337, + "unit": "block" + }, + "direction": "outgoing", + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "withdrawals": [ + { + "stake_address": "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "status": "pending", + "metadata": { + "0": { + "string": "cardano" + }, + "1": { + "int": 14 + }, + "2": { + "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + }, + "3": { + "list": [ + { + "int": 14 + }, + { + "int": 42 + }, + { + "string": "1337" + } + ] + }, + "4": { + "map": [ + { + "k": { + "string": "key" + }, + "v": { + "string": "value" + } + }, + { + "k": { + "int": 14 + }, + "v": { + "int": 42 + } + } + ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/transactions.json b/src/test/resources/jsons/transactions.json new file mode 100644 index 0000000..3d83f59 --- /dev/null +++ b/src/test/resources/jsons/transactions.json @@ -0,0 +1,112 @@ +[ + { + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "inserted_at": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "pending_since": { + "time": "2019-02-27T14:46:45.000Z", + "block": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + }, + "depth": { + "quantity": 1337, + "unit": "block" + }, + "direction": "outgoing", + "inputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + }, + "id": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + "index": 0 + } + ], + "outputs": [ + { + "address": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "withdrawals": [ + { + "stake_address": "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + "amount": { + "quantity": 42000000, + "unit": "lovelace" + } + } + ], + "status": "pending", + "metadata": { + "0": { + "string": "cardano" + }, + "1": { + "int": 14 + }, + "2": { + "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + }, + "3": { + "list": [ + { + "int": 14 + }, + { + "int": 42 + }, + { + "string": "1337" + } + ] + }, + "4": { + "map": [ + { + "k": { + "string": "key" + }, + "v": { + "string": "value" + } + }, + { + "k": { + "int": 14 + }, + "v": { + "int": 42 + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/src/test/resources/jsons/wallet.json b/src/test/resources/jsons/wallet.json new file mode 100644 index 0000000..1a8b23a --- /dev/null +++ b/src/test/resources/jsons/wallet.json @@ -0,0 +1,49 @@ +{ + "id": "2512a00e9653fe49a44a5886202e24d77eeb998f", + "address_pool_gap": 20, + "balance": { + "available": { + "quantity": 42000000, + "unit": "lovelace" + }, + "reward": { + "quantity": 42000000, + "unit": "lovelace" + }, + "total": { + "quantity": 42000000, + "unit": "lovelace" + } + }, + "delegation": { + "active": { + "status": "delegating", + "target": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1" + }, + "next": [ + { + "status": "not_delegating", + "changes_at": { + "epoch_number": 14, + "epoch_start_time": "2020-01-22T10:06:39.037Z" + } + } + ] + }, + "name": "Alan's Wallet", + "passphrase": { + "last_updated_at": "2019-02-27T14:46:45.000Z" + }, + "state": { + "status": "ready" + }, + "tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } +} \ No newline at end of file diff --git a/src/test/resources/jsons/wallets.json b/src/test/resources/jsons/wallets.json new file mode 100644 index 0000000..5a1f37a --- /dev/null +++ b/src/test/resources/jsons/wallets.json @@ -0,0 +1,51 @@ +[ + { + "id": "2512a00e9653fe49a44a5886202e24d77eeb998f", + "address_pool_gap": 20, + "balance": { + "available": { + "quantity": 42000000, + "unit": "lovelace" + }, + "reward": { + "quantity": 42000000, + "unit": "lovelace" + }, + "total": { + "quantity": 42000000, + "unit": "lovelace" + } + }, + "delegation": { + "active": { + "status": "delegating", + "target": "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1" + }, + "next": [ + { + "status": "not_delegating", + "changes_at": { + "epoch_number": 14, + "epoch_start_time": "2020-01-22T10:06:39.037Z" + } + } + ] + }, + "name": "Alan's Wallet", + "passphrase": { + "last_updated_at": "2019-02-27T14:46:45.000Z" + }, + "state": { + "status": "ready" + }, + "tip": { + "slot_number": 1337, + "epoch_number": 14, + "height": { + "quantity": 1337, + "unit": "block" + }, + "absolute_slot_number": 8086 + } + } +] \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala new file mode 100644 index 0000000..4cf45df --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -0,0 +1,235 @@ +package iog.psg.cardano + +import java.time.ZonedDateTime + +import io.circe.Decoder +import io.circe.parser._ +import io.circe.generic.auto._ + +import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.util.ModelCompare + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.io.Source + +class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { + + "Wallet" should "be decoded properly" in { + val decoded = decodeJsonFile[Wallet]("wallet.json") + + compareWallets(decoded, wallet) + } + + it should "decode wallet's list" in { + val decodedWallets = decodeJsonFile[Seq[Wallet]]("wallets.json") + + decodedWallets.size shouldBe 1 + compareWallets(decodedWallets.head, wallet) + } + + "network information" should "be decoded properly" in { + val decoded = decodeJsonFile[NetworkInfo]("netinfo.json") + + compareNetworkInformation( + decoded, + NetworkInfo( + syncProgress = SyncStatus(SyncState.ready, None), + networkTip = networkTip.copy(height = None), + nodeTip = nodeTip, + nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) + ) + ) + + } + + "list addresses" should "be decoded properly" in { + val decoded = decodeJsonFile[Seq[WalletAddressId]]("addresses.json") + decoded.size shouldBe 1 + + compareAddress(decoded.head, WalletAddressId(id = addressIdStr, Some(AddressFilter.used))) + } + + "list transactions" should "be decoded properly" in { + val decoded = decodeJsonFile[Seq[CreateTransactionResponse]]("transactions.json") + decoded.size shouldBe 1 + + compareTransaction(decoded.head, createdTransactionResponse) + } + + it should "decode one transaction" in { + val decoded = decodeJsonFile[CreateTransactionResponse]("transaction.json") + + compareTransaction(decoded, createdTransactionResponse) + } + + "estimate fees" should "be decoded properly" in { + val decoded = decodeJsonFile[EstimateFeeResponse]("estimate_fees.json") + + compareEstimateFeeResponse(decoded, estimateFeeResponse) + } + + "fund payments" should "be decoded properly" in { + val decoded = decodeJsonFile[FundPaymentsResponse]("coin_selections_random.json") + + compareFundPaymentsResponse(decoded, fundPaymentsResponse) + } + + private def getJsonFromFile(file: String): String = { + val source = Source.fromURL(getClass.getResource(s"/jsons/$file")) + val jsonStr = source.mkString + source.close() + jsonStr + } + + + private def decodeJsonFile[T](file: String)(implicit dec: Decoder[T]) = { + val jsonStr = getJsonFromFile(file) + decode[T](jsonStr).getOrElse(fail("Could not decode wallet")) + } + + private final lazy val wallet = Wallet( + id = "2512a00e9653fe49a44a5886202e24d77eeb998f", + addressPoolGap = 20, + balance = Balance( + available = QuantityUnit(42000000, Units.lovelace), + reward = QuantityUnit(42000000, Units.lovelace), + total = QuantityUnit(42000000, Units.lovelace) + ), + delegation = Some( + Delegation( + active = DelegationActive( + status = DelegationStatus.delegating, + target = Some("1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1") + ), + next = List( + DelegationNext( + status = DelegationStatus.notDelegating, + changesAt = + Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14)) + ) + ) + ) + ), + name = "Alan's Wallet", + passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2019-02-27T14:46:45.000Z")), + state = SyncStatus(SyncState.ready, None), + tip = networkTip + ) + + private final lazy val networkTip = NetworkTip( + epochNumber = 14, + slotNumber = 1337, + height = Some(QuantityUnit(1337, Units.block)), + absoluteSlotNumber = Some(8086) + ) + + private final lazy val nodeTip = NodeTip( + epochNumber = 14, + slotNumber = 1337, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + + private final lazy val timedBlock = TimedBlock( + time = ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), + block = Block( + slotNumber = 1337, + epochNumber = 14, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + ) + + private final lazy val createdTransactionResponse = { + val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + + CreateTransactionResponse( + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + amount = commonAmount, + insertedAt = Some(timedBlock), + pendingSince = Some(timedBlock), + depth = Some(QuantityUnit(quantity = 1337, unit = Units.block)), + direction = TxDirection.outgoing, + inputs = Seq(inAddress), + outputs = Seq(outAddress), + withdrawals = Seq( + StakeAddress( + stakeAddress = "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + amount = commonAmount + ) + ), + status = TxState.pending, + metadata = Some(TxMetadataOut(json = parse(""" + |{ + | "0": { + | "string": "cardano" + | }, + | "1": { + | "int": 14 + | }, + | "2": { + | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + | }, + | "3": { + | "list": [ + | { + | "int": 14 + | }, + | { + | "int": 42 + | }, + | { + | "string": "1337" + | } + | ] + | }, + | "4": { + | "map": [ + | { + | "k": { + | "string": "key" + | }, + | "v": { + | "string": "value" + | } + | }, + | { + | "k": { + | "int": 14 + | }, + | "v": { + | "int": 42 + | } + | } + | ] + | } + | } + |""".stripMargin).getOrElse(fail("Invalid metadata json")))) + ) + } + + private final lazy val estimateFeeResponse = { + val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + + EstimateFeeResponse(estimatedMin = commonAmount, estimatedMax = commonAmount) + } + + private final lazy val fundPaymentsResponse = + FundPaymentsResponse(inputs = IndexedSeq(inAddress), outputs = Seq(outAddress)) + + private final lazy val inAddress = InAddress( + address = Some(addressIdStr), + amount = Some(QuantityUnit(quantity = 42000000, unit = Units.lovelace)), + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + index = 0 + ) + + private final lazy val outAddress = + OutAddress(address = addressIdStr, amount = QuantityUnit(quantity = 42000000, unit = Units.lovelace)) + + private final lazy val addressIdStr = + "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" + +} diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 0c566b1..1b5c40c 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -1,16 +1,17 @@ package iog.psg.cardano.jpi +import java.time.ZonedDateTime import java.util.concurrent.TimeUnit -import iog.psg.cardano.CardanoApiCodec.GenericMnemonicSentence -import iog.psg.cardano.util.Configure +import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.util.{Configure, ModelCompare} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} -class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { +class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare { private val baseUrl = config.getString("cardano.wallet.baseUrl") private val testWalletName = config.getString("cardano.wallet.name") @@ -42,7 +43,61 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure { mnem.mnemonicSentence.asJava, 10 ).toCompletableFuture.get(); + + wallet.id shouldBe "id" + + val delegation = wallet.delegation.getOrElse(fail("Missing delegation")) + val properDelegation = Delegation( + DelegationActive( + DelegationStatus.delegating, + Some("1234567890") + ), + List(DelegationNext( + DelegationStatus.notDelegating, + Some(NextEpoch( + epochStartTime = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"), epochNumber = 10 + )) + )) + ) + + val networkTip = NetworkTip( + epochNumber = 3, + slotNumber = 4, + height = None, + absoluteSlotNumber = Some(10) + ) + + val properWallet = Wallet( + id = "id", + addressPoolGap = 10, + balance = Balance( + available = QuantityUnit(1, Units.lovelace), + reward = QuantityUnit(1, Units.lovelace), + total = QuantityUnit(1, Units.lovelace) + ), + delegation = Some( + Delegation( + active = DelegationActive( + status = DelegationStatus.delegating, + target = Some("1234567890") + ), + next = List( + DelegationNext( + status = DelegationStatus.notDelegating, + changesAt = + Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"), epochNumber = 10)) + ) + ) + ) + ), + name = "name", + passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2000-01-02T10:01:02+01:00")), + state = SyncStatus(SyncState.ready, None), + tip = networkTip + ) + + compareWallets(wallet, properWallet) } "Bad wallet creation" should "be prevented" in { diff --git a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala new file mode 100644 index 0000000..65bd7df --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala @@ -0,0 +1,136 @@ +package iog.psg.cardano.util + +import iog.psg.cardano.CardanoApiCodec._ +import org.scalatest.Assertion +import org.scalatest.matchers.should.Matchers + +trait ModelCompare extends Matchers { + + final def compareInAddress(decoded: InAddress, expected: InAddress): Assertion = { + decoded.address shouldBe expected.address + compareQuantityUnitOpts(decoded.amount, expected.amount) + decoded.id shouldBe expected.id + decoded.index shouldBe expected.index + } + + final def compareOutAddress(decoded: OutAddress, expected: OutAddress): Assertion = { + decoded.address shouldBe expected.address + compareQuantityUnit(decoded.amount, expected.amount) + } + + final def compareInputs(decoded: Seq[InAddress], expected: Seq[InAddress]): Seq[Assertion] = + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareInAddress(decodedAddress, expectedAddress) + } + + final def compareOutputs(decoded: Seq[OutAddress], expected: Seq[OutAddress]): Seq[Assertion] = + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareOutAddress(decodedAddress, expectedAddress) + } + + final def compareFundPaymentsResponse(decoded: FundPaymentsResponse, expected: FundPaymentsResponse): Seq[Assertion] = { + compareInputs(decoded.inputs, expected.inputs) + compareOutputs(decoded.outputs, expected.outputs) + } + + final def compareEstimateFeeResponse(decoded: EstimateFeeResponse, expected: EstimateFeeResponse): Assertion = { + compareQuantityUnit(decoded.estimatedMax, expected.estimatedMax) + compareQuantityUnit(decoded.estimatedMin, expected.estimatedMin) + } + + final def compareStakeAddress(decoded: StakeAddress, expected: StakeAddress): Assertion = { + compareQuantityUnit(decoded.amount, expected.amount) + decoded.stakeAddress shouldBe expected.stakeAddress + } + + final def compareStakeAddresses(decoded: Seq[StakeAddress], expected: Seq[StakeAddress]): Seq[Assertion] = { + decoded.zip(expected).map { + case (decodedAddress, expectedAddress) => compareStakeAddress(decodedAddress, expectedAddress) + } + } + + final def compareTransaction(decoded: CreateTransactionResponse, expected: CreateTransactionResponse): Assertion = { + decoded.id shouldBe expected.id + compareQuantityUnit(decoded.amount, expected.amount) + decoded.insertedAt shouldBe expected.insertedAt + decoded.pendingSince shouldBe expected.pendingSince + decoded.depth shouldBe expected.depth + decoded.direction shouldBe expected.direction + compareInputs(decoded.inputs, expected.inputs) + compareOutputs(decoded.outputs, expected.outputs) + compareStakeAddresses(decoded.withdrawals, expected.withdrawals) + decoded.status shouldBe expected.status + decoded.metadata shouldBe expected.metadata + } + + final def compareAddress(decoded: WalletAddressId, expected: WalletAddressId): Assertion = { + decoded.id shouldBe expected.id + decoded.state shouldBe expected.state + } + + final def compareNetworkInformation(decoded: NetworkInfo, expected: NetworkInfo): Assertion = { + decoded.nextEpoch shouldBe expected.nextEpoch + decoded.nodeTip shouldBe expected.nodeTip + decoded.networkTip shouldBe expected.networkTip + decoded.syncProgress.status.toString shouldBe expected.syncProgress.status.toString + + compareQuantityUnitOpts(decoded.syncProgress.progress, expected.syncProgress.progress) + } + + final def compareQuantityUnitOpts(decoded: Option[QuantityUnit], expected: Option[QuantityUnit]): Assertion = { + if (decoded.isEmpty && expected.isEmpty) assert(true) + else (for { + decodedQU <- decoded + expectedQU <- expected + } yield compareQuantityUnit(decodedQU, expectedQU)).getOrElse(assert(false, "one of units is none")) + } + + final def compareQuantityUnit(decoded: QuantityUnit, expected: QuantityUnit): Assertion = { + decoded.unit.toString shouldBe expected.unit.toString + decoded.quantity shouldBe expected.quantity + } + + final def compareBalance(decoded: Balance, expected: Balance): Assertion = { + decoded.available.quantity shouldBe expected.available.quantity + decoded.available.unit.toString shouldBe expected.available.unit.toString + + decoded.reward.quantity shouldBe expected.reward.quantity + decoded.reward.unit.toString shouldBe expected.reward.unit.toString + + decoded.total.quantity shouldBe expected.total.quantity + decoded.total.unit.toString shouldBe expected.total.unit.toString + } + + final def compareState(decoded: SyncStatus, expected: SyncStatus): Assertion = { + decoded.status.toString shouldBe expected.status.toString + decoded.progress shouldBe expected.progress + } + + final def compareDelegation(decoded: Delegation, expected: Delegation): Seq[Assertion] = { + decoded.active.status.toString shouldBe expected.active.status.toString + decoded.active.target shouldBe expected.active.target + + decoded.next.zip(expected.next).map { + case (decodedNext, expectedNext) => + decodedNext.status.toString shouldBe expectedNext.status.toString + decodedNext.changesAt shouldBe expectedNext.changesAt + } + } + + final def compareDelegationOpts(decoded: Option[Delegation], expected: Option[Delegation]): Seq[Assertion] = { + if (decoded.nonEmpty && expected.nonEmpty) compareDelegation(decoded.get, expected.get) + else Seq(assert(false, "one of delegations is none")) + } + + final def compareWallets(decoded: Wallet, expected: Wallet): Assertion = { + decoded.id shouldBe expected.id + decoded.addressPoolGap shouldBe expected.addressPoolGap + compareBalance(decoded.balance, expected.balance) + compareDelegationOpts(decoded.delegation, expected.delegation) + decoded.name shouldBe expected.name + decoded.passphrase shouldBe expected.passphrase + compareState(decoded.state, expected.state) + decoded.tip shouldBe expected.tip + } + +} From b3f7755d6a9ba26398d0504a89bfeb11f9ddb4cd Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 11:25:34 +0100 Subject: [PATCH 29/39] cmd lines done --- .../iog/psg/cardano/CardanoApiMainSpec.scala | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 1aaf3ab..8d6d198 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -136,6 +136,16 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S assert(results.exists(!_.contains(testWalletId)), "Test wallet found after deletion?") } + "The Cmd Line -restoreWallet" should "restore deleted wallet 2" in { + val cmdLineResults = runCmdLine( + CmdLine.restoreWallet, + CmdLine.passphrase, testWallet2Passphrase, + CmdLine.name, testWallet2Name, + CmdLine.mnemonic, testWallet2Mnemonic) + + assert(cmdLineResults.exists(_.contains(s"Wallet($testWallet2Id"))) + } + "The Cmd Line -listAddresses -walletId [walletId] -state [state]" should "list unused wallet addresses" in { val cmdLineResults = runCmdLine( CmdLine.listWalletAddresses, @@ -143,7 +153,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S CmdLine.walletId, testWalletId) assert(cmdLineResults.exists(_.contains("Some(unused)"))) - cmdLineResults.count(_.contains("Some(used)")) shouldBe 0 + assert(cmdLineResults.exists(!_.contains("Some(used)"))) } it should "list used wallet addresses" in { @@ -192,46 +202,17 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S val postTxTime = ZonedDateTime.now().plusMinutes(5) - def listWalletTxs: Seq[String] = runCmdLine( + val resultsListWalletTxs = runCmdLine( CmdLine.listWalletTransactions, CmdLine.start, preTxTime.toString, CmdLine.`end`, postTxTime.toString, CmdLine.walletId, testWalletId) - val foundTx = listWalletTxs.exists(_.contains(txId)) + val foundTx = resultsListWalletTxs.exists(_.contains(txId)) assert(foundTx, s"Couldn't find txId $txId in transactions ") } - //=--------------> - - - private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) - - private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) - - def getUnusedAddress(walletId: String): String = { - val results = runCmdLine( - CmdLine.listWalletAddresses, - CmdLine.state, "unused", - CmdLine.walletId, walletId) - - - val all = results.last.split(",") - val cleanedUp = all.map(s => { - if (s.indexOf("addr") > 0) - Some(s.substring(s.indexOf("addr"))) - else None - }) collect { - case Some(goodAddr) => goodAddr - } - cleanedUp.head - } - - def extractTxId(toStringCreateTransactionResult: String): String = { - toStringCreateTransactionResult.split(",").head.stripPrefix("CreateTransactionResponse(") - } - - "--help" should "show possible commands" in { + "The Cmd Line --help" should "show possible commands" in { val results = runCmdLine(CmdLine.help) results.mkString("\n") shouldBe @@ -362,5 +343,28 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S |""".stripMargin } + private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) + + private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) + + private def getUnusedAddress(walletId: String): String = { + val results = runCmdLine( + CmdLine.listWalletAddresses, + CmdLine.state, "unused", + CmdLine.walletId, walletId) + + val all = results.last.split(",") + val cleanedUp = all.map(s => { + if (s.indexOf("addr") > 0) + Some(s.substring(s.indexOf("addr"))) + else None + }) collect { + case Some(goodAddr) => goodAddr + } + cleanedUp.head + } + + private def extractTxId(toStringCreateTransactionResult: String): String = + toStringCreateTransactionResult.split(",").head.stripPrefix("CreateTransactionResponse(") } From ff71d1dca60e43ebfb3455fdc8596e30c4c3946d Mon Sep 17 00:00:00 2001 From: Maciej Bak Date: Thu, 1 Oct 2020 11:37:52 +0100 Subject: [PATCH 30/39] last polishing in main spec --- .../scala/iog/psg/cardano/CardanoApiMainSpec.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala index 8d6d198..d813699 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -5,13 +5,19 @@ import java.time.ZonedDateTime import akka.actor.ActorSystem import iog.psg.cardano.CardanoApiMain.CmdLine import iog.psg.cardano.util.{ArgumentParser, Configure, Trace} +import org.scalatest.BeforeAndAfterAll import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures with BeforeAndAfterAll { -class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures { - + override def afterAll(): Unit = { + runCmdLine( + CmdLine.deleteWallet, + CmdLine.walletId, testWallet2Id) + super.afterAll() + } private implicit val system = ActorSystem("SingleRequest") private implicit val context = system.dispatcher @@ -97,8 +103,6 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S CmdLine.name, testWallet2Name, CmdLine.mnemonic, testWallet2Mnemonic) - println(results) - assert(results.last.contains(testWallet2Id), "Test wallet 2 not found.") } From 6505135d5aa6f77f44927a12ccd4eaa88bfc7ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Fri, 2 Oct 2020 13:55:18 +0100 Subject: [PATCH 31/39] Task/psgs 44 sub 56 (#13) it:test --- .github/workflows/test.yml | 2 +- build.sbt | 106 ++++++++---------- .../java/iog/psg/cardano/TestMain.java | 7 +- src/it/resources/application.conf | 12 ++ .../iog/psg/cardano/CardanoApiMainSpec.scala | 2 + .../psg/cardano/CardanoApiTestScript.scala | 2 +- .../iog/psg/cardano/jpi/CardanoJpiSpec.scala | 22 +--- .../iog/psg/cardano/CardanoApiMain.scala | 9 +- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 2 +- 9 files changed, 74 insertions(+), 90 deletions(-) rename src/{test => it}/java/iog/psg/cardano/TestMain.java (96%) create mode 100644 src/it/resources/application.conf rename src/{test => it}/scala/iog/psg/cardano/CardanoApiMainSpec.scala (99%) rename src/{test => it}/scala/iog/psg/cardano/CardanoApiTestScript.scala (97%) rename src/{test => it}/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala (89%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6a0501..c56c479 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v1.4.2 with: java-version: '11.0.8' - - run: sbt coverage test coverageReport + - run: sbt coverage test it:test coverageReport env: BASE_URL: ${{ secrets.BASE_URL }} CARDANO_API_WALLET_1_PASSPHRASE: ${{ secrets.CARDANO_API_WALLET_1_PASSPHRASE }} diff --git a/build.sbt b/build.sbt index 7658164..fbe625b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,39 +1,3 @@ - -name:= "psg-cardano-wallet-api" - -scalaVersion := "2.13.3" - -organization := "solutions.iog" - -homepage := Some(url("https://github.com/input-output-hk/psg-cardano-wallet-api")) -scmInfo := Some(ScmInfo(url("https://github.com/input-output-hk/psg-cardano-wallet-api"), "scm:git@github.com:input-output-hk/psg-cardano-wallet-api.git")) -developers := List( - Developer("mcsherrylabs", "Alan McSherry", "alan.mcsherry@iohk.io", url("https://github.com/mcsherrylabs")), - Developer("maciejbak85", "Maciej Bak", "maciej.bak@iohk.io", url("https://github.com/maciejbak85")) -) -publishMavenStyle := true -licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")) -description := "A java/scala wrapper for the cardano wallet backend API" -usePgpKeyHex("75E12F006A3F08C757EE8343927AE95EEEF4A02F") - - - -publishTo := Some { - // publish to the sonatype repository - val sonaUrl = "https://oss.sonatype.org/" - if (isSnapshot.value) - "snapshots" at sonaUrl + "content/repositories/snapshots" - else - "releases" at sonaUrl + "service/local/staging/deploy/maven2" -} - -credentials += Credentials("Sonatype Nexus Repository Manager", - "oss.sonatype.org", - sys.env.getOrElse("SONA_USER", ""), - sys.env.getOrElse("SONA_PASS", "")) - -dynverSonatypeSnapshots in ThisBuild := true - val akkaVersion = "2.6.8" val akkaHttpVersion = "10.2.0" val akkaHttpCirce = "1.31.0" @@ -41,26 +5,50 @@ val circeVersion = "0.13.0" val scalaTestVersion = "3.1.2" val commonsCodecVersion = "1.15" -/** - * Don't include a logger binding as this is a library for embedding - * http://www.slf4j.org/codes.html#StaticLoggerBinder - */ -libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, - "com.typesafe.akka" %% "akka-stream" % akkaVersion, - "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, - "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, - "com.typesafe.akka" %% "akka-stream" % akkaVersion, - "io.circe" %% "circe-generic-extras" % circeVersion, - "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, - "commons-codec" % "commons-codec" % commonsCodecVersion, - "org.scalatest" %% "scalatest" % scalaTestVersion % Test, -) - - -javacOptions ++= Seq("-source", "1.8", "-target", "1.8") - -scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations") - -parallelExecution in Test := false - +lazy val rootProject = (project in file(".")) + .configs(IntegrationTest) + .settings( + Defaults.itSettings, + IntegrationTest / dependencyClasspath := (IntegrationTest / dependencyClasspath).value ++ (Test / exportedProducts).value, + name:= "psg-cardano-wallet-api", + scalaVersion := "2.13.3", + organization := "solutions.iog", + homepage := Some(url("https://github.com/input-output-hk/psg-cardano-wallet-api")), + scmInfo := Some(ScmInfo(url("https://github.com/input-output-hk/psg-cardano-wallet-api"), "scm:git@github.com:input-output-hk/psg-cardano-wallet-api.git")), + developers := List( + Developer("mcsherrylabs", "Alan McSherry", "alan.mcsherry@iohk.io", url("https://github.com/mcsherrylabs")), + Developer("maciejbak85", "Maciej Bak", "maciej.bak@iohk.io", url("https://github.com/maciejbak85")) + ), + publishMavenStyle := true, + licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")), + description := "A java/scala wrapper for the cardano wallet backend API", + usePgpKeyHex("75E12F006A3F08C757EE8343927AE95EEEF4A02F"), + publishTo := Some { + // publish to the sonatype repository + val sonaUrl = "https://oss.sonatype.org/" + if (isSnapshot.value) + "snapshots" at sonaUrl + "content/repositories/snapshots" + else + "releases" at sonaUrl + "service/local/staging/deploy/maven2" + }, + credentials += Credentials("Sonatype Nexus Repository Manager", + "oss.sonatype.org", + sys.env.getOrElse("SONA_USER", ""), + sys.env.getOrElse("SONA_PASS", "")), + dynverSonatypeSnapshots in ThisBuild := true, + javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), + scalacOptions ++= Seq("-unchecked", "-deprecation", "-Ymacro-annotations"), + parallelExecution in Test := true, + parallelExecution in IntegrationTest := false, + libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "io.circe" %% "circe-generic-extras" % circeVersion, + "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce, + "commons-codec" % "commons-codec" % commonsCodecVersion, + "org.scalatest" %% "scalatest" % scalaTestVersion % "it, test", + ) +) \ No newline at end of file diff --git a/src/test/java/iog/psg/cardano/TestMain.java b/src/it/java/iog/psg/cardano/TestMain.java similarity index 96% rename from src/test/java/iog/psg/cardano/TestMain.java rename to src/it/java/iog/psg/cardano/TestMain.java index c60f8c1..b8b1dce 100644 --- a/src/test/java/iog/psg/cardano/TestMain.java +++ b/src/it/java/iog/psg/cardano/TestMain.java @@ -1,11 +1,14 @@ package iog.psg.cardano; import akka.actor.ActorSystem; -import iog.psg.cardano.jpi.*; import iog.psg.cardano.jpi.CardanoApi; +import iog.psg.cardano.jpi.*; import scala.Enumeration; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; diff --git a/src/it/resources/application.conf b/src/it/resources/application.conf new file mode 100644 index 0000000..8584057 --- /dev/null +++ b/src/it/resources/application.conf @@ -0,0 +1,12 @@ +cardano.wallet.baseUrl=${BASE_URL} +cardano.wallet.passphrase=${CARDANO_API_WALLET_1_PASSPHRASE} +cardano.wallet.name="cardano_api_wallet_1" +cardano.wallet.amount=2000000 +cardano.wallet.mnemonic=${CARDANO_API_WALLET_1_MNEMONIC} +cardano.wallet.id="6cd6d11a489b7ea82a4624d18b93bdf9b77f0620" +cardano.wallet.metadata="0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" + +cardano.wallet2.mnemonic=${CARDANO_API_WALLET_2_MNEMONIC} +cardano.wallet2.id="bfa9530c4ecfee6e5561e950bd7a7a332e4e7497" +cardano.wallet2.name="somethrowawayname" +cardano.wallet2.passphrase="somethrowawayname" \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala similarity index 99% rename from src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala rename to src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala index d813699..ad603fc 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -48,6 +48,8 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S override def close(): Unit = () } + implicit val apiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor + CardanoApiMain.run(arguments) results.reverse diff --git a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala similarity index 97% rename from src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala rename to src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala index 698c4e0..d6359dd 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -5,7 +5,7 @@ import java.util import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps._ import iog.psg.cardano.CardanoApi._ -import iog.psg.cardano.CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueByteArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} +import iog.psg.cardano.CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} import org.apache.commons.codec.binary.Hex import scala.annotation.tailrec diff --git a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala similarity index 89% rename from src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala rename to src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 1b5c40c..5e14413 100644 --- a/src/test/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -42,24 +42,7 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model testWalletPassphrase, mnem.mnemonicSentence.asJava, 10 - ).toCompletableFuture.get(); - - - wallet.id shouldBe "id" - - val delegation = wallet.delegation.getOrElse(fail("Missing delegation")) - val properDelegation = Delegation( - DelegationActive( - DelegationStatus.delegating, - Some("1234567890") - ), - List(DelegationNext( - DelegationStatus.notDelegating, - Some(NextEpoch( - epochStartTime = ZonedDateTime.parse("2000-01-02T10:01:02+01:00"), epochNumber = 10 - )) - )) - ) + ).toCompletableFuture.get() val networkTip = NetworkTip( epochNumber = 3, @@ -106,11 +89,8 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model "Test wallet" should "exist or be created" in { - println(s"WALLET $baseUrl") val aryLen = testWalletMnemonic.split(" ").length val aryLen2 = testWallet2Mnemonic.split(" ").length - println(s"WALLET 1 words ${aryLen} <-") - println(s"WALLET 2 words ${aryLen2} <-") val mnem = GenericMnemonicSentence(testWalletMnemonic) sut diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index d8a37e2..11d5795 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -65,13 +65,13 @@ object CardanoApiMain { } else NoOpTrace ) - run(arguments) + implicit val apiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor + run(arguments) } - private[cardano] def run(arguments: ArgumentParser)(implicit trace: Trace): Unit = { - + private[cardano] def run(arguments: ArgumentParser)(implicit trace: Trace, apiRequestExecutor: ApiRequestExecutor): Unit = { if (arguments.noArgs || arguments.contains(CmdLine.help)) { showHelp() @@ -84,8 +84,7 @@ object CardanoApiMain { } implicit val system: ActorSystem = ActorSystem("SingleRequest") - import system.dispatcher //the - implicit val apiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor + import system.dispatcher Try { diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 4cf45df..0891ec7 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -232,4 +232,4 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { private final lazy val addressIdStr = "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" -} +} \ No newline at end of file From e900d15ca0719cef8231334a2f8a679b1a3ba02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Tue, 6 Oct 2020 12:46:07 +0100 Subject: [PATCH 32/39] Task/psgs 53 (#14) * createRestoreWallet filled docs * Filled up docs * get rid of nullable, use optional * Improved javas specs * estimate fee use metadata * Update CI with envs * Remove nullpointers * Get rid of Optional + val walletId = TestWalletsConfig.walletsMap(num).id + runCmdLine( + CmdLine.deleteWallet, + CmdLine.walletId, walletId) + } super.afterAll() } private implicit val system = ActorSystem("SingleRequest") private implicit val context = system.dispatcher - private val baseUrl = config.getString("cardano.wallet.baseUrl") - private val testWalletName = config.getString("cardano.wallet.name") - private val testWallet2Name = config.getString("cardano.wallet2.name") - private val testWalletMnemonic = config.getString("cardano.wallet.mnemonic") - private val testWallet2Mnemonic = config.getString("cardano.wallet2.mnemonic") - private val testWalletId = config.getString("cardano.wallet.id") - private val testWallet2Id = config.getString("cardano.wallet2.id") - private val testWalletPassphrase = config.getString("cardano.wallet.passphrase") - private val testWallet2Passphrase = config.getString("cardano.wallet2.passphrase") - private val testAmountToTransfer = config.getString("cardano.wallet.amount") - private val testMetadata = config.getString("cardano.wallet.metadata") private val defaultArgs = Array(CmdLine.baseUrl, baseUrl) @@ -60,7 +54,8 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S assert(cmdLineResults.exists(_.contains("ready")), s"Testnet API service not ready - '$baseUrl' \n $cmdLineResults") } - "The Cmd Line -wallets" should "show our test wallet in the list" in { + "The Cmd Line -wallets" should "show our test wallet in the list" in new TestWalletFixture(walletNum = 1) { + val cmdLineResults = runCmdLine( CmdLine.listWallets) @@ -78,19 +73,32 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S } - "The Cmd Line -estimateFee" should "estimate transaction costs" in { + "The Cmd Line -estimateFee" should "estimate transaction costs" in new TestWalletFixture(walletNum = 1){ + val unusedAddr = getUnusedAddressWallet1 + + val cmdLineResults = runCmdLine( + CmdLine.estimateFee, + CmdLine.amount, testAmountToTransfer.get, + CmdLine.address, unusedAddr, + CmdLine.walletId, testWalletId) + + assert(cmdLineResults.exists(_.contains("EstimateFeeResponse(QuantityUnit("))) + } + + it should "estimate transaction costs with metadata" in new TestWalletFixture(walletNum = 1){ val unusedAddr = getUnusedAddressWallet1 val cmdLineResults = runCmdLine( CmdLine.estimateFee, - CmdLine.amount, testAmountToTransfer, + CmdLine.amount, testAmountToTransfer.get, CmdLine.address, unusedAddr, + CmdLine.metadata, testMetadata.get, CmdLine.walletId, testWalletId) assert(cmdLineResults.exists(_.contains("EstimateFeeResponse(QuantityUnit("))) } - "The Cmd Line -wallet [walletId]" should "get our wallet" in { + "The Cmd Line -wallet [walletId]" should "get our wallet" in new TestWalletFixture(walletNum = 1){ val cmdLineResults = runCmdLine( CmdLine.getWallet, CmdLine.walletId, testWalletId) @@ -98,14 +106,25 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S assert(cmdLineResults.exists(_.contains(testWalletId)), "Test wallet not found.") } - "The Cmd Line -createWallet" should "create wallet 2" in { + "The Cmd Line -createWallet" should "create wallet 2" in new TestWalletFixture(walletNum = 2){ val results = runCmdLine( CmdLine.createWallet, - CmdLine.passphrase, testWallet2Passphrase, - CmdLine.name, testWallet2Name, - CmdLine.mnemonic, testWallet2Mnemonic) + CmdLine.passphrase, testWalletPassphrase, + CmdLine.name, testWalletName, + CmdLine.mnemonic, testWalletMnemonic) + + assert(results.last.contains(testWalletId), "Test wallet 2 not found.") + } - assert(results.last.contains(testWallet2Id), "Test wallet 2 not found.") + it should "create wallet with secondary factor" in new TestWalletFixture(walletNum = 3){ + val results = runCmdLine( + CmdLine.createWallet, + CmdLine.passphrase, testWalletPassphrase, + CmdLine.name, testWalletName, + CmdLine.mnemonic, testWalletMnemonic, + CmdLine.mnemonicSecondary, testWalletMnemonicSecondary.get + ) + assert(results.last.contains(testWalletId), "Test wallet 3 not found.") } it should "not create a wallet with a bad mnemonic" in { @@ -118,41 +137,41 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S assert(results.exists(_.contains("Found an unknown word")), "Bad menmonic not stopped") } - "The Cmd Line -updatePassphrase" should "allow password change in test wallet 2" in { + "The Cmd Line -updatePassphrase" should "allow password change in test wallet 2" in new TestWalletFixture(walletNum = 2){ val cmdLineResults = runCmdLine( CmdLine.updatePassphrase, - CmdLine.oldPassphrase, testWallet2Passphrase, - CmdLine.passphrase, testWalletPassphrase, - CmdLine.walletId, testWallet2Id) + CmdLine.oldPassphrase, testWalletPassphrase, + CmdLine.passphrase, testWalletPassphrase.toUpperCase, + CmdLine.walletId, testWalletId) assert(cmdLineResults.exists(_.contains("Unit result from update passphrase"))) } - "The Cmd Line -deleteWallet [walletId]" should "delete test wallet 2" in { + "The Cmd Line -deleteWallet [walletId]" should "delete test wallet 2" in new TestWalletFixture(walletNum = 2){ val cmdLineResults = runCmdLine( CmdLine.deleteWallet, - CmdLine.walletId, testWallet2Id) + CmdLine.walletId, testWalletId) assert(cmdLineResults.exists(_.contains("Unit result from delete wallet"))) val results = runCmdLine( CmdLine.getWallet, - CmdLine.walletId, testWallet2Id) + CmdLine.walletId, testWalletId) assert(results.exists(!_.contains(testWalletId)), "Test wallet found after deletion?") } - "The Cmd Line -restoreWallet" should "restore deleted wallet 2" in { + "The Cmd Line -restoreWallet" should "restore deleted wallet 2" in new TestWalletFixture(walletNum = 2){ val cmdLineResults = runCmdLine( CmdLine.restoreWallet, - CmdLine.passphrase, testWallet2Passphrase, - CmdLine.name, testWallet2Name, - CmdLine.mnemonic, testWallet2Mnemonic) + CmdLine.passphrase, testWalletPassphrase, + CmdLine.name, testWalletName, + CmdLine.mnemonic, testWalletMnemonic) - assert(cmdLineResults.exists(_.contains(s"Wallet($testWallet2Id"))) + assert(cmdLineResults.exists(_.contains(s"Wallet($testWalletId"))) } - "The Cmd Line -listAddresses -walletId [walletId] -state [state]" should "list unused wallet addresses" in { + "The Cmd Line -listAddresses -walletId [walletId] -state [state]" should "list unused wallet addresses" in new TestWalletFixture(walletNum = 1){ val cmdLineResults = runCmdLine( CmdLine.listWalletAddresses, CmdLine.state, "unused", @@ -162,7 +181,7 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S assert(cmdLineResults.exists(!_.contains("Some(used)"))) } - it should "list used wallet addresses" in { + it should "list used wallet addresses" in new TestWalletFixture(walletNum = 1){ val cmdLineResults = runCmdLine( CmdLine.listWalletAddresses, CmdLine.state, "used", @@ -172,10 +191,10 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S cmdLineResults.count(_.contains("Some(unused)")) shouldBe 0 } - "The Cmd Line -fundTx" should "fund payments" in { + "The Cmd Line -fundTx" should "fund payments" in new TestWalletFixture(walletNum = 1){ val cmdLineResults = runCmdLine( CmdLine.fundTx, - CmdLine.amount, testAmountToTransfer, + CmdLine.amount, testAmountToTransfer.get, CmdLine.address, getUnusedAddressWallet2, CmdLine.walletId, testWalletId) @@ -183,15 +202,15 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S cmdLineResults.mkString("").contains("cannot_cover_fee"), s"$cmdLineResults") } - "The Cmd Lines -createTx, -getTx, -listTxs" should "transact from A to B with metadata, txId should be visible in get and list" in { + "The Cmd Lines -createTx, -getTx, -listTxs" should "transact from A to B with metadata, txId should be visible in get and list" in new TestWalletFixture(walletNum = 1){ val unusedAddr = getUnusedAddressWallet1 val preTxTime = ZonedDateTime.now().minusMinutes(1) val resultsCreateTx = runCmdLine( CmdLine.createTx, CmdLine.passphrase, testWalletPassphrase, - CmdLine.amount, testAmountToTransfer, - CmdLine.metadata, testMetadata, + CmdLine.amount, testAmountToTransfer.get, + CmdLine.metadata, testMetadata.get, CmdLine.address, unusedAddr, CmdLine.walletId, testWalletId) @@ -349,9 +368,9 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S |""".stripMargin } - private def getUnusedAddressWallet2 = getUnusedAddress(testWallet2Id) + private def getUnusedAddressWallet2 = getUnusedAddress(TestWalletsConfig.walletsMap(2).id) - private def getUnusedAddressWallet1 = getUnusedAddress(testWalletId) + private def getUnusedAddressWallet1 = getUnusedAddress(TestWalletsConfig.walletsMap(1).id) private def getUnusedAddress(walletId: String): String = { val results = runCmdLine( diff --git a/src/it/scala/iog/psg/cardano/TestWalletsConfig.scala b/src/it/scala/iog/psg/cardano/TestWalletsConfig.scala new file mode 100644 index 0000000..49b1da6 --- /dev/null +++ b/src/it/scala/iog/psg/cardano/TestWalletsConfig.scala @@ -0,0 +1,44 @@ +package iog.psg.cardano + +import iog.psg.cardano.util.Configure + +final case class WalletConfig( + id: String, + name: String, + passphrase: String, + mnemonic: String, + mnemonicSecondary: Option[String], + amount: Option[String], + metadata: Option[String] +) + +object TestWalletsConfig extends Configure { + + lazy val baseUrl = config.getString("cardano.wallet.baseUrl") + lazy val walletsMap = (1 to 3).map { num => + num -> loadWallet(num) + }.toMap + + private def loadWallet(num: Int) = { + val name = config.getString(s"cardano.wallet$num.name") + val mnemonic = config.getString(s"cardano.wallet$num.mnemonic") + val mnemonicSecondary = + if (config.hasPath(s"cardano.wallet$num.mnemonicsecondary")) + Some(config.getString(s"cardano.wallet$num.mnemonicsecondary")) + else None + val id = config.getString(s"cardano.wallet$num.id") + val passphrase = config.getString(s"cardano.wallet$num.passphrase") + + val amount = + if (config.hasPath(s"cardano.wallet$num.amount")) + Some(config.getString(s"cardano.wallet$num.amount")) + else None + + val metadata = + if (config.hasPath(s"cardano.wallet$num.metadata")) + Some(config.getString(s"cardano.wallet$num.metadata")) + else None + + WalletConfig(id, name, passphrase, mnemonic, mnemonicSecondary, amount, metadata) + } +} diff --git a/src/it/scala/iog/psg/cardano/common/TestWalletFixture.scala b/src/it/scala/iog/psg/cardano/common/TestWalletFixture.scala new file mode 100644 index 0000000..8b64b5c --- /dev/null +++ b/src/it/scala/iog/psg/cardano/common/TestWalletFixture.scala @@ -0,0 +1,15 @@ +package iog.psg.cardano.common + +import iog.psg.cardano.{TestWalletsConfig, WalletConfig} + +abstract class TestWalletFixture(walletNum: Int) { + val wallet: WalletConfig = TestWalletsConfig.walletsMap(walletNum) + + val testWalletName = wallet.name + val testWalletId = wallet.id + val testWalletPassphrase = wallet.passphrase + val testWalletMnemonic = wallet.mnemonic + val testWalletMnemonicSecondary = wallet.mnemonicSecondary + val testAmountToTransfer = wallet.amount + val testMetadata = wallet.metadata +} diff --git a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala index 5e14413..4407204 100644 --- a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala @@ -4,25 +4,25 @@ import java.time.ZonedDateTime import java.util.concurrent.TimeUnit import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.TestWalletsConfig +import iog.psg.cardano.TestWalletsConfig.baseUrl +import iog.psg.cardano.common.TestWalletFixture import iog.psg.cardano.util.{Configure, ModelCompare} +import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} +import scala.jdk.OptionConverters.RichOption -class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare { +class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare with BeforeAndAfterAll { + + override def afterAll(): Unit = { + sut.deleteWallet(TestWalletsConfig.walletsMap(3).id) + super.afterAll() + } - private val baseUrl = config.getString("cardano.wallet.baseUrl") - private val testWalletName = config.getString("cardano.wallet.name") - private val testWallet2Name = config.getString("cardano.wallet2.name") - private val testWalletMnemonic = config.getString("cardano.wallet.mnemonic") - private val testWallet2Mnemonic = config.getString("cardano.wallet2.mnemonic") - private val testWalletId = config.getString("cardano.wallet.id") - private val testWallet2Id = config.getString("cardano.wallet2.id") - private val testWalletPassphrase = config.getString("cardano.wallet.passphrase") - private val testWallet2Passphrase = config.getString("cardano.wallet2.passphrase") - private val testAmountToTransfer = config.getString("cardano.wallet.amount") private val timeoutValue: Long = 10 private val timeoutUnits = TimeUnit.SECONDS private lazy val sut = new JpiResponseCheck(new CardanoApiFixture(baseUrl).getJpi, timeoutValue, timeoutUnits) @@ -33,10 +33,10 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model networkState shouldBe "ready" } - "Jpi CardanoAPI" should "allow override of execute" in { + "Jpi CardanoAPI" should "allow override of execute" in new TestWalletFixture(1) { val api = JpiResponseCheck.buildWithDummyApiExecutor() val mnem = GenericMnemonicSentence(testWalletMnemonic) - val wallet = api + val createdWallet = api .createRestore( testWalletName, testWalletPassphrase, @@ -51,7 +51,7 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model absoluteSlotNumber = Some(10) ) - val properWallet = Wallet( + val expectedWallet = Wallet( id = "id", addressPoolGap = 10, balance = Balance( @@ -80,17 +80,17 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model tip = networkTip ) - compareWallets(wallet, properWallet) + compareWallets(createdWallet, expectedWallet) } "Bad wallet creation" should "be prevented" in { an[IllegalArgumentException] shouldBe thrownBy(sut.createBadWallet()) } - "Test wallet" should "exist or be created" in { + "Test wallet" should "exist or be created" in new TestWalletFixture(1) { val aryLen = testWalletMnemonic.split(" ").length - val aryLen2 = testWallet2Mnemonic.split(" ").length + val aryLen2 = TestWalletsConfig.walletsMap(2).mnemonic.split(" ").length val mnem = GenericMnemonicSentence(testWalletMnemonic) sut @@ -101,34 +101,48 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model mnem.mnemonicSentence.asJava, 10) shouldBe true } - it should "get our wallet" in { + it should "get our wallet" in new TestWalletFixture(1){ sut.getWallet(testWalletId) shouldBe true } - it should "create r find wallet 2" in { - val mnem = GenericMnemonicSentence(testWallet2Mnemonic) + it should "create r find wallet 2" in new TestWalletFixture(2){ + val mnem = GenericMnemonicSentence(testWalletMnemonic) sut .findOrCreateTestWallet( - testWallet2Id, - testWallet2Name, - testWallet2Passphrase, + testWalletId, + testWalletName, + testWalletPassphrase, mnem.mnemonicSentence.asJava, 10) shouldBe true } - it should "allow password change in wallet 2" in { - sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase) + it should "allow password change in wallet 2" in new TestWalletFixture(2) { + sut.passwordChange(testWalletId, testWalletPassphrase, testWalletPassphrase) //now this is the wrong password - an[Exception] shouldBe thrownBy(sut.passwordChange(testWallet2Id, testWallet2Passphrase, testWalletPassphrase)) + an[Exception] shouldBe thrownBy(sut.passwordChange(testWalletId, testWalletPassphrase.toUpperCase(), testWalletPassphrase)) - sut.passwordChange(testWallet2Id, testWalletPassphrase, testWallet2Passphrase) + sut.passwordChange(testWalletId, testWalletPassphrase, testWalletPassphrase.toUpperCase()) } - it should "fund payments" in { - val response = sut.fundPayments(testWalletId, testAmountToTransfer.toInt) + it should "create wallet with secondary factor" in new TestWalletFixture(3) { + val mnem = GenericMnemonicSentence(testWalletMnemonic) + val mnemSecondary = GenericMnemonicSecondaryFactor(testWalletMnemonicSecondary.get) + + val createdWallet = sut.createTestWallet( + testWalletName, + testWalletPassphrase, + mnem.mnemonicSentence.asJava, + mnemSecondary.mnemonicSentence.asJava, + 10) + + createdWallet.id shouldBe testWalletId + } + + it should "fund payments" in new TestWalletFixture(1) { + val response = sut.fundPayments(testWalletId, testAmountToTransfer.get.toInt) } - it should "transact from a to a with metadata" in { + it should "transact from a to a with metadata" in new TestWalletFixture(1) { val metadata: Map[String, String] = Map( Long.box(Long.MaxValue).toString -> "0" * 64, @@ -136,7 +150,7 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model ) val createTxResponse = - sut.paymentToSelf(testWalletId, testWalletPassphrase, testAmountToTransfer.toInt, metadata.asJava) + sut.paymentToSelf(testWalletId, testWalletPassphrase, testAmountToTransfer.get.toInt, metadata.asJava) val id = createTxResponse.id val getTxResponse = sut.getTx(testWalletId, createTxResponse.id) @@ -148,9 +162,10 @@ class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with Model } - it should "delete wallet 2" in { - sut.deleteWallet(testWallet2Id) - an[Exception] shouldBe thrownBy(sut.getWallet(testWallet2Id), "Wallet should not be retrieved") + it should "delete wallet 2" in new TestWalletFixture(2){ + sut.deleteWallet(testWalletId) + an[Exception] shouldBe thrownBy(sut.getWallet(testWalletId), "Wallet should not be retrieved") } + } diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java index edb0813..6fab74b 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java @@ -21,6 +21,12 @@ private CardanoApi() { api = null; } + /** + * CardanoApi constructor + * + * @param api iog.psg.cardano.CardanoApi instance + * @param helpExecute og.psg.cardano.jpi.HelpExecute instance + */ public CardanoApi(iog.psg.cardano.CardanoApi api, HelpExecute helpExecute) { this.helpExecute = helpExecute; this.api = api; @@ -28,17 +34,71 @@ public CardanoApi(iog.psg.cardano.CardanoApi api, HelpExecute helpExecute) { Objects.requireNonNull(helpExecute, "HelpExecute cannot be null"); } + /** + * Create and restore a wallet from a mnemonic sentence or account public key. + * Api Url: #postWallet + * + * @param name wallet's name + * @param passphrase A master passphrase to lock and protect the wallet for sensitive operation (e.g. sending funds) + * @param mnemonicWordList A list of mnemonic words [ 15 .. 24 ] items ( can be generated using https://iancoleman.io/bip39> ) + * @param addressPoolGap An optional number of consecutive unused addresses allowed + * @return Created wallet + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage createRestore( String name, String passphrase, List mnemonicWordList, int addressPoolGap) throws CardanoApiException { + return createRestore(name, passphrase, mnemonicWordList, null, addressPoolGap); + } + + /** + * Create and restore a wallet from a mnemonic sentence or account public key. + * Api Url: #postWallet + * + * @param name wallet's name + * @param passphrase A master passphrase to lock and protect the wallet for sensitive operation (e.g. sending funds) + * @param mnemonicWordList A list of mnemonic words [ 15 .. 24 ] items ( can be generated using https://iancoleman.io/bip39> ) + * @param mnemonicSecondFactor A passphrase used to encrypt the mnemonic sentence. [ 9 .. 12 ] items + * @param addressPoolGap An optional number of consecutive unused addresses allowed + * @return Created wallet + * @throws CardanoApiException thrown on API error response, contains error message and code from API + * + */ + public CompletionStage createRestore( + String name, + String passphrase, + List mnemonicWordList, + List mnemonicSecondFactor, + int addressPoolGap) throws CardanoApiException { CardanoApiCodec.MnemonicSentence mnem = createMnemonic(mnemonicWordList); + + Optional mnemonicSecondaryFactorOpt = Optional.empty(); + if (mnemonicSecondFactor != null) { + CardanoApiCodec.MnemonicSentence mnemonicSentence = createMnemonicSecondary(mnemonicSecondFactor); + mnemonicSecondaryFactorOpt = Optional.of(mnemonicSentence); + } + return helpExecute.execute( - api.createRestoreWallet(name, passphrase, mnem, option(addressPoolGap)) + api.createRestoreWallet(name, passphrase, mnem, option(mnemonicSecondaryFactorOpt), option(addressPoolGap)) ); } + /** + * Create and send transaction from the wallet. + * Api Url: #postTransaction + * + * @param fromWalletId wallet's id + * @param passphrase The wallet's master passphrase. [ 0 .. 255 ] characters + * @param payments A list of target outputs ( address, amount ) + * @param withdrawal nullable, when provided, instruments the server to automatically withdraw rewards from the source + * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much + * as they cost). + * @param metadata Extra application data attached to the transaction. + * @return created transaction + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage createTransaction( String fromWalletId, String passphrase, @@ -53,6 +113,16 @@ public CompletionStage createTransact option(withdrawal))); } + /** + * Create and send transaction from the wallet. + * Api Url: #postTransaction + * + * @param fromWalletId wallet's id + * @param passphrase The wallet's master passphrase. [ 0 .. 255 ] characters + * @param payments A list of target outputs ( address, amount ) + * @return created transaction + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage createTransaction( String fromWalletId, String passphrase, @@ -62,6 +132,14 @@ public CompletionStage createTransact return createTransaction(fromWalletId, passphrase, payments, null, "self"); } + /** + * Get wallet details by id + * Api Url: #getWallet + * + * @param fromWalletId wallet's id + * @return wallet + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage getWallet( String fromWalletId) throws CardanoApiException { @@ -69,6 +147,14 @@ public CompletionStage getWallet( api.getWallet(fromWalletId)); } + /** + * Delete wallet by id + * Api Url: #deleteWallet + * + * @param fromWalletId wallet's id + * @return void + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage deleteWallet( String fromWalletId) throws CardanoApiException { @@ -76,6 +162,14 @@ public CompletionStage deleteWallet( api.deleteWallet(fromWalletId)).thenApply(x -> null); } + /** + * Get transaction by id. + * Api Url: #getTransaction + * + * @param walletId wallet's id + * @param transactionId transaction's id + * @return get transaction request + */ public CompletionStage getTransaction( String walletId, String transactionId) throws CardanoApiException { @@ -83,19 +177,75 @@ public CompletionStage getTransaction api.getTransaction(walletId, transactionId)); } + /** + * Estimate fee for the transaction. The estimate is made by assembling multiple transactions and analyzing the + * distribution of their fees. The estimated_max is the highest fee observed, and the estimated_min is the fee which + * is lower than at least 90% of the fees observed. + * Api Url: #estimateFee + * + * @param walletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @return estimatedfee response + */ public CompletionStage estimateFee( String walletId, List payments) throws CardanoApiException { return estimateFee(walletId, payments, "self"); } + /** + * Estimate fee for the transaction. The estimate is made by assembling multiple transactions and analyzing the + * distribution of their fees. The estimated_max is the highest fee observed, and the estimated_min is the fee which + * is lower than at least 90% of the fees observed. + * Api Url: #estimateFee + * + * @param walletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @param withdrawal nullable, when provided, instruments the server to automatically withdraw rewards from the source + * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much + * as they cost). + * @return estimatedfee response + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage estimateFee( String walletId, List payments, String withdrawal) throws CardanoApiException { return helpExecute.execute( api.estimateFee(walletId, new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), - withdrawal)); + withdrawal, option(Optional.empty()))); + } + + /** + * Estimate fee for the transaction. The estimate is made by assembling multiple transactions and analyzing the + * distribution of their fees. The estimated_max is the highest fee observed, and the estimated_min is the fee which + * is lower than at least 90% of the fees observed. + * Api Url: #estimateFee + * + * @param walletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @param withdrawal nullable, when provided, instruments the server to automatically withdraw rewards from the source + * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much + * as they cost). + * @param metadata Extra application data attached to the transaction. + * @return estimated fee response + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ + public CompletionStage estimateFee( + String walletId, List payments, String withdrawal, CardanoApiCodec.TxMetadataIn metadata) throws CardanoApiException { + return helpExecute.execute( + api.estimateFee(walletId, + new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), + withdrawal, option(Optional.of(metadata)))); } + /** + * Select coins to cover the given set of payments. + * Api Url: #CoinSelections + * + * @param walletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @return fund payments + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage fundPayments( String walletId, List payments) throws CardanoApiException { return helpExecute.execute( @@ -103,6 +253,15 @@ public CompletionStage fundPayments( new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()))); } + /** + * list of known addresses, ordered from newest to oldest + * Api Url: #Addresses + * + * @param walletId wallet's id + * @param addressFilter addresses state: used, unused + * @return list of wallet's addresses + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage> listAddresses( String walletId, AddressFilter addressFilter) throws CardanoApiException { Enumeration.Value v = CardanoApiCodec.AddressFilter$.MODULE$.Value(addressFilter.name().toLowerCase()); @@ -110,13 +269,28 @@ public CompletionStage> listAddresses( api.listAddresses(walletId, scala.Option.apply(v))).thenApply(CollectionConverters::asJava); } + /** + * list of known addresses, ordered from newest to oldest + * Api Url: #Addresses + * + * @param walletId wallet's id + * @return list of wallet's addresses + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage> listAddresses( String walletId) throws CardanoApiException { return helpExecute.execute( api.listAddresses(walletId, scala.Option.empty())).thenApply(CollectionConverters::asJava); } - + /** + * Lists all incoming and outgoing wallet's transactions. + * Api Url: #listTransactions + * + * @param builder ListTransactionsParamBuilder + * @return list of wallet's transactions + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage> listTransactions( ListTransactionsParamBuilder builder) throws CardanoApiException { return helpExecute.execute( @@ -129,13 +303,28 @@ public CompletionStage> listTran .thenApply(CollectionConverters::asJava); } - + /** + * list of known wallets, ordered from oldest to newest. + * Api Url: #listWallets + * + * @return wallets's list + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage> listWallets() throws CardanoApiException { return helpExecute.execute( api.listWallets()) .thenApply(CollectionConverters::asJava); } + /** + * Update Passphrase + * Api Url: #putWalletPassphrase + * @param walletId wallet's id + * @param oldPassphrase current passphrase + * @param newPassphrase new passphrase + * @return void + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage updatePassphrase( String walletId, String oldPassphrase, @@ -144,11 +333,17 @@ public CompletionStage updatePassphrase( return helpExecute.execute(api.updatePassphrase(walletId, oldPassphrase, newPassphrase)).thenApply(x -> null); } + /** + * Gives network information + * Api Url: #getNetworkInformation + * + * @return network info + * @throws CardanoApiException thrown on API error response, contains error message and code from API + */ public CompletionStage networkInfo() throws CardanoApiException { return helpExecute.execute(api.networkInfo()); } - private static scala.Option option(final T value) { return (value != null) ? new Some(value) : scala.Option.apply((T) null); } @@ -163,4 +358,10 @@ private static CardanoApiCodec.GenericMnemonicSentence createMnemonic(List wordList) { + return new CardanoApiCodec.GenericMnemonicSecondaryFactor( + CollectionConverters.asScala(wordList).toIndexedSeq() + ); + } + } diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 14dd417..5bfaa61 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -8,10 +8,12 @@ import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.Uri.Query import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.Unmarshal import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import io.circe.generic.auto._ import io.circe.generic.extras.Configuration import iog.psg.cardano.CardanoApi.Order.Order + import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} import scala.concurrent.{Await, ExecutionContext, Future} @@ -83,6 +85,12 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames + /** + * List of known wallets, ordered from oldest to newest. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listWallets #listWallets]] + * + * @return list wallets request + */ def listWallets: CardanoApiRequest[Seq[Wallet]] = CardanoApiRequest( HttpRequest( uri = wallets, @@ -91,14 +99,27 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act _.toWallets ) + /** + * Get wallet details by id + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getWallet #getWallet]] + * + * @param walletId wallet's id + * @return get wallet request + */ def getWallet(walletId: String): CardanoApiRequest[Wallet] = CardanoApiRequest( HttpRequest( - uri = s"${wallets}/$walletId", + uri = s"$wallets/$walletId", method = GET ), _.toWallet ) + /** + * Gives network information + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getNetworkInformation #getNetworkInformation]] + * + * @return network info request + */ def networkInfo: CardanoApiRequest[NetworkInfo] = CardanoApiRequest( HttpRequest( uri = s"${network}/information", @@ -107,12 +128,23 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act _.toNetworkInfoResponse ) + /** + * Create and restore a wallet from a mnemonic sentence or account public key. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet #postWallet]] + * + * @param name wallet's name + * @param passphrase A master passphrase to lock and protect the wallet for sensitive operation (e.g. sending funds) + * @param mnemonicSentence A list of mnemonic words [ 15 .. 24 ] items ( can be generated using https://iancoleman.io/bip39 ) + * @param mnemonicSecondFactor An optional passphrase used to encrypt the mnemonic sentence. [ 9 .. 12 ] items + * @param addressPoolGap An optional number of consecutive unused addresses allowed + * @return create/restore wallet request + */ def createRestoreWallet( name: String, passphrase: String, mnemonicSentence: MnemonicSentence, + mnemonicSecondFactor: Option[MnemonicSentence] = None, addressPoolGap: Option[Int] = None - ): Future[CardanoApiRequest[Wallet]] = { val createRestore = @@ -120,6 +152,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act name, passphrase, mnemonicSentence.mnemonicSentence, + mnemonicSecondFactor.map(_.mnemonicSentence), addressPoolGap ) @@ -136,10 +169,18 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } + /** + * List of known addresses, ordered from newest to oldest + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#tag/Addresses #Addresses]] + * + * @param walletId wallet's id + * @param state addresses state: used, unused + * @return list wallet addresses request + */ def listAddresses(walletId: String, state: Option[AddressFilter]): CardanoApiRequest[Seq[WalletAddressId]] = { - val baseUri = Uri(s"${wallets}/${walletId}/addresses") + val baseUri = Uri(s"$wallets/${walletId}/addresses") val url = state.map { s => baseUri.withQuery(Query("state" -> s.toString)) @@ -155,12 +196,28 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } + /** + * Lists all incoming and outgoing wallet's transactions. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listTransactions #listTransactions]] + * + * @param walletId wallet's id + * @param start An optional start time in ISO 8601 date-and-time format. Basic and extended formats are both accepted. Times can be local (with a timezone offset) or UTC. + * If both a start time and an end time are specified, then the start time must not be later than the end time. + * Example: 2008-08-08T08:08:08Z + * @param end An optional end time in ISO 8601 date-and-time format. Basic and extended formats are both accepted. Times can be local (with a timezone offset) or UTC. + * If both a start time and an end time are specified, then the start time must not be later than the end time. + * Example: 2008-08-08T08:08:08Z + * @param order Default: "descending" ( "ascending", "descending" ) + * @param minWithdrawal Returns only transactions that have at least one withdrawal above the given amount. + * This is particularly useful when set to 1 in order to list the withdrawal history of a wallet. + * @return list wallet's transactions request + */ def listTransactions(walletId: String, start: Option[ZonedDateTime] = None, end: Option[ZonedDateTime] = None, order: Order = Order.descendingOrder, minWithdrawal: Option[Int] = None): CardanoApiRequest[Seq[CreateTransactionResponse]] = { - val baseUri = Uri(s"${wallets}/${walletId}/transactions") + val baseUri = Uri(s"$wallets/${walletId}/transactions") val queries = Seq("start", "end", "order", "minWithdrawal").zip(Seq(start, end, order, minWithdrawal)) @@ -182,7 +239,19 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act ) } - + /** + * Create and send transaction from the wallet. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postTransaction #postTransaction]] + * + * @param fromWalletId wallet's id + * @param passphrase The wallet's master passphrase. [ 0 .. 255 ] characters + * @param payments A list of target outputs ( address, amount ) + * @param withdrawal Optional, when provided, instruments the server to automatically withdraw rewards from the source + * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much + * as they cost). + * @param metadata Extra application data attached to the transaction. + * @return create transaction request + */ def createTransaction(fromWalletId: String, passphrase: String, payments: Payments, @@ -196,7 +265,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act Marshal(createTx).to[RequestEntity] map { marshalled => CardanoApiRequest( HttpRequest( - uri = s"${wallets}/${fromWalletId}/transactions", + uri = s"$wallets/$fromWalletId/transactions", method = POST, entity = marshalled ), @@ -205,17 +274,32 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } + /** + * Estimate fee for the transaction. The estimate is made by assembling multiple transactions and analyzing the + * distribution of their fees. The estimated_max is the highest fee observed, and the estimated_min is the fee which + * is lower than at least 90% of the fees observed. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postTransactionFee #estimateFee]] + * + * @param fromWalletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @param withdrawal Optional, when provided, instruments the server to automatically withdraw rewards from the source + * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much + * as they cost). + * @param metadataIn Extra application data attached to the transaction. + * @return estimate fee request + */ def estimateFee(fromWalletId: String, payments: Payments, - withdrawal: String = "self" + withdrawal: String = "self", + metadataIn: Option[TxMetadataIn] = None ): Future[CardanoApiRequest[EstimateFeeResponse]] = { - val estimateFees = EstimateFee(payments.payments, withdrawal) + val estimateFees = EstimateFee(payments.payments, withdrawal, metadataIn) Marshal(estimateFees).to[RequestEntity] map { marshalled => CardanoApiRequest( HttpRequest( - uri = s"${wallets}/${fromWalletId}/payment-fees", + uri = s"$wallets/$fromWalletId/payment-fees", method = POST, entity = marshalled ), @@ -224,12 +308,20 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } + /** + * Select coins to cover the given set of payments. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#tag/Coin-Selections #CoinSelections]] + * + * @param walletId wallet's id + * @param payments A list of target outputs ( address, amount ) + * @return fund payments request + */ def fundPayments(walletId: String, payments: Payments): Future[CardanoApiRequest[FundPaymentsResponse]] = { Marshal(payments).to[RequestEntity] map { marshalled => CardanoApiRequest( HttpRequest( - uri = s"${wallets}/${walletId}/coin-selections/random", + uri = s"$wallets/${walletId}/coin-selections/random", method = POST, entity = marshalled ), @@ -238,11 +330,19 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } + /** + * Get transaction by id. + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/getTransaction #getTransaction]] + * + * @param walletId wallet's id + * @param transactionId transaction's id + * @return get transaction request + */ def getTransaction[T <: TxMetadataIn]( walletId: String, transactionId: String): CardanoApiRequest[CreateTransactionResponse] = { - val uri = Uri(s"${wallets}/${walletId}/transactions/${transactionId}") + val uri = Uri(s"$wallets/${walletId}/transactions/${transactionId}") CardanoApiRequest( HttpRequest( @@ -253,12 +353,20 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act ) } + /** + * Update Passphrase + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/putWalletPassphrase #putWalletPassphrase]] + * @param walletId wallet's id + * @param oldPassphrase current passphrase + * @param newPassphrase new passphrase + * @return update passphrase request + */ def updatePassphrase( walletId: String, oldPassphrase: String, newPassphrase: String): Future[CardanoApiRequest[Unit]] = { - val uri = Uri(s"${wallets}/${walletId}/passphrase") + val uri = Uri(s"$wallets/${walletId}/passphrase") val updater = UpdatePassphrase(oldPassphrase, newPassphrase) Marshal(updater).to[RequestEntity] map { marshalled => { @@ -274,11 +382,17 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act } } + /** + * Delete wallet by id + * Api Url: [[https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/deleteWallet #deleteWallet]] + * @param walletId wallet's id + * @return delete wallet request + */ def deleteWallet( walletId: String ): CardanoApiRequest[Unit] = { - val uri = Uri(s"${wallets}/${walletId}") + val uri = Uri(s"$wallets/${walletId}") CardanoApiRequest( HttpRequest( diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 3bb8e83..a4db5be 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -162,7 +162,7 @@ object CardanoApiCodec { metadata: Option[TxMetadataIn], withdrawal: Option[String]) - private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String) + private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String, metadata: Option[TxMetadataIn]) case class Payments(payments: Seq[Payment]) @@ -186,6 +186,17 @@ object CardanoApiCodec { GenericMnemonicSentence(mnemonicString.split(" ").toIndexedSeq) } + final case class GenericMnemonicSecondaryFactor(mnemonicSentence: IndexedSeq[String]) extends MnemonicSentence { + require( + mnemonicSentence.length == 9 || + mnemonicSentence.length == 12, s"Mnemonic word list must be 9, 12 long (not ${mnemonicSentence.length})") + } + + object GenericMnemonicSecondaryFactor { + def apply(mnemonicSentence: String): GenericMnemonicSecondaryFactor = + GenericMnemonicSecondaryFactor(mnemonicSentence.split(" ").toIndexedSeq) + } + @ConfiguredJsonCodec case class NextEpoch(epochStartTime: ZonedDateTime, epochNumber: Long) @@ -202,12 +213,18 @@ object CardanoApiCodec { name: String, passphrase: String, mnemonicSentence: IndexedSeq[String], + mnemonicSecondFactor: Option[IndexedSeq[String]] = None, addressPoolGap: Option[Int] = None ) { require( mnemonicSentence.length == 15 || mnemonicSentence.length == 21 || mnemonicSentence.length == 24, s"Mnemonic word list must be 15, 21, or 24 long (not ${mnemonicSentence.length})") + + private lazy val mnemonicSecondFactorLength = mnemonicSecondFactor.map(_.length).getOrElse(0) + require( + mnemonicSecondFactor.isEmpty || (mnemonicSecondFactorLength == 9 || mnemonicSecondFactorLength == 12) + ) } object Units extends Enumeration { diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 11d5795..66b8b29 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -6,7 +6,7 @@ import java.time.ZonedDateTime import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, Order, defaultMaxWaitTime} -import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} +import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSecondaryFactor, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap import iog.psg.cardano.util._ @@ -33,6 +33,7 @@ object CardanoApiMain { val passphrase = "-passphrase" val metadata = "-metadata" val mnemonic = "-mnemonic" + val mnemonicSecondary = "-mnemonicSecondary" val addressPoolGap = "-addressPoolGap" val listWalletAddresses = "-listAddresses" val listWalletTransactions = "-listTxs" @@ -196,12 +197,14 @@ object CardanoApiMain { val name = arguments.get(CmdLine.name) val passphrase = arguments.get(CmdLine.passphrase) val mnemonic = arguments.get(CmdLine.mnemonic) + val mnemonicSecondaryOpt = arguments(CmdLine.mnemonicSecondary) val addressPoolGap = arguments(CmdLine.addressPoolGap).map(_.toInt) val result = unwrap(api.createRestoreWallet( name, passphrase, GenericMnemonicSentence(mnemonic), + mnemonicSecondaryOpt.map(m => GenericMnemonicSecondaryFactor(m)), addressPoolGap ).executeBlocking) diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index 5274cf4..f74ea24 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -5,7 +5,6 @@ import scala.Option; import scala.jdk.CollectionConverters; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -20,7 +19,6 @@ private JpiResponseCheck() { jpi = null; timeout = 0; timeoutUnit = null; - } public JpiResponseCheck(CardanoApi jpi, long timeout, TimeUnit timeoutUnit) { @@ -35,7 +33,7 @@ static String get(CardanoApiCodec.NetworkInfo info) { public void createBadWallet() throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { List mnem = Arrays.asList("", "sdfa", "dfd"); - jpi.createRestore("some name", "password99", mnem, 4).toCompletableFuture().get(timeout, timeoutUnit); + jpi.createRestore("some name", "password99", mnem,4).toCompletableFuture().get(timeout, timeoutUnit); } public boolean findOrCreateTestWallet(String ourWalletId, String ourWalletName, String walletPassphrase, List wordList, int addressPoolGap) throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { @@ -45,10 +43,21 @@ public boolean findOrCreateTestWallet(String ourWalletId, String ourWalletName, return true; } } - CardanoApiCodec.Wallet created = jpi.createRestore(ourWalletName, walletPassphrase, wordList,addressPoolGap).toCompletableFuture().get(timeout, timeoutUnit); + + CardanoApiCodec.Wallet created = createTestWallet(ourWalletName, walletPassphrase, wordList, addressPoolGap); return created.id().contentEquals(ourWalletId); } + public CardanoApiCodec.Wallet createTestWallet(String ourWalletName, String walletPassphrase, List wordList, int addressPoolGap) throws CardanoApiException, InterruptedException, ExecutionException, TimeoutException { + CardanoApiCodec.Wallet wallet = jpi.createRestore(ourWalletName, walletPassphrase, wordList, addressPoolGap).toCompletableFuture().get(timeout, timeoutUnit); + return wallet; + } + + public CardanoApiCodec.Wallet createTestWallet(String ourWalletName, String walletPassphrase, List wordList, List mnemSecondaryWordList, int addressPoolGap) throws CardanoApiException, InterruptedException, ExecutionException, TimeoutException { + CardanoApiCodec.Wallet wallet = jpi.createRestore(ourWalletName, walletPassphrase, wordList, mnemSecondaryWordList, addressPoolGap).toCompletableFuture().get(timeout, timeoutUnit); + return wallet; + } + public boolean getWallet(String walletId) throws CardanoApiException, InterruptedException, TimeoutException, ExecutionException { CardanoApiCodec.Wallet w = jpi.getWallet(walletId).toCompletableFuture().get(timeout, timeoutUnit); return w.id().contentEquals(walletId); From 1db6ba4c38b673be637e85cea967ac633ff22f4d Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Tue, 6 Oct 2020 13:28:20 +0100 Subject: [PATCH 33/39] Make withdrawal an option in scala api, remove one estimateFee call. (#16) --- .../java/iog/psg/cardano/jpi/CardanoApi.java | 36 +++++-------------- .../scala/iog/psg/cardano/CardanoApi.scala | 2 +- .../iog/psg/cardano/CardanoApiCodec.scala | 2 +- .../iog/psg/cardano/CardanoApiMain.scala | 2 +- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java index 6fab74b..d9f6b65 100644 --- a/src/main/java/iog/psg/cardano/jpi/CardanoApi.java +++ b/src/main/java/iog/psg/cardano/jpi/CardanoApi.java @@ -189,29 +189,7 @@ public CompletionStage getTransaction */ public CompletionStage estimateFee( String walletId, List payments) throws CardanoApiException { - return estimateFee(walletId, payments, "self"); - } - - /** - * Estimate fee for the transaction. The estimate is made by assembling multiple transactions and analyzing the - * distribution of their fees. The estimated_max is the highest fee observed, and the estimated_min is the fee which - * is lower than at least 90% of the fees observed. - * Api Url: #estimateFee - * - * @param walletId wallet's id - * @param payments A list of target outputs ( address, amount ) - * @param withdrawal nullable, when provided, instruments the server to automatically withdraw rewards from the source - * wallet when they are deemed sufficient (i.e. they contribute to the balance for at least as much - * as they cost). - * @return estimatedfee response - * @throws CardanoApiException thrown on API error response, contains error message and code from API - */ - public CompletionStage estimateFee( - String walletId, List payments, String withdrawal) throws CardanoApiException { - return helpExecute.execute( - api.estimateFee(walletId, - new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), - withdrawal, option(Optional.empty()))); + return estimateFee(walletId, payments, "self", null); } /** @@ -230,11 +208,15 @@ public CompletionStage estimateFee( * @throws CardanoApiException thrown on API error response, contains error message and code from API */ public CompletionStage estimateFee( - String walletId, List payments, String withdrawal, CardanoApiCodec.TxMetadataIn metadata) throws CardanoApiException { + String walletId, + List payments, + String withdrawal, + CardanoApiCodec.TxMetadataIn metadata) throws CardanoApiException { + return helpExecute.execute( api.estimateFee(walletId, new CardanoApiCodec.Payments(CollectionConverters.asScala(payments).toSeq()), - withdrawal, option(Optional.of(metadata)))); + option(withdrawal), option(metadata))); } /** @@ -257,6 +239,7 @@ public CompletionStage fundPayments( * list of known addresses, ordered from newest to oldest * Api Url: #Addresses * + * * @param walletId wallet's id * @param addressFilter addresses state: used, unused * @return list of wallet's addresses @@ -279,8 +262,7 @@ public CompletionStage> listAddresses( */ public CompletionStage> listAddresses( String walletId) throws CardanoApiException { - return helpExecute.execute( - api.listAddresses(walletId, scala.Option.empty())).thenApply(CollectionConverters::asJava); + return listAddresses(walletId, null); } /** diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index 5bfaa61..f96109a 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -290,7 +290,7 @@ class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: Act */ def estimateFee(fromWalletId: String, payments: Payments, - withdrawal: String = "self", + withdrawal: Option[String], metadataIn: Option[TxMetadataIn] = None ): Future[CardanoApiRequest[EstimateFeeResponse]] = { diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index a4db5be..3152171 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -162,7 +162,7 @@ object CardanoApiCodec { metadata: Option[TxMetadataIn], withdrawal: Option[String]) - private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: String, metadata: Option[TxMetadataIn]) + private[cardano] case class EstimateFee(payments: Seq[Payment], withdrawal: Option[String], metadata: Option[TxMetadataIn]) case class Payments(payments: Seq[Payment]) diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index 66b8b29..bd537ce 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -109,7 +109,7 @@ object CardanoApiMain { val addr = arguments.get(CmdLine.address) val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) val payments = Payments(Seq(singlePayment)) - val result = unwrap(api.estimateFee(walletId, payments).executeBlocking) + val result = unwrap(api.estimateFee(walletId, payments, None).executeBlocking) trace(result) } else if (hasArgument(CmdLine.getWallet)) { From f9e62ce4476b464570c49a515fa5319cd3484e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Wed, 7 Oct 2020 11:41:06 +0100 Subject: [PATCH 34/39] Task/psgs 55 cardano api units (#17) Cardano API units ( scala + java ) #PSGS-55 --- src/it/resources/application.conf | 14 +- ...noJpiSpec.scala => CardanoJpiITSpec.scala} | 4 +- .../jpi/ListTransactionsParamBuilder.java | 3 +- .../iog/psg/cardano/jpi/HelpExecute.scala | 9 + .../iog/psg/cardano/jpi/JpiResponseCheck.java | 17 ++ src/test/resources/application.conf | 8 +- src/test/resources/jsons/addresses.json | 8 + src/test/resources/jsons/estimate_fees.json | 2 +- .../resources/jsons/unused_addresses.json | 10 + src/test/resources/jsons/used_addresses.json | 6 + .../iog/psg/cardano/CardanoApiCodecSpec.scala | 205 ++---------------- .../iog/psg/cardano/CardanoApiSpec.scala | 144 ++++++++++++ .../iog/psg/cardano/CardanoJpiSpec.scala | 148 +++++++++++++ .../iog/psg/cardano/util/DummyModel.scala | 187 ++++++++++++++++ .../psg/cardano/util/InMemoryCardanoApi.scala | 98 +++++++++ .../iog/psg/cardano/util/JsonFiles.scala | 33 +++ .../iog/psg/cardano/util/ModelCompare.scala | 2 +- .../cardano/util/PatienceConfiguration.scala | 11 + 18 files changed, 699 insertions(+), 210 deletions(-) rename src/it/scala/iog/psg/cardano/jpi/{CardanoJpiSpec.scala => CardanoJpiITSpec.scala} (97%) create mode 100644 src/test/resources/jsons/unused_addresses.json create mode 100644 src/test/resources/jsons/used_addresses.json create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/CardanoJpiSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/util/DummyModel.scala create mode 100644 src/test/scala/iog/psg/cardano/util/InMemoryCardanoApi.scala create mode 100644 src/test/scala/iog/psg/cardano/util/JsonFiles.scala create mode 100644 src/test/scala/iog/psg/cardano/util/PatienceConfiguration.scala diff --git a/src/it/resources/application.conf b/src/it/resources/application.conf index 0e35b59..f929658 100644 --- a/src/it/resources/application.conf +++ b/src/it/resources/application.conf @@ -1,19 +1,19 @@ -cardano.wallet.baseUrl=${BASE_URL} +cardano.wallet.baseUrl=${?BASE_URL} -cardano.wallet1.passphrase=${CARDANO_API_WALLET_1_PASSPHRASE} +cardano.wallet1.passphrase=${?CARDANO_API_WALLET_1_PASSPHRASE} cardano.wallet1.name="cardano_api_wallet_1" cardano.wallet1.amount=2000000 -cardano.wallet1.mnemonic=${CARDANO_API_WALLET_1_MNEMONIC} +cardano.wallet1.mnemonic=${?CARDANO_API_WALLET_1_MNEMONIC} cardano.wallet1.id="6cd6d11a489b7ea82a4624d18b93bdf9b77f0620" cardano.wallet1.metadata="0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" -cardano.wallet2.mnemonic=${CARDANO_API_WALLET_2_MNEMONIC} +cardano.wallet2.mnemonic=${?CARDANO_API_WALLET_2_MNEMONIC} cardano.wallet2.id="bfa9530c4ecfee6e5561e950bd7a7a332e4e7497" cardano.wallet2.name="somethrowawayname" cardano.wallet2.passphrase="somethrowawayname" -cardano.wallet3.mnemonic=${CARDANO_API_WALLET_3_MNEMONIC} -cardano.wallet3.mnemonicsecondary=${CARDANO_API_WALLET_3_MNEMONIC_SECONDARY} +cardano.wallet3.mnemonic=${?CARDANO_API_WALLET_3_MNEMONIC} +cardano.wallet3.mnemonicsecondary=${?CARDANO_API_WALLET_3_MNEMONIC_SECONDARY} cardano.wallet3.id="4a583f9487bac2059caf50d753da1c91ede74345" cardano.wallet3.name="cardano_api_wallet_3" -cardano.wallet3.passphrase=${CARDANO_API_WALLET_3_PASSPHRASE} +cardano.wallet3.passphrase=${?CARDANO_API_WALLET_3_PASSPHRASE} diff --git a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala similarity index 97% rename from src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala rename to src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala index 4407204..5c457d3 100644 --- a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiSpec.scala +++ b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala @@ -13,10 +13,8 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava} -import scala.jdk.OptionConverters.RichOption - -class CardanoJpiSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare with BeforeAndAfterAll { +class CardanoJpiITSpec extends AnyFlatSpec with Matchers with Configure with ModelCompare with BeforeAndAfterAll { override def afterAll(): Unit = { sut.deleteWallet(TestWalletsConfig.walletsMap(3).id) diff --git a/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java b/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java index b5535cd..3c7e22e 100644 --- a/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java +++ b/src/main/java/iog/psg/cardano/jpi/ListTransactionsParamBuilder.java @@ -49,11 +49,10 @@ private ListTransactionsParamBuilder(String walletId) { Objects.requireNonNull(walletId, "WalletId cannot be null"); } - static ListTransactionsParamBuilder create(String walletId) { + public static ListTransactionsParamBuilder create(String walletId) { return new ListTransactionsParamBuilder(walletId); } - public ListTransactionsParamBuilder withEndTime(ZonedDateTime endTime) { this.endTime = endTime; return this; diff --git a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala index ec94d30..b5f064f 100644 --- a/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala +++ b/src/main/scala/iog/psg/cardano/jpi/HelpExecute.scala @@ -25,6 +25,15 @@ object HelpExecute { case (k, v) => k.toLong -> MetadataValueStr (v) } }.toMap + + def unwrap[T](responseF: Future[CardanoApiResponse[T]])(implicit ec: ExecutionContext): Future[T] = for { + either <- responseF + response <- either match { + case Left(error) => Future.failed(new CardanoApiException(error.message, error.code)) + case Right(value) => Future.successful(value) + } + } yield response + } class HelpExecute(implicit ec: ExecutionContext, as: ActorSystem) extends JApiRequestExecutor { diff --git a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java index f74ea24..682f6fc 100644 --- a/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java +++ b/src/test/java/iog/psg/cardano/jpi/JpiResponseCheck.java @@ -1,13 +1,17 @@ package iog.psg.cardano.jpi; +import akka.actor.ActorSystem; import iog.psg.cardano.CardanoApiCodec; import scala.Enumeration; import scala.Option; +import scala.concurrent.Future; import scala.jdk.CollectionConverters; import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; +import static scala.compat.java8.FutureConverters.*; +import scala.util.Either; public class JpiResponseCheck { @@ -105,6 +109,19 @@ public CardanoApiCodec.CreateTransactionResponse getTx(String walletId, String t return jpi.getTransaction(walletId, txId).toCompletableFuture().get(timeout, timeoutUnit); } + public static CardanoApi buildWithPredefinedApiExecutor(iog.psg.cardano.ApiRequestExecutor executor, ActorSystem as) { + CardanoApiBuilder builder = CardanoApiBuilder.create("http://fake:1234/").withApiExecutor(new ApiRequestExecutor() { + @Override + public CompletionStage execute(iog.psg.cardano.CardanoApi.CardanoApiRequest request) throws CardanoApiException { + Future> sResponse = executor.execute(request, as.dispatcher(), as); + CompletionStage jResponse = toJava(HelpExecute.unwrap(sResponse, as.dispatcher())); + return jResponse; + } + }); + + return builder.build(); + } + public static CardanoApi buildWithDummyApiExecutor() { CardanoApiBuilder builder = CardanoApiBuilder.create("http://fake/").withApiExecutor(new ApiRequestExecutor() { @Override diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index 8584057..1939ea7 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -1,12 +1,12 @@ -cardano.wallet.baseUrl=${BASE_URL} -cardano.wallet.passphrase=${CARDANO_API_WALLET_1_PASSPHRASE} +cardano.wallet.baseUrl=${?BASE_URL} +cardano.wallet.passphrase=${?CARDANO_API_WALLET_1_PASSPHRASE} cardano.wallet.name="cardano_api_wallet_1" cardano.wallet.amount=2000000 -cardano.wallet.mnemonic=${CARDANO_API_WALLET_1_MNEMONIC} +cardano.wallet.mnemonic=${?CARDANO_API_WALLET_1_MNEMONIC} cardano.wallet.id="6cd6d11a489b7ea82a4624d18b93bdf9b77f0620" cardano.wallet.metadata="0:0123456789012345678901234567890123456789012345678901234567890123:2:TESTINGCARDANOAPI" -cardano.wallet2.mnemonic=${CARDANO_API_WALLET_2_MNEMONIC} +cardano.wallet2.mnemonic=${?CARDANO_API_WALLET_2_MNEMONIC} cardano.wallet2.id="bfa9530c4ecfee6e5561e950bd7a7a332e4e7497" cardano.wallet2.name="somethrowawayname" cardano.wallet2.passphrase="somethrowawayname" \ No newline at end of file diff --git a/src/test/resources/jsons/addresses.json b/src/test/resources/jsons/addresses.json index ef15cbc..3a1adbc 100644 --- a/src/test/resources/jsons/addresses.json +++ b/src/test/resources/jsons/addresses.json @@ -1,6 +1,14 @@ [ { "id": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "unused" + }, + { + "id": "addr2sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", "state": "used" + }, + { + "id": "addr3sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "unused" } ] \ No newline at end of file diff --git a/src/test/resources/jsons/estimate_fees.json b/src/test/resources/jsons/estimate_fees.json index ed2c553..e26545c 100644 --- a/src/test/resources/jsons/estimate_fees.json +++ b/src/test/resources/jsons/estimate_fees.json @@ -4,7 +4,7 @@ "unit": "lovelace" }, "estimated_max": { - "quantity": 42000000, + "quantity": 126000000, "unit": "lovelace" } } \ No newline at end of file diff --git a/src/test/resources/jsons/unused_addresses.json b/src/test/resources/jsons/unused_addresses.json new file mode 100644 index 0000000..9f88002 --- /dev/null +++ b/src/test/resources/jsons/unused_addresses.json @@ -0,0 +1,10 @@ +[ + { + "id": "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "unused" + }, + { + "id": "addr3sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "unused" + } +] \ No newline at end of file diff --git a/src/test/resources/jsons/used_addresses.json b/src/test/resources/jsons/used_addresses.json new file mode 100644 index 0000000..dd94028 --- /dev/null +++ b/src/test/resources/jsons/used_addresses.json @@ -0,0 +1,6 @@ +[ + { + "id": "addr2sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + "state": "used" + } +] \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala index 0891ec7..1f5f252 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala @@ -2,38 +2,25 @@ package iog.psg.cardano import java.time.ZonedDateTime -import io.circe.Decoder -import io.circe.parser._ -import io.circe.generic.auto._ - import iog.psg.cardano.CardanoApiCodec._ -import iog.psg.cardano.util.ModelCompare - +import iog.psg.cardano.util.{DummyModel, JsonFiles, ModelCompare} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import scala.io.Source - -class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { +class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare with DummyModel with JsonFiles { "Wallet" should "be decoded properly" in { - val decoded = decodeJsonFile[Wallet]("wallet.json") - - compareWallets(decoded, wallet) + compareWallets(jsonFileWallet, wallet) } it should "decode wallet's list" in { - val decodedWallets = decodeJsonFile[Seq[Wallet]]("wallets.json") - - decodedWallets.size shouldBe 1 - compareWallets(decodedWallets.head, wallet) + jsonFileWallets.size shouldBe 1 + compareWallets(jsonFileWallets.head, wallet) } "network information" should "be decoded properly" in { - val decoded = decodeJsonFile[NetworkInfo]("netinfo.json") - compareNetworkInformation( - decoded, + jsonFileNetInfo, NetworkInfo( syncProgress = SyncStatus(SyncState.ready, None), networkTip = networkTip.copy(height = None), @@ -41,195 +28,29 @@ class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare { nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) ) ) - } "list addresses" should "be decoded properly" in { - val decoded = decodeJsonFile[Seq[WalletAddressId]]("addresses.json") - decoded.size shouldBe 1 + jsonFileAddresses.size shouldBe 3 - compareAddress(decoded.head, WalletAddressId(id = addressIdStr, Some(AddressFilter.used))) + compareAddress(jsonFileAddresses.head, WalletAddressId(id = addressIdStr, Some(AddressFilter.unUsed))) } "list transactions" should "be decoded properly" in { - val decoded = decodeJsonFile[Seq[CreateTransactionResponse]]("transactions.json") - decoded.size shouldBe 1 - - compareTransaction(decoded.head, createdTransactionResponse) + jsonFileCreatedTransactionsResponse.size shouldBe 1 + compareTransaction(jsonFileCreatedTransactionsResponse.head, createdTransactionResponse) } it should "decode one transaction" in { - val decoded = decodeJsonFile[CreateTransactionResponse]("transaction.json") - - compareTransaction(decoded, createdTransactionResponse) + compareTransaction(jsonFileCreatedTransactionResponse, createdTransactionResponse) } "estimate fees" should "be decoded properly" in { - val decoded = decodeJsonFile[EstimateFeeResponse]("estimate_fees.json") - - compareEstimateFeeResponse(decoded, estimateFeeResponse) + compareEstimateFeeResponse(jsonFileEstimateFees, estimateFeeResponse) } "fund payments" should "be decoded properly" in { - val decoded = decodeJsonFile[FundPaymentsResponse]("coin_selections_random.json") - - compareFundPaymentsResponse(decoded, fundPaymentsResponse) - } - - private def getJsonFromFile(file: String): String = { - val source = Source.fromURL(getClass.getResource(s"/jsons/$file")) - val jsonStr = source.mkString - source.close() - jsonStr + compareFundPaymentsResponse(jsonFileCoinSelectionRandom, fundPaymentsResponse) } - - private def decodeJsonFile[T](file: String)(implicit dec: Decoder[T]) = { - val jsonStr = getJsonFromFile(file) - decode[T](jsonStr).getOrElse(fail("Could not decode wallet")) - } - - private final lazy val wallet = Wallet( - id = "2512a00e9653fe49a44a5886202e24d77eeb998f", - addressPoolGap = 20, - balance = Balance( - available = QuantityUnit(42000000, Units.lovelace), - reward = QuantityUnit(42000000, Units.lovelace), - total = QuantityUnit(42000000, Units.lovelace) - ), - delegation = Some( - Delegation( - active = DelegationActive( - status = DelegationStatus.delegating, - target = Some("1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1") - ), - next = List( - DelegationNext( - status = DelegationStatus.notDelegating, - changesAt = - Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14)) - ) - ) - ) - ), - name = "Alan's Wallet", - passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2019-02-27T14:46:45.000Z")), - state = SyncStatus(SyncState.ready, None), - tip = networkTip - ) - - private final lazy val networkTip = NetworkTip( - epochNumber = 14, - slotNumber = 1337, - height = Some(QuantityUnit(1337, Units.block)), - absoluteSlotNumber = Some(8086) - ) - - private final lazy val nodeTip = NodeTip( - epochNumber = 14, - slotNumber = 1337, - height = QuantityUnit(1337, Units.block), - absoluteSlotNumber = Some(8086) - ) - - private final lazy val timedBlock = TimedBlock( - time = ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), - block = Block( - slotNumber = 1337, - epochNumber = 14, - height = QuantityUnit(1337, Units.block), - absoluteSlotNumber = Some(8086) - ) - ) - - private final lazy val createdTransactionResponse = { - val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) - - CreateTransactionResponse( - id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", - amount = commonAmount, - insertedAt = Some(timedBlock), - pendingSince = Some(timedBlock), - depth = Some(QuantityUnit(quantity = 1337, unit = Units.block)), - direction = TxDirection.outgoing, - inputs = Seq(inAddress), - outputs = Seq(outAddress), - withdrawals = Seq( - StakeAddress( - stakeAddress = "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", - amount = commonAmount - ) - ), - status = TxState.pending, - metadata = Some(TxMetadataOut(json = parse(""" - |{ - | "0": { - | "string": "cardano" - | }, - | "1": { - | "int": 14 - | }, - | "2": { - | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" - | }, - | "3": { - | "list": [ - | { - | "int": 14 - | }, - | { - | "int": 42 - | }, - | { - | "string": "1337" - | } - | ] - | }, - | "4": { - | "map": [ - | { - | "k": { - | "string": "key" - | }, - | "v": { - | "string": "value" - | } - | }, - | { - | "k": { - | "int": 14 - | }, - | "v": { - | "int": 42 - | } - | } - | ] - | } - | } - |""".stripMargin).getOrElse(fail("Invalid metadata json")))) - ) - } - - private final lazy val estimateFeeResponse = { - val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) - - EstimateFeeResponse(estimatedMin = commonAmount, estimatedMax = commonAmount) - } - - private final lazy val fundPaymentsResponse = - FundPaymentsResponse(inputs = IndexedSeq(inAddress), outputs = Seq(outAddress)) - - private final lazy val inAddress = InAddress( - address = Some(addressIdStr), - amount = Some(QuantityUnit(quantity = 42000000, unit = Units.lovelace)), - id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", - index = 0 - ) - - private final lazy val outAddress = - OutAddress(address = addressIdStr, amount = QuantityUnit(quantity = 42000000, unit = Units.lovelace)) - - private final lazy val addressIdStr = - "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" - } \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala new file mode 100644 index 0000000..cf77864 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala @@ -0,0 +1,144 @@ +package iog.psg.cardano + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi.ErrorMessage +import iog.psg.cardano.CardanoApiCodec.AddressFilter +import iog.psg.cardano.util.{CustomPatienceConfiguration, DummyModel, InMemoryCardanoApi, JsonFiles, ModelCompare} +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class CardanoApiSpec + extends AnyFlatSpec + with Matchers + with ModelCompare + with ScalaFutures + with InMemoryCardanoApi + with DummyModel + with JsonFiles + with CustomPatienceConfiguration { + + lazy val api = new CardanoApi(baseUrl) + + private val walletNotFoundError = ErrorMessage(s"Wallet not found", "404") + + "GET /wallets" should "return wallets list" in { + api.listWallets.executeOrFail().head shouldBe wallet + } + + "GET /wallets/{walletId}" should "return existing wallet" in { + api.getWallet(wallet.id).executeOrFail() shouldBe wallet + } + + it should "return 404 if wallet does not exists" in { + api.getWallet("invalid_wallet_id").executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "GET /network/information" should "return network information" in { + api.networkInfo.executeOrFail() shouldBe networkInfo + } + + "POST /wallets" should "" in { + api.createRestoreWallet(wallet.name, "Pass9128!", mnemonicSentence).executeOrFail() shouldBe wallet + } + + "GET /wallets/{walletId}/addresses?state=unused" should "return wallet's unused addresses" in { + api.listAddresses(wallet.id, Some(AddressFilter.unUsed)).executeOrFail().map(_.id) shouldBe unUsedAddresses.map( + _.id + ) + } + + it should "return wallet's used addresses" in { + api.listAddresses(wallet.id, Some(AddressFilter.used)).executeOrFail().map(_.id) shouldBe usedAddresses.map(_.id) + } + + it should "return wallet not found error" in { + api + .listAddresses("invalid_wallet_id", Some(AddressFilter.used)) + .executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "GET /wallets/{walletId}/transactions" should "return wallet's transactions" in { + api.listTransactions(wallet.id).executeOrFail().map(_.id) shouldBe Seq(createdTransactionResponse.id) + } + + it should "return not found error" in { + api.listTransactions("invalid_wallet_id").executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "GET /wallets/{walletId}/transactions/{transactionId}" should "return transaction" in { + api + .getTransaction(wallet.id, createdTransactionResponse.id) + .executeOrFail() + .id shouldBe createdTransactionResponse.id + } + + it should "return not found error" in { + api.getTransaction(wallet.id, "not_existing_id").executeExpectingErrorOrFail() shouldBe ErrorMessage( + "Transaction not found", + "404" + ) + } + + "POST /wallets/{walletId}/transactions" should "create transaction" in { + api + .createTransaction( + fromWalletId = wallet.id, + passphrase = "MySecret", + payments = payments, + metadata = None, + withdrawal = None + ) + .executeOrFail() + .id shouldBe createdTransactionResponse.id + } + + it should "return not found" in { + api + .createTransaction( + fromWalletId = "invalid_wallet_id", + passphrase = "MySecret", + payments = payments, + metadata = None, + withdrawal = None + ) + .executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "POST /wallets/{fromWalletId}/payment-fees" should "estimate fee" in { + api.estimateFee(wallet.id, payments, None).executeOrFail() shouldBe estimateFeeResponse + } + + it should "return not found" in { + api.estimateFee("invalid_wallet_id", payments, None).executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "POST /wallets/{walletId}/coin-selections/random" should "fund payments" in { + api.fundPayments(wallet.id, payments).executeOrFail() shouldBe fundPaymentsResponse + } + + it should "return not found" in { + api.fundPayments("invalid_wallet_id", payments).executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "PUT /wallets/{walletId/passphrase" should "update passphrase" in { + api.updatePassphrase(wallet.id, "old_password", "new_password").executeOrFail() shouldBe () + } + + it should "return not found" in { + api + .updatePassphrase("invalid_wallet_id", "old_password", "new_password") + .executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + "DELETE /wallets/{walletId" should "delete wallet" in { + api.deleteWallet(wallet.id).executeOrFail() shouldBe () + } + + it should "return not found" in { + api.deleteWallet("invalid_wallet_id").executeExpectingErrorOrFail() shouldBe walletNotFoundError + } + + override implicit val as: ActorSystem = ActorSystem("cardano-api-test-system") + +} diff --git a/src/test/scala/iog/psg/cardano/CardanoJpiSpec.scala b/src/test/scala/iog/psg/cardano/CardanoJpiSpec.scala new file mode 100644 index 0000000..bee060f --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoJpiSpec.scala @@ -0,0 +1,148 @@ +package iog.psg.cardano + +import java.util.concurrent.CompletionStage + +import akka.actor.ActorSystem +import iog.psg.cardano.jpi.{ AddressFilter, JpiResponseCheck, ListTransactionsParamBuilder } +import iog.psg.cardano.util.{ Configure, DummyModel, InMemoryCardanoApi, JsonFiles, ModelCompare } +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.jdk.CollectionConverters._ +import scala.util.Try + +class CardanoJpiSpec + extends AnyFlatSpec + with Matchers + with Configure + with ModelCompare + with ScalaFutures + with InMemoryCardanoApi + with DummyModel + with JsonFiles { + + lazy val api = JpiResponseCheck.buildWithPredefinedApiExecutor(inMemoryExecutor, as) + + private def tryGetErrorMessage[T](completionStage: CompletionStage[T]) = + Try(completionStage.toCompletableFuture.get()).toEither.swap.getOrElse(fail("should fail")).getMessage + + private val walletNotFoundError = "iog.psg.cardano.jpi.CardanoApiException: Message: Wallet not found, Code: 404" + + "GET /wallets" should "return wallets list" in { + val response = api.listWallets().toCompletableFuture.get() + response.size() shouldBe 1 + response.get(0) shouldBe wallet + } + + "GET /wallets/{walletId}" should "return existing wallet" in { + val response = api.getWallet(wallet.id).toCompletableFuture.get() + response shouldBe wallet + } + + it should "return 404 if wallet does not exists" in { + tryGetErrorMessage(api.getWallet("invalid_wallet_id")) shouldBe walletNotFoundError + } + + "GET /network/information" should "return network information" in { + api.networkInfo.toCompletableFuture.get() shouldBe networkInfo + } + + "POST /wallets" should "" in { + api + .createRestore(wallet.name, "Pass9128!", mnemonicSentence.mnemonicSentence.toList.asJava, 5) + .toCompletableFuture + .get() shouldBe wallet + } + + "GET /wallets/{walletId}/addresses?state=unused" should "return wallet's unused addresses" in { + val ids = api.listAddresses(wallet.id, AddressFilter.UNUSED).toCompletableFuture.get().asScala.toList.map(_.id) + ids shouldBe unUsedAddresses.map(_.id) + } + + it should "return wallet's used addresses" in { + val ids = api.listAddresses(wallet.id, AddressFilter.USED).toCompletableFuture.get().asScala.toList.map(_.id) + ids shouldBe usedAddresses.map(_.id) + } + + it should "return wallet not found error" in { + tryGetErrorMessage(api.listAddresses("invalid_wallet_id", AddressFilter.USED)) shouldBe walletNotFoundError + } + + "GET /wallets/{walletId}/transactions" should "return wallet's transactions" in { + val builder = ListTransactionsParamBuilder.create(wallet.id) + api.listTransactions(builder).toCompletableFuture.get().asScala.map(_.id) shouldBe Seq( + createdTransactionResponse.id + ) + } + + it should "return not found error" in { + val builder = ListTransactionsParamBuilder.create("invalid_wallet_id") + tryGetErrorMessage(api.listTransactions(builder)) shouldBe walletNotFoundError + } + + "GET /wallets/{walletId}/transactions/{transactionId}" should "return transaction" in { + api + .getTransaction(wallet.id, createdTransactionResponse.id) + .toCompletableFuture + .get() + .id shouldBe createdTransactionResponse.id + } + + it should "return not found error" in { + tryGetErrorMessage( + api.getTransaction(wallet.id, "not_existing_id") + ) shouldBe "iog.psg.cardano.jpi.CardanoApiException: Message: Transaction not found, Code: 404" + } + + "POST /wallets/{walletId}/transactions" should "create transaction" in { + api + .createTransaction(wallet.id, "MySecret", payments.payments.asJava) + .toCompletableFuture + .get() + .id shouldBe createdTransactionResponse.id + } + + it should "return not found" in { + tryGetErrorMessage( + api.createTransaction("invalid_wallet_id", "MySecret", payments.payments.asJava) + ) shouldBe walletNotFoundError + } + + "POST /wallets/{fromWalletId}/payment-fees" should "estimate fee" in { + api.estimateFee(wallet.id, payments.payments.asJava).toCompletableFuture.get() shouldBe estimateFeeResponse + } + + it should "return not found" in { + tryGetErrorMessage(api.estimateFee("invalid_wallet_id", payments.payments.asJava)) shouldBe walletNotFoundError + } + + "POST /wallets/{walletId}/coin-selections/random" should "fund payments" in { + api.fundPayments(wallet.id, payments.payments.asJava).toCompletableFuture.get() shouldBe fundPaymentsResponse + } + + it should "return not found" in { + tryGetErrorMessage(api.fundPayments("invalid_wallet_id", payments.payments.asJava)) shouldBe walletNotFoundError + } + + "PUT /wallets/{walletId/passphrase" should "update passphrase" in { + api.updatePassphrase(wallet.id, "old_password", "new_password").toCompletableFuture.get() shouldBe null + } + + it should "return not found" in { + tryGetErrorMessage( + api.updatePassphrase("invalid_wallet_id", "old_password", "new_password") + ) shouldBe walletNotFoundError + } + + "DELETE /wallets/{walletId" should "delete wallet" in { + api.deleteWallet(wallet.id).toCompletableFuture.get() shouldBe null + } + + it should "return not found" in { + tryGetErrorMessage(api.deleteWallet("invalid_wallet_id")) shouldBe walletNotFoundError + } + + override implicit val as: ActorSystem = ActorSystem("cardano-api-jpi-test-system") + +} diff --git a/src/test/scala/iog/psg/cardano/util/DummyModel.scala b/src/test/scala/iog/psg/cardano/util/DummyModel.scala new file mode 100644 index 0000000..e32197c --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/DummyModel.scala @@ -0,0 +1,187 @@ +package iog.psg.cardano.util + +import java.time.ZonedDateTime + +import io.circe.parser.parse +import iog.psg.cardano.CardanoApiCodec._ +import org.scalatest.Assertions + +trait DummyModel { self: Assertions => + + final val addressIdStr = + "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" + + final val inAddress = InAddress( + address = Some(addressIdStr), + amount = Some(QuantityUnit(quantity = 42000000, unit = Units.lovelace)), + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + index = 0 + ) + + final val outAddress = + OutAddress(address = addressIdStr, amount = QuantityUnit(quantity = 42000000, unit = Units.lovelace)) + + final val timedBlock = TimedBlock( + time = ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), + block = Block( + slotNumber = 1337, + epochNumber = 14, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + ) + + final val createdTransactionResponse = { + val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + + CreateTransactionResponse( + id = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1", + amount = commonAmount, + insertedAt = Some(timedBlock), + pendingSince = Some(timedBlock), + depth = Some(QuantityUnit(quantity = 1337, unit = Units.block)), + direction = TxDirection.outgoing, + inputs = Seq(inAddress), + outputs = Seq(outAddress), + withdrawals = Seq( + StakeAddress( + stakeAddress = "stake1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2x", + amount = commonAmount + ) + ), + status = TxState.pending, + metadata = Some(TxMetadataOut(json = parse(""" + |{ + | "0": { + | "string": "cardano" + | }, + | "1": { + | "int": 14 + | }, + | "2": { + | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + | }, + | "3": { + | "list": [ + | { + | "int": 14 + | }, + | { + | "int": 42 + | }, + | { + | "string": "1337" + | } + | ] + | }, + | "4": { + | "map": [ + | { + | "k": { + | "string": "key" + | }, + | "v": { + | "string": "value" + | } + | }, + | { + | "k": { + | "int": 14 + | }, + | "v": { + | "int": 42 + | } + | } + | ] + | } + | } + |""".stripMargin).getOrElse(fail("Invalid metadata json")))) + ) + } + + final val addresses = Seq( + WalletAddressId( + id = "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + state = Some(AddressFilter.unUsed) + ), + WalletAddressId( + id = "addr2sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + state = Some(AddressFilter.used) + ), + WalletAddressId( + id = "addr3sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", + state = Some(AddressFilter.unUsed) + ) + ) + + final val unUsedAddresses = addresses.filter(_.state.contains(AddressFilter.unUsed)) + final val usedAddresses = addresses.filter(_.state.contains(AddressFilter.used)) + + final val networkTip = NetworkTip( + epochNumber = 14, + slotNumber = 1337, + height = Some(QuantityUnit(1337, Units.block)), + absoluteSlotNumber = Some(8086) + ) + + final val wallet = Wallet( + id = "2512a00e9653fe49a44a5886202e24d77eeb998f", + addressPoolGap = 20, + balance = Balance( + available = QuantityUnit(42000000, Units.lovelace), + reward = QuantityUnit(42000000, Units.lovelace), + total = QuantityUnit(42000000, Units.lovelace) + ), + delegation = Some( + Delegation( + active = DelegationActive( + status = DelegationStatus.delegating, + target = Some("1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1") + ), + next = List( + DelegationNext( + status = DelegationStatus.notDelegating, + changesAt = + Some(NextEpoch(epochStartTime = ZonedDateTime.parse("2020-01-22T10:06:39.037Z"), epochNumber = 14)) + ) + ) + ) + ), + name = "Alan's Wallet", + passphrase = Passphrase(lastUpdatedAt = ZonedDateTime.parse("2019-02-27T14:46:45.000Z")), + state = SyncStatus(SyncState.ready, None), + tip = networkTip + ) + + final lazy val nodeTip = NodeTip( + epochNumber = 14, + slotNumber = 1337, + height = QuantityUnit(1337, Units.block), + absoluteSlotNumber = Some(8086) + ) + + final val networkInfo = NetworkInfo( + syncProgress = SyncStatus(SyncState.ready, None), + networkTip = networkTip.copy(height = None), + nodeTip = nodeTip, + nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) + ) + + final val mnemonicSentence = GenericMnemonicSentence( + "a b c d e a b c d e a b c d e" + ) + + final val payments = Payments( + Seq( + Payment(unUsedAddresses.head.id, QuantityUnit(100000, Units.lovelace)) + ) + ) + + final val estimateFeeResponse = { + val estimatedMin = QuantityUnit(quantity = 42000000, unit = Units.lovelace) + EstimateFeeResponse(estimatedMin = estimatedMin, estimatedMax = estimatedMin.copy(quantity = estimatedMin.quantity * 3)) + } + + final val fundPaymentsResponse = + FundPaymentsResponse(inputs = IndexedSeq(inAddress), outputs = Seq(outAddress)) +} diff --git a/src/test/scala/iog/psg/cardano/util/InMemoryCardanoApi.scala b/src/test/scala/iog/psg/cardano/util/InMemoryCardanoApi.scala new file mode 100644 index 0000000..3bd950b --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/InMemoryCardanoApi.scala @@ -0,0 +1,98 @@ +package iog.psg.cardano.util + +import java.io.File + +import akka.actor.ActorSystem +import akka.http.scaladsl.model._ +import io.circe.generic.auto._ +import io.circe.syntax._ +import iog.psg.cardano.CardanoApi.{ CardanoApiRequest, CardanoApiResponse, ErrorMessage } +import iog.psg.cardano.CardanoApiCodec.AddressFilter +import iog.psg.cardano.{ ApiRequestExecutor, CardanoApi } +import org.scalatest.Assertions +import org.scalatest.concurrent.ScalaFutures + +import scala.concurrent.{ ExecutionContext, Future } + +trait InMemoryCardanoApi { + this: ScalaFutures with Assertions with JsonFiles => + + implicit val as: ActorSystem + implicit lazy val ec = as.dispatcher + + final val baseUrl: String = "http://fake:1234/" + + private implicit final class RegexOps(sc: StringContext) { + def r = new util.matching.Regex(sc.parts.mkString, sc.parts.tail.map(_ => "x"): _*) + } + + implicit final class InMemoryExecutor[T](req: CardanoApiRequest[T]) { + def executeOrFail(): T = inMemoryExecutor.execute(req).futureValue.getOrElse(fail("Request failed.")) + + def executeExpectingErrorOrFail(): ErrorMessage = + inMemoryExecutor.execute(req).futureValue.swap.getOrElse(fail("Request should failed.")) + } + + implicit final class InMemoryFExecutor[T](req: Future[CardanoApiRequest[T]]) { + def executeOrFail(): T = inMemoryExecutor.execute(req.futureValue).futureValue.getOrElse(fail("Request failed.")) + + def executeExpectingErrorOrFail(): ErrorMessage = + inMemoryExecutor.execute(req.futureValue).futureValue.swap.getOrElse(fail("Request should failed.")) + } + + private def httpEntityFromJson( + jsonFileName: String, + contentType: ContentType = ContentType.WithFixedCharset(MediaTypes.`application/json`) + ) = { + val resource = getClass.getResource(s"/jsons/$jsonFileName") + val file = new File(resource.getFile) + HttpEntity.fromFile(contentType, file) + } + + val inMemoryExecutor: ApiRequestExecutor = new ApiRequestExecutor { + override def execute[T]( + request: CardanoApi.CardanoApiRequest[T] + )(implicit ec: ExecutionContext, as: ActorSystem): Future[CardanoApiResponse[T]] = { + val apiAddress = request.request.uri.toString().split(baseUrl).lastOption.getOrElse("") + val method = request.request.method + + implicit def univEntToHttpResponse[T](ue: UniversalEntity): HttpResponse = + HttpResponse(entity = ue) + + def notFound(msg: String) = { + val json: String = ErrorMessage(msg, "404").asJson.noSpaces + val entity = HttpEntity(json) + request.mapper(HttpResponse(status = StatusCodes.NotFound, entity = entity)) + } + + (apiAddress, method) match { + case ("network/information", HttpMethods.GET) => request.mapper(httpEntityFromJson("netinfo.json")) + case ("wallets", HttpMethods.GET) => request.mapper(httpEntityFromJson("wallets.json")) + case ("wallets", HttpMethods.POST) => request.mapper(httpEntityFromJson("wallet.json")) + case (s"wallets/${jsonFileWallet.id}", HttpMethods.GET) => request.mapper(httpEntityFromJson("wallet.json")) + case (s"wallets/${jsonFileWallet.id}", HttpMethods.DELETE) => + request.mapper(HttpResponse(status = StatusCodes.NoContent)) + case (s"wallets/${jsonFileWallet.id}/passphrase", HttpMethods.PUT) => + request.mapper(HttpResponse(status = StatusCodes.NoContent)) + case (s"wallets/${jsonFileWallet.id}/addresses?state=unused", HttpMethods.GET) => + request.mapper(httpEntityFromJson("unused_addresses.json")) + case (s"wallets/${jsonFileWallet.id}/addresses?state=used", HttpMethods.GET) => + request.mapper(httpEntityFromJson("used_addresses.json")) + case (s"wallets/${jsonFileWallet.id}/transactions", HttpMethods.GET) => + request.mapper(httpEntityFromJson("transactions.json")) + case (s"wallets/${jsonFileWallet.id}/transactions/${jsonFileCreatedTransactionResponse.id}", HttpMethods.GET) => + request.mapper(httpEntityFromJson("transaction.json")) + case (s"wallets/${jsonFileWallet.id}/transactions", HttpMethods.POST) => + request.mapper(httpEntityFromJson("transaction.json")) + case (s"wallets/${jsonFileWallet.id}/payment-fees", HttpMethods.POST) => + request.mapper(httpEntityFromJson("estimate_fees.json")) + case (s"wallets/${jsonFileWallet.id}/coin-selections/random", HttpMethods.POST) => + request.mapper(httpEntityFromJson("coin_selections_random.json")) + case (r"wallets/.+/transactions/.+", HttpMethods.GET) => notFound("Transaction not found") + case (r"wallets/.+", _) => notFound("Wallet not found") + case _ => notFound("Not found") + } + + } + } +} diff --git a/src/test/scala/iog/psg/cardano/util/JsonFiles.scala b/src/test/scala/iog/psg/cardano/util/JsonFiles.scala new file mode 100644 index 0000000..f89ab20 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/JsonFiles.scala @@ -0,0 +1,33 @@ +package iog.psg.cardano.util + +import io.circe.Decoder +import io.circe.generic.auto._ +import io.circe.parser.decode +import iog.psg.cardano.CardanoApiCodec._ +import org.scalatest.Assertions + +import scala.io.Source + +trait JsonFiles { self: Assertions => + + final lazy val jsonFileWallet = decodeJsonFile[Wallet]("wallet.json") + final lazy val jsonFileWallets = decodeJsonFile[Seq[Wallet]]("wallets.json") + final lazy val jsonFileNetInfo = decodeJsonFile[NetworkInfo]("netinfo.json") + final lazy val jsonFileAddresses = decodeJsonFile[Seq[WalletAddressId]]("addresses.json") + final lazy val jsonFileCreatedTransactionResponse = decodeJsonFile[CreateTransactionResponse]("transaction.json") + final lazy val jsonFileCreatedTransactionsResponse = decodeJsonFile[Seq[CreateTransactionResponse]]("transactions.json") + final lazy val jsonFileCoinSelectionRandom = decodeJsonFile[FundPaymentsResponse]("coin_selections_random.json") + final lazy val jsonFileEstimateFees = decodeJsonFile[EstimateFeeResponse]("estimate_fees.json") + + final def getJsonFromFile(file: String): String = { + val source = Source.fromURL(getClass.getResource(s"/jsons/$file")) + val jsonStr = source.mkString + source.close() + jsonStr + } + + final def decodeJsonFile[T](file: String)(implicit dec: Decoder[T]): T = { + val jsonStr = getJsonFromFile(file) + decode[T](jsonStr).getOrElse(fail(s"Could not decode $file")) + } +} diff --git a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala index 65bd7df..11b232d 100644 --- a/src/test/scala/iog/psg/cardano/util/ModelCompare.scala +++ b/src/test/scala/iog/psg/cardano/util/ModelCompare.scala @@ -65,7 +65,7 @@ trait ModelCompare extends Matchers { final def compareAddress(decoded: WalletAddressId, expected: WalletAddressId): Assertion = { decoded.id shouldBe expected.id - decoded.state shouldBe expected.state + decoded.state.map(_.toString) shouldBe expected.state.map(_.toString) } final def compareNetworkInformation(decoded: NetworkInfo, expected: NetworkInfo): Assertion = { diff --git a/src/test/scala/iog/psg/cardano/util/PatienceConfiguration.scala b/src/test/scala/iog/psg/cardano/util/PatienceConfiguration.scala new file mode 100644 index 0000000..0b916e4 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/util/PatienceConfiguration.scala @@ -0,0 +1,11 @@ +package iog.psg.cardano.util + +import org.scalatest.concurrent.Eventually +import org.scalatest.time.{Millis, Seconds, Span} + +trait CustomPatienceConfiguration extends Eventually { + + implicit override val patienceConfig = + PatienceConfig(timeout = scaled(Span(2, Seconds)), interval = scaled(Span(5, Millis))) + +} From c0ff9af6ab3e6558e494e0cfccf126bea18bf7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20B=C4=85k?= Date: Thu, 8 Oct 2020 09:18:42 +0100 Subject: [PATCH 35/39] Task/psgs 44 sub 55 pure units (#15) TxMetadataOut.toMetadataMap, CmdLine simple units for netinfo #PSGS-55 --- ...nSpec.scala => CardanoApiMainITSpec.scala} | 3 +- .../psg/cardano/CardanoApiTestScript.scala | 2 +- .../psg/cardano/jpi/CardanoJpiITSpec.scala | 12 +- .../scala/iog/psg/cardano/CardanoApi.scala | 2 +- .../iog/psg/cardano/CardanoApiCodec.scala | 29 ++-- .../iog/psg/cardano/CardanoApiMain.scala | 64 +++----- .../scala/iog/psg/cardano/TxMetadataOut.scala | 133 +++++++++++++++++ src/test/resources/jsons/netinfo.json | 2 +- src/test/resources/jsons/transaction.json | 4 +- src/test/resources/jsons/transactions.json | 4 +- .../iog/psg/cardano/CardanoApiCodecSpec.scala | 56 ------- .../iog/psg/cardano/CardanoApiMainSpec.scala | 74 ++++++++++ .../iog/psg/cardano/CardanoApiSpec.scala | 2 +- .../iog/psg/cardano/TxMetadataCodecSpec.scala | 107 ++++++++++++++ .../iog/psg/cardano/util/DummyModel.scala | 138 +++++++++--------- .../iog/psg/cardano/util/JsonFiles.scala | 7 - 16 files changed, 438 insertions(+), 201 deletions(-) rename src/it/scala/iog/psg/cardano/{CardanoApiMainSpec.scala => CardanoApiMainITSpec.scala} (99%) create mode 100644 src/main/scala/iog/psg/cardano/TxMetadataOut.scala delete mode 100644 src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala create mode 100644 src/test/scala/iog/psg/cardano/TxMetadataCodecSpec.scala diff --git a/src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/it/scala/iog/psg/cardano/CardanoApiMainITSpec.scala similarity index 99% rename from src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala rename to src/it/scala/iog/psg/cardano/CardanoApiMainITSpec.scala index b228ff4..3b731bc 100644 --- a/src/it/scala/iog/psg/cardano/CardanoApiMainSpec.scala +++ b/src/it/scala/iog/psg/cardano/CardanoApiMainITSpec.scala @@ -12,7 +12,7 @@ import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures with BeforeAndAfterAll { +class CardanoApiMainITSpec extends AnyFlatSpec with Matchers with Configure with ScalaFutures with BeforeAndAfterAll { override def afterAll(): Unit = { Seq(2, 3).map { num => @@ -38,7 +38,6 @@ class CardanoApiMainSpec extends AnyFlatSpec with Matchers with Configure with S var results: Seq[String] = Seq.empty implicit val memTrace = new Trace { override def apply(s: Object): Unit = results = s.toString +: results - override def close(): Unit = () } diff --git a/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala b/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala index d6359dd..e88b70d 100644 --- a/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala +++ b/src/it/scala/iog/psg/cardano/CardanoApiTestScript.scala @@ -5,7 +5,7 @@ import java.util import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps._ import iog.psg.cardano.CardanoApi._ -import iog.psg.cardano.CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} +import CardanoApiCodec.{AddressFilter, CreateTransactionResponse, GenericMnemonicSentence, MetadataValueArray, MetadataValueLong, MetadataValueStr, Payment, Payments, QuantityUnit, SyncState, TxMetadataMapIn, TxState, Units} import org.apache.commons.codec.binary.Hex import scala.annotation.tailrec diff --git a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala index 5c457d3..e25155a 100644 --- a/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala +++ b/src/it/scala/iog/psg/cardano/jpi/CardanoJpiITSpec.scala @@ -7,7 +7,7 @@ import iog.psg.cardano.CardanoApiCodec._ import iog.psg.cardano.TestWalletsConfig import iog.psg.cardano.TestWalletsConfig.baseUrl import iog.psg.cardano.common.TestWalletFixture -import iog.psg.cardano.util.{Configure, ModelCompare} +import iog.psg.cardano.util.{Configure, CustomPatienceConfiguration, ModelCompare} import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -21,7 +21,7 @@ class CardanoJpiITSpec extends AnyFlatSpec with Matchers with Configure with Mod super.afterAll() } - private val timeoutValue: Long = 10 + private val timeoutValue: Long = 30 private val timeoutUnits = TimeUnit.SECONDS private lazy val sut = new JpiResponseCheck(new CardanoApiFixture(baseUrl).getJpi, timeoutValue, timeoutUnits) @@ -154,9 +154,11 @@ class CardanoJpiITSpec extends AnyFlatSpec with Matchers with Configure with Mod createTxResponse.id shouldBe getTxResponse.id createTxResponse.amount shouldBe getTxResponse.amount - val Right(mapOut) = createTxResponse.metadata.get.json.as[Map[Long, String]] - mapOut(Long.MaxValue) shouldBe "0" * 64 - mapOut(Long.MaxValue - 1) shouldBe "1" * 64 + + val responseMetadataMap = createTxResponse.metadata.get.toMetadataMap.getOrElse(fail("Invalid metadata json.")) + + responseMetadataMap(Long.MaxValue) shouldBe MetadataValueStr("0" * 64) + responseMetadataMap(Long.MaxValue - 1) shouldBe MetadataValueStr("1" * 64) } diff --git a/src/main/scala/iog/psg/cardano/CardanoApi.scala b/src/main/scala/iog/psg/cardano/CardanoApi.scala index f96109a..9c1bf0e 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApi.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApi.scala @@ -77,7 +77,7 @@ object CardanoApi { class CardanoApi(baseUriWithPort: String)(implicit ec: ExecutionContext, as: ActorSystem) { import iog.psg.cardano.CardanoApi._ - import iog.psg.cardano.CardanoApiCodec._ + import CardanoApiCodec._ import AddressFilter.AddressFilter private val wallets = s"${baseUriWithPort}wallets" diff --git a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala index 3152171..4628d77 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiCodec.scala @@ -63,10 +63,6 @@ object CardanoApiCodec { private[cardano] implicit val decodeDelegationStatus: Decoder[DelegationStatus] = Decoder.decodeString.map(DelegationStatus.withName) private[cardano] implicit val encodeDelegationStatus: Encoder[DelegationStatus] = (a: DelegationStatus) => Json.fromString(a.toString) - final case class TxMetadataOut(json: Json) { - def toMapMetadataStr: Decoder.Result[Map[Long, String]] = json.as[Map[Long, String]] - } - private[cardano] implicit val decodeTxMetadataOut: Decoder[TxMetadataOut] = Decoder.decodeJson.map(TxMetadataOut) private[cardano] implicit val decodeKeyMetadata: KeyDecoder[MetadataKey] = (key: String) => Some(MetadataValueStr(key)) @@ -79,7 +75,12 @@ object CardanoApiCodec { final case class TxMetadataMapIn[K <: Long](m: Map[K, MetadataValue]) extends TxMetadataIn object JsonMetadata { - def apply(str: String): JsonMetadata = JsonMetadata(str.asJson) + def apply(rawJson: String): JsonMetadata = parse(rawJson) match { + case Left(p: ParsingFailure) => throw p + case Right(jsonMeta) => jsonMeta + } + + def parse(rawJson: String): Either[ParsingFailure, JsonMetadata] = parser.parse(rawJson).map(JsonMetadata(_)) } final case class JsonMetadata(metadataCompliantJson: Json) extends TxMetadataIn @@ -90,9 +91,9 @@ object CardanoApiCodec { final case class MetadataValueArray(ary: Seq[MetadataValue]) extends MetadataValue - final case class MetadataValueByteArray(ary: ByteString) extends MetadataValue + final case class MetadataValueByteString(bs: ByteString) extends MetadataValue - final case class MetadataValueObject(s: Map[MetadataKey, MetadataValue]) extends MetadataValue + final case class MetadataValueMap(s: Map[MetadataKey, MetadataValue]) extends MetadataValue implicit val metadataKeyDecoder: KeyEncoder[MetadataKey] = { case MetadataValueLong(l) => l.toString @@ -105,17 +106,19 @@ object CardanoApiCodec { } implicit val encodeTxMeta: Encoder[MetadataValue] = Encoder.instance { - case MetadataValueLong(s) => s.asJson - case MetadataValueStr(s) => s.asJson - case MetadataValueArray(s) => s.asJson - case MetadataValueObject(s) => s.asJson - case MetadataValueByteArray(ary: ByteString) => toMetadataHex(ary) + case MetadataValueLong(s) => Json.obj(("int", Json.fromLong(s))) + case MetadataValueStr(s) => Json.obj(("string", Json.fromString(s))) + case MetadataValueByteString(bs: ByteString) => Json.obj(("bytes", Json.fromString(bs.utf8String))) + case MetadataValueArray(s) => Json.obj(("list", s.asJson)) + case MetadataValueMap(s) => + Json.obj(("map", s.map { + case (key, value) => Map("k" -> key, "v" -> value) + }.asJson)) } implicit val encodeTxMetadata: Encoder[TxMetadataIn] = Encoder.instance { case JsonMetadata(metadataCompliantJson) => metadataCompliantJson case TxMetadataMapIn(s) => s.asJson - } diff --git a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala index bd537ce..d1c419c 100644 --- a/src/main/scala/iog/psg/cardano/CardanoApiMain.scala +++ b/src/main/scala/iog/psg/cardano/CardanoApiMain.scala @@ -6,7 +6,7 @@ import java.time.ZonedDateTime import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.CardanoApiOps.{CardanoApiRequestFOps, CardanoApiRequestOps} import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage, Order, defaultMaxWaitTime} -import iog.psg.cardano.CardanoApiCodec.{AddressFilter, GenericMnemonicSecondaryFactor, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} +import CardanoApiCodec.{AddressFilter, GenericMnemonicSecondaryFactor, GenericMnemonicSentence, Payment, Payments, QuantityUnit, Units} import iog.psg.cardano.util.StringToMetaMapParser.toMetaMap import iog.psg.cardano.util._ @@ -97,51 +97,37 @@ object CardanoApiMain { if (hasArgument(CmdLine.netInfo)) { - val result = unwrap(api.networkInfo.executeBlocking) - trace(result) + unwrap[CardanoApiCodec.NetworkInfo](api.networkInfo.executeBlocking, trace(_)) } else if (hasArgument(CmdLine.listWallets)) { - val result = unwrap(api.listWallets.executeBlocking) - result.foreach(trace.apply) - + unwrap[Seq[CardanoApiCodec.Wallet]](api.listWallets.executeBlocking, r => r.foreach(trace.apply)) } else if (hasArgument(CmdLine.estimateFee)) { val walletId = arguments.get(CmdLine.walletId) val amount = arguments.get(CmdLine.amount).toLong val addr = arguments.get(CmdLine.address) val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) val payments = Payments(Seq(singlePayment)) - val result = unwrap(api.estimateFee(walletId, payments, None).executeBlocking) - trace(result) - + unwrap[CardanoApiCodec.EstimateFeeResponse](api.estimateFee(walletId, payments, None).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.getWallet)) { val walletId = arguments.get(CmdLine.walletId) - val result = unwrap(api.getWallet(walletId).executeBlocking) - trace(result) - + unwrap[CardanoApiCodec.Wallet](api.getWallet(walletId).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.updatePassphrase)) { val walletId = arguments.get(CmdLine.walletId) val oldPassphrase = arguments.get(CmdLine.oldPassphrase) val newPassphrase = arguments.get(CmdLine.passphrase) - val result: Unit = unwrap(api.updatePassphrase(walletId, oldPassphrase, newPassphrase).executeBlocking) - trace("Unit result from update passphrase") + unwrap[Unit](api.updatePassphrase(walletId, oldPassphrase, newPassphrase).executeBlocking, _ => trace("Unit result from update passphrase")) } else if (hasArgument(CmdLine.deleteWallet)) { val walletId = arguments.get(CmdLine.walletId) - val result: Unit = unwrap(api.deleteWallet(walletId).executeBlocking) - trace("Unit result from delete wallet") - + unwrap[Unit](api.deleteWallet(walletId).executeBlocking, _ => trace("Unit result from delete wallet")) } else if (hasArgument(CmdLine.listWalletAddresses)) { val walletId = arguments.get(CmdLine.walletId) val addressesState = Some(AddressFilter.withName(arguments.get(CmdLine.state))) - val result = unwrap(api.listAddresses(walletId, addressesState).executeBlocking) - trace(result) - + unwrap[Seq[CardanoApiCodec.WalletAddressId]](api.listAddresses(walletId, addressesState).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.getTx)) { val walletId = arguments.get(CmdLine.walletId) val txId = arguments.get(CmdLine.txId) - val result = unwrap(api.getTransaction(walletId, txId).executeBlocking) - trace(result) - + unwrap[CardanoApiCodec.CreateTransactionResponse](api.getTransaction(walletId, txId).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.createTx)) { val walletId = arguments.get(CmdLine.walletId) val amount = arguments.get(CmdLine.amount).toLong @@ -150,14 +136,14 @@ object CardanoApiMain { val metadata = toMetaMap(arguments(CmdLine.metadata)) val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) val payments = Payments(Seq(singlePayment)) - val result = unwrap(api.createTransaction( + + unwrap[CardanoApiCodec.CreateTransactionResponse](api.createTransaction( walletId, pass, payments, metadata, None - ).executeBlocking) - trace(result) + ).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.fundTx)) { val walletId = arguments.get(CmdLine.walletId) @@ -165,12 +151,11 @@ object CardanoApiMain { val addr = arguments.get(CmdLine.address) val singlePayment = Payment(addr, QuantityUnit(amount, Units.lovelace)) val payments = Payments(Seq(singlePayment)) - val result = unwrap(api.fundPayments( + + unwrap[CardanoApiCodec.FundPaymentsResponse](api.fundPayments( walletId, payments - ).executeBlocking) - trace(result) - + ).executeBlocking, trace(_)) } else if (hasArgument(CmdLine.listWalletTransactions)) { val walletId = arguments.get(CmdLine.walletId) @@ -179,19 +164,13 @@ object CardanoApiMain { val orderOf = arguments(CmdLine.order).flatMap(s => Try(Order.withName(s)).toOption).getOrElse(Order.descendingOrder) val minWithdrawalTx = arguments(CmdLine.minWithdrawal).map(_.toInt) - val result = unwrap(api.listTransactions( + unwrap[Seq[CardanoApiCodec.CreateTransactionResponse]](api.listTransactions( walletId, startDate, endDate, orderOf, minWithdrawal = minWithdrawalTx - ).executeBlocking) - - if (result.isEmpty) { - trace("No txs returned") - } else { - result.foreach(trace.apply) - } + ).executeBlocking, r => if (r.isEmpty) trace("No txs returned") else r.foreach(trace.apply)) } else if (hasArgument(CmdLine.createWallet) || hasArgument(CmdLine.restoreWallet)) { val name = arguments.get(CmdLine.name) @@ -200,15 +179,13 @@ object CardanoApiMain { val mnemonicSecondaryOpt = arguments(CmdLine.mnemonicSecondary) val addressPoolGap = arguments(CmdLine.addressPoolGap).map(_.toInt) - val result = unwrap(api.createRestoreWallet( + unwrap[CardanoApiCodec.Wallet](api.createRestoreWallet( name, passphrase, GenericMnemonicSentence(mnemonic), mnemonicSecondaryOpt.map(m => GenericMnemonicSecondaryFactor(m)), addressPoolGap - ).executeBlocking) - - trace(result) + ).executeBlocking, trace(_)) } else { trace("No command recognised") @@ -384,7 +361,8 @@ object CardanoApiMain { ) } - def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T])(implicit t: Trace): T = unwrapOpt(Try(apiResult)).get + def unwrap[T: ClassTag](apiResult: CardanoApiResponse[T], onSuccess: T => Unit)(implicit t: Trace): Unit = + unwrapOpt(Try(apiResult)).foreach(onSuccess) def unwrapOpt[T: ClassTag](apiResult: Try[CardanoApiResponse[T]])(implicit trace: Trace): Option[T] = apiResult match { case Success(Left(ErrorMessage(message, code))) => diff --git a/src/main/scala/iog/psg/cardano/TxMetadataOut.scala b/src/main/scala/iog/psg/cardano/TxMetadataOut.scala new file mode 100644 index 0000000..f9162c8 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/TxMetadataOut.scala @@ -0,0 +1,133 @@ +package iog.psg.cardano + +import akka.util.ByteString +import io.circe.CursorOp.DownField +import io.circe._ +import iog.psg.cardano.CardanoApiCodec._ + +final case class TxMetadataOut(json: Json) { + + type DecodingEither[T] = Either[DecodingFailure, T] + + final val ValueTypeString = "string" + final val ValueTypeLong = "int" //named int but will work as long + final val ValueTypeBytes = "bytes" + final val ValueTypeList = "list" + final val ValueTypeMap = "map" + + def toMetadataMap: Decoder.Result[Map[Long, MetadataValue]] = { + type KeyVal = Map[Long, MetadataValue] + + implicit val decodeMap: Decoder[Map[Long, MetadataValue]] = (c: HCursor) => { + + def extractStringField(cursor: ACursor): DecodingEither[MetadataValueStr] = + cursor.downField(ValueTypeString).as[String].fold( + err => Left(err), + (value: String) => Right(MetadataValueStr(value)) + ) + + def extractLongField(cursor: ACursor): DecodingEither[MetadataValueLong] = + cursor.downField(ValueTypeLong).as[Long].fold( + err => Left(err), + (value: Long) => Right(MetadataValueLong(value)) + ) + + def extractBytesField(cursor: ACursor): DecodingEither[MetadataValueByteString] = + cursor.downField(ValueTypeBytes).as[String].fold( + err => Left(err), + (value: String) => Right(MetadataValueByteString(ByteString(value))) + ) + + def extractTypedFieldValue(json: Json, key: String): DecodingEither[MetadataValue] = { + val cursor = json.hcursor + cursor.keys.flatMap(_.headOption) match { + case Some(ValueTypeString) => + extractStringField(cursor) + + case Some(ValueTypeLong) => + extractLongField(cursor) + + case Some(ValueTypeBytes) => + extractBytesField(cursor) + + case _ => Left(DecodingFailure("Invalid type value", List(DownField(key)))) + } + } + + def extractListField(cursor: ACursor, key: String): DecodingEither[MetadataValueArray] = { + val keyValuesObjects: List[Json] = cursor.downField(ValueTypeList).values.map(_.toList).getOrElse(Nil) + val listResults: Seq[DecodingEither[MetadataValue]] = keyValuesObjects.map(objJson => extractTypedFieldValue(objJson, key)) + + listMapErrorOrResult(listResults, () => { + val values = listResults.flatMap(_.toOption) + MetadataValueArray(values) + }) + } + + def extractMapField(cursor: ACursor, key: String): DecodingEither[MetadataValueMap] = { + val downFieldMap = cursor.downField(ValueTypeMap) + val keyValuesObjects: List[Json] = downFieldMap.values.map(_.toList).getOrElse(Nil) + + def getMapField[T <: MetadataValue](keyName: String, json: Json): DecodingEither[MetadataValue] = for { + keyJson <- json.\\(keyName).headOption.toRight(DecodingFailure(s"Missing '$keyName' value", List(DownField(key)))) + value <- extractTypedFieldValue(keyJson, key) + } yield value + + val listResults: Seq[DecodingEither[(MetadataKey, MetadataValue)]] = keyValuesObjects.map { json => + (getMapField[MetadataKey]("k", json), getMapField[MetadataValue]("v", json)) match { + case (Right(keyField), Right(valueField)) => Right(keyField.asInstanceOf[MetadataKey] -> valueField) + case (Left(error), _) => Left(error) + case (_, Left(error)) => Left(error) + } + } + + listMapErrorOrResult(listResults, () => { + val values: Map[MetadataKey, MetadataValue] = listResults.flatMap(_.toOption).toMap + MetadataValueMap(values) + }) + } + + /** + * If any of parsed values in list or map contains an error, it will return it + */ + def listMapErrorOrResult[A, B](results: Seq[DecodingEither[A]], onRight: () => B): DecodingEither[B] = + results.find(_.isLeft) match { + case Some(Left(error)) => Left(error) + case _ => Right(onRight()) + } + + def extractValueForKeyInto(res: Decoder.Result[KeyVal], key: String): Decoder.Result[KeyVal] = { + res.flatMap((map: KeyVal) => { + val keyDownField: ACursor = c.downField(key) + keyDownField.keys.flatMap(_.headOption) match { + case Some(valueType) if valueType == ValueTypeString => + extractStringField(keyDownField).map(extractedValue => map.+(key.toLong -> extractedValue)) + + case Some(valueType) if valueType == ValueTypeLong => + extractLongField(keyDownField).map(extractedValue => map.+(key.toLong -> extractedValue)) + + case Some(valueType) if valueType == ValueTypeBytes => + extractBytesField(keyDownField).map(extractedValue => map.+(key.toLong -> extractedValue)) + + case Some(valueType) if valueType == ValueTypeList => + extractListField(keyDownField, key).map(valueArray => map.+(key.toLong -> valueArray)) + + case Some(valueType) if valueType == ValueTypeMap => + extractMapField(keyDownField, key).map(valueMap => map.+(key.toLong -> valueMap)) + + case None => Left(DecodingFailure("Missing value under key", List(DownField(key)))) + } + }) + } + + def emptyMapResult: Decoder.Result[KeyVal] = Right(Map[Long, MetadataValue]().empty) + + def withKeys(keys: Iterable[String]): Decoder.Result[KeyVal] = keys.foldLeft(emptyMapResult)(extractValueForKeyInto) + + c.keys.fold[Decoder.Result[KeyVal]](ifEmpty = emptyMapResult)(withKeys) + } + + json.as[Map[Long, MetadataValue]](decodeMap) + } +} + diff --git a/src/test/resources/jsons/netinfo.json b/src/test/resources/jsons/netinfo.json index 4bd32d2..124c444 100644 --- a/src/test/resources/jsons/netinfo.json +++ b/src/test/resources/jsons/netinfo.json @@ -18,6 +18,6 @@ }, "next_epoch": { "epoch_number": 14, - "epoch_start_time": "2019-02-27T14:46:45.000Z" + "epoch_start_time": "2000-01-02T03:04:05.000Z" } } \ No newline at end of file diff --git a/src/test/resources/jsons/transaction.json b/src/test/resources/jsons/transaction.json index aa1692a..f2e92b1 100644 --- a/src/test/resources/jsons/transaction.json +++ b/src/test/resources/jsons/transaction.json @@ -5,7 +5,7 @@ "unit": "lovelace" }, "inserted_at": { - "time": "2019-02-27T14:46:45.000Z", + "time": "2000-01-02T03:04:05.000Z", "block": { "slot_number": 1337, "epoch_number": 14, @@ -17,7 +17,7 @@ } }, "pending_since": { - "time": "2019-02-27T14:46:45.000Z", + "time": "2000-01-02T03:04:05.000Z", "block": { "slot_number": 1337, "epoch_number": 14, diff --git a/src/test/resources/jsons/transactions.json b/src/test/resources/jsons/transactions.json index 3d83f59..0fd9894 100644 --- a/src/test/resources/jsons/transactions.json +++ b/src/test/resources/jsons/transactions.json @@ -6,7 +6,7 @@ "unit": "lovelace" }, "inserted_at": { - "time": "2019-02-27T14:46:45.000Z", + "time": "2000-01-02T03:04:05.000Z", "block": { "slot_number": 1337, "epoch_number": 14, @@ -18,7 +18,7 @@ } }, "pending_since": { - "time": "2019-02-27T14:46:45.000Z", + "time": "2000-01-02T03:04:05.000Z", "block": { "slot_number": 1337, "epoch_number": 14, diff --git a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala deleted file mode 100644 index 1f5f252..0000000 --- a/src/test/scala/iog/psg/cardano/CardanoApiCodecSpec.scala +++ /dev/null @@ -1,56 +0,0 @@ -package iog.psg.cardano - -import java.time.ZonedDateTime - -import iog.psg.cardano.CardanoApiCodec._ -import iog.psg.cardano.util.{DummyModel, JsonFiles, ModelCompare} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class CardanoApiCodecSpec extends AnyFlatSpec with Matchers with ModelCompare with DummyModel with JsonFiles { - - "Wallet" should "be decoded properly" in { - compareWallets(jsonFileWallet, wallet) - } - - it should "decode wallet's list" in { - jsonFileWallets.size shouldBe 1 - compareWallets(jsonFileWallets.head, wallet) - } - - "network information" should "be decoded properly" in { - compareNetworkInformation( - jsonFileNetInfo, - NetworkInfo( - syncProgress = SyncStatus(SyncState.ready, None), - networkTip = networkTip.copy(height = None), - nodeTip = nodeTip, - nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) - ) - ) - } - - "list addresses" should "be decoded properly" in { - jsonFileAddresses.size shouldBe 3 - - compareAddress(jsonFileAddresses.head, WalletAddressId(id = addressIdStr, Some(AddressFilter.unUsed))) - } - - "list transactions" should "be decoded properly" in { - jsonFileCreatedTransactionsResponse.size shouldBe 1 - compareTransaction(jsonFileCreatedTransactionsResponse.head, createdTransactionResponse) - } - - it should "decode one transaction" in { - compareTransaction(jsonFileCreatedTransactionResponse, createdTransactionResponse) - } - - "estimate fees" should "be decoded properly" in { - compareEstimateFeeResponse(jsonFileEstimateFees, estimateFeeResponse) - } - - "fund payments" should "be decoded properly" in { - compareFundPaymentsResponse(jsonFileCoinSelectionRandom, fundPaymentsResponse) - } - -} \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala new file mode 100644 index 0000000..8caf92c --- /dev/null +++ b/src/test/scala/iog/psg/cardano/CardanoApiMainSpec.scala @@ -0,0 +1,74 @@ +package iog.psg.cardano + +import akka.actor.ActorSystem +import iog.psg.cardano.CardanoApi.{CardanoApiResponse, ErrorMessage} +import CardanoApiCodec.NetworkInfo +import iog.psg.cardano.CardanoApiMain.CmdLine +import iog.psg.cardano.util.{ArgumentParser, DummyModel, ModelCompare, Trace} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.{ExecutionContext, Future} + + +class CardanoApiMainSpec extends AnyFlatSpec with Matchers with ModelCompare with DummyModel { + + "The Cmd Line -netInfo" should "show current network information" in new ApiRequestExecutorFixture[NetworkInfo]{ + override val expectedRequestUrl: String = "http://127.0.0.1:8090/v2/network/information" + override val response: CardanoApiResponse[NetworkInfo] = Right(networkInfo) + override val args: Array[String] = Array(CmdLine.netInfo) + + getTraceResults shouldBe "baseurl:http://127.0.0.1:8090/v2/, -netInfo, NetworkInfo(SyncStatus(ready,None),NetworkTip(14,1337,None,Some(8086)),NodeTip(QuantityUnit(1337,block),1337,14,Some(8086)),NextEpoch(2000-01-02T03:04:05Z,14))" + } + + it should "fail with exception during executing request" in new ApiRequestExecutorFixture[NetworkInfo] { + override val expectedRequestUrl: String = "http://127.0.0.1:8090/v2/network/information" + override val response: CardanoApiResponse[NetworkInfo] = Right(networkInfo) + override val args: Array[String] = Array(CmdLine.netInfo) + + override implicit val apiExecutor = new ApiRequestExecutor { + override def execute[T](request: CardanoApi.CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem): Future[CardanoApiResponse[T]] = { + Future.failed(new RuntimeException("Test failed.")) + }.asInstanceOf[Future[CardanoApiResponse[T]]] + } + + getTraceResults shouldBe "baseurl:http://127.0.0.1:8090/v2/, -netInfo, java.lang.RuntimeException: Test failed." + } + + it should "return an API error" in new ApiRequestExecutorFixture[NetworkInfo] { + override val expectedRequestUrl: String = "http://127.0.0.1:8090/v2/network/information" + override val response: CardanoApiResponse[NetworkInfo] = Left(ErrorMessage("Test error.", "12345")) + override val args: Array[String] = Array(CmdLine.netInfo) + + getTraceResults shouldBe "baseurl:http://127.0.0.1:8090/v2/, -netInfo, API Error message Test error., code 12345" + } + + private sealed trait ApiRequestExecutorFixture[T] { + val expectedRequestUrl: String + val response: CardanoApiResponse[T] + val args: Array[String] + + lazy val arguments = new ArgumentParser(args) + + private val traceResults: ArrayBuffer[String] = ArrayBuffer.empty + + implicit private val memTrace = new Trace { + override def apply(s: Object): Unit = traceResults += s.toString + override def close(): Unit = () + } + + implicit val apiExecutor = new ApiRequestExecutor { + override def execute[T](request: CardanoApi.CardanoApiRequest[T])(implicit ec: ExecutionContext, as: ActorSystem): Future[CardanoApiResponse[T]] = { + request.request.uri.toString() shouldBe expectedRequestUrl + Future.successful(response) + }.asInstanceOf[Future[CardanoApiResponse[T]]] + } + + final def getTraceResults: String = { + traceResults.clear() + CardanoApiMain.run(arguments) + traceResults.mkString(", ") + } + } +} diff --git a/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala b/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala index cf77864..30e68a4 100644 --- a/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala +++ b/src/test/scala/iog/psg/cardano/CardanoApiSpec.scala @@ -3,7 +3,7 @@ package iog.psg.cardano import akka.actor.ActorSystem import iog.psg.cardano.CardanoApi.ErrorMessage import iog.psg.cardano.CardanoApiCodec.AddressFilter -import iog.psg.cardano.util.{CustomPatienceConfiguration, DummyModel, InMemoryCardanoApi, JsonFiles, ModelCompare} +import iog.psg.cardano.util._ import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/iog/psg/cardano/TxMetadataCodecSpec.scala b/src/test/scala/iog/psg/cardano/TxMetadataCodecSpec.scala new file mode 100644 index 0000000..d1a3ad6 --- /dev/null +++ b/src/test/scala/iog/psg/cardano/TxMetadataCodecSpec.scala @@ -0,0 +1,107 @@ +package iog.psg.cardano + +import akka.util.ByteString +import io.circe.ParsingFailure +import io.circe.syntax.EncoderOps +import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.util.DummyModel +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class TxMetadataCodecSpec extends AnyFlatSpec with Matchers with DummyModel { + + "TxMetadataMapIn encode" should "encode string value to proper json" in { + val map: Map[Long, MetadataValue] = Map(0L -> MetadataValueStr("cardano")) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + + metaInJsonStr shouldBe """{"0":{"string":"cardano"}}""" + } + + it should "encode long value to proper json" in { + val map: Map[Long, MetadataValue] = Map(1L -> MetadataValueLong(14)) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + + metaInJsonStr shouldBe """{"1":{"int":14}}""" + } + + it should "encode byte string value to proper json" in { + val map: Map[Long, MetadataValue] = Map(2L -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f"))) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + + metaInJsonStr shouldBe """{"2":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"}}""" + } + + it should "encode list value to proper json" in { + val map: Map[Long, MetadataValue] = Map(3L -> MetadataValueArray(List( + MetadataValueLong(14), MetadataValueLong(42), MetadataValueStr("1337") + ))) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + + metaInJsonStr shouldBe """{"3":{"list":[{"int":14},{"int":42},{"string":"1337"}]}}""" + } + + it should "encode map value to proper json" in { + val map: Map[Long, MetadataValue] = Map(4L -> MetadataValueMap(Map( + MetadataValueStr("key") -> MetadataValueStr("value"), + MetadataValueLong(14) -> MetadataValueLong(42) + ))) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + metaInJsonStr shouldBe """{"4":{"map":[{"k":{"string":"key"},"v":{"string":"value"}},{"k":{"int":14},"v":{"int":42}}]}}""" + } + + it should "encode properly all types to json" in { + val map: Map[Long, MetadataValue] = Map( + 0L -> MetadataValueStr("cardano"), + 1L -> MetadataValueLong(14), + 2L -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")), + 3L -> MetadataValueArray(List( + MetadataValueLong(14), MetadataValueLong(42), MetadataValueStr("1337") + )), + 4L -> MetadataValueMap(Map( + MetadataValueStr("key") -> MetadataValueStr("value"), + MetadataValueLong(14) -> MetadataValueLong(42) + )) + ) + val metaDataIn: TxMetadataIn = TxMetadataMapIn(map) + val metaInJsonStr = metaDataIn.asJson.noSpaces + + metaInJsonStr shouldBe """{"0":{"string":"cardano"},"1":{"int":14},"2":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"},"3":{"list":[{"int":14},{"int":42},{"string":"1337"}]},"4":{"map":[{"k":{"string":"key"},"v":{"string":"value"}},{"k":{"int":14},"v":{"int":42}}]}}""".stripMargin + } + + "txMetadataOut toMapMetadataStr" should "be parsed properly" in { + txMetadataOut.toMetadataMap.getOrElse(fail("could not parse map")) shouldBe Map( + 0 -> MetadataValueStr("cardano"), + 1 -> MetadataValueLong(14), + 2 -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")), + 3 -> MetadataValueArray(Seq( + MetadataValueLong(14), + MetadataValueLong(42), + MetadataValueStr("1337") + )), + 4 -> MetadataValueMap( + Map( + MetadataValueStr("key") -> MetadataValueStr("value"), + MetadataValueLong(14) -> MetadataValueLong(42) + ))) + } + + "Raw Good TxMetadata" should "be parsed properly" in { + val asString = txMetadataOut.json.noSpaces + val Right(rawTxMetaJsonIn) = JsonMetadata.parse(asString) + val rawTxMetaJsonIn2 = JsonMetadata(asString) + rawTxMetaJsonIn.metadataCompliantJson shouldBe txMetadataOut.json + rawTxMetaJsonIn2.metadataCompliantJson shouldBe txMetadataOut.json + } + + "Raw Bad TxMetadata" should "be rejected" in { + val asString = txMetadataOut.json.noSpaces + val badJson = asString.substring(0, asString.length - 1) + val Left(ParsingFailure(_, _)) = JsonMetadata.parse(badJson) + intercept[Exception](JsonMetadata(badJson)) + } +} \ No newline at end of file diff --git a/src/test/scala/iog/psg/cardano/util/DummyModel.scala b/src/test/scala/iog/psg/cardano/util/DummyModel.scala index e32197c..ba2538a 100644 --- a/src/test/scala/iog/psg/cardano/util/DummyModel.scala +++ b/src/test/scala/iog/psg/cardano/util/DummyModel.scala @@ -4,10 +4,13 @@ import java.time.ZonedDateTime import io.circe.parser.parse import iog.psg.cardano.CardanoApiCodec._ +import iog.psg.cardano.TxMetadataOut import org.scalatest.Assertions trait DummyModel { self: Assertions => + final lazy val dummyDateTime = ZonedDateTime.parse("2000-01-02T03:04:05.000Z") + final val addressIdStr = "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g" @@ -18,11 +21,11 @@ trait DummyModel { self: Assertions => index = 0 ) - final val outAddress = + final lazy val outAddress = OutAddress(address = addressIdStr, amount = QuantityUnit(quantity = 42000000, unit = Units.lovelace)) - final val timedBlock = TimedBlock( - time = ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), + final lazy val timedBlock = TimedBlock( + time = dummyDateTime, block = Block( slotNumber = 1337, epochNumber = 14, @@ -31,7 +34,54 @@ trait DummyModel { self: Assertions => ) ) - final val createdTransactionResponse = { + final lazy val txMetadataOut = TxMetadataOut(json = parse(""" + |{ + | "0": { + | "string": "cardano" + | }, + | "1": { + | "int": 14 + | }, + | "2": { + | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + | }, + | "3": { + | "list": [ + | { + | "int": 14 + | }, + | { + | "int": 42 + | }, + | { + | "string": "1337" + | } + | ] + | }, + | "4": { + | "map": [ + | { + | "k": { + | "string": "key" + | }, + | "v": { + | "string": "value" + | } + | }, + | { + | "k": { + | "int": 14 + | }, + | "v": { + | "int": 42 + | } + | } + | ] + | } + | } + |""".stripMargin).getOrElse(fail("Invalid metadata json"))) + + final lazy val createdTransactionResponse = { val commonAmount = QuantityUnit(quantity = 42000000, unit = Units.lovelace) CreateTransactionResponse( @@ -50,55 +100,11 @@ trait DummyModel { self: Assertions => ) ), status = TxState.pending, - metadata = Some(TxMetadataOut(json = parse(""" - |{ - | "0": { - | "string": "cardano" - | }, - | "1": { - | "int": 14 - | }, - | "2": { - | "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" - | }, - | "3": { - | "list": [ - | { - | "int": 14 - | }, - | { - | "int": 42 - | }, - | { - | "string": "1337" - | } - | ] - | }, - | "4": { - | "map": [ - | { - | "k": { - | "string": "key" - | }, - | "v": { - | "string": "value" - | } - | }, - | { - | "k": { - | "int": 14 - | }, - | "v": { - | "int": 42 - | } - | } - | ] - | } - | } - |""".stripMargin).getOrElse(fail("Invalid metadata json")))) + metadata = Some(txMetadataOut) ) } + final val addresses = Seq( WalletAddressId( id = "addr1sjck9mdmfyhzvjhydcjllgj9vjvl522w0573ncustrrr2rg7h9azg4cyqd36yyd48t5ut72hgld0fg2xfvz82xgwh7wal6g2xt8n996s3xvu5g", @@ -114,17 +120,17 @@ trait DummyModel { self: Assertions => ) ) - final val unUsedAddresses = addresses.filter(_.state.contains(AddressFilter.unUsed)) - final val usedAddresses = addresses.filter(_.state.contains(AddressFilter.used)) + final lazy val unUsedAddresses = addresses.filter(_.state.contains(AddressFilter.unUsed)) + final lazy val usedAddresses = addresses.filter(_.state.contains(AddressFilter.used)) - final val networkTip = NetworkTip( + final lazy val networkTip = NetworkTip( epochNumber = 14, slotNumber = 1337, height = Some(QuantityUnit(1337, Units.block)), absoluteSlotNumber = Some(8086) ) - final val wallet = Wallet( + final lazy val wallet = Wallet( id = "2512a00e9653fe49a44a5886202e24d77eeb998f", addressPoolGap = 20, balance = Balance( @@ -160,28 +166,26 @@ trait DummyModel { self: Assertions => absoluteSlotNumber = Some(8086) ) - final val networkInfo = NetworkInfo( + final lazy val networkInfo = NetworkInfo( syncProgress = SyncStatus(SyncState.ready, None), networkTip = networkTip.copy(height = None), nodeTip = nodeTip, - nextEpoch = NextEpoch(ZonedDateTime.parse("2019-02-27T14:46:45.000Z"), 14) + nextEpoch = NextEpoch(dummyDateTime, 14) ) - final val mnemonicSentence = GenericMnemonicSentence( - "a b c d e a b c d e a b c d e" - ) + final lazy val mnemonicSentence = GenericMnemonicSentence("a b c d e a b c d e a b c d e") - final val payments = Payments( - Seq( - Payment(unUsedAddresses.head.id, QuantityUnit(100000, Units.lovelace)) - ) - ) + final lazy val payments = Payments(Seq(Payment(unUsedAddresses.head.id, QuantityUnit(100000, Units.lovelace)))) - final val estimateFeeResponse = { + final lazy val estimateFeeResponse = { val estimatedMin = QuantityUnit(quantity = 42000000, unit = Units.lovelace) - EstimateFeeResponse(estimatedMin = estimatedMin, estimatedMax = estimatedMin.copy(quantity = estimatedMin.quantity * 3)) + EstimateFeeResponse( + estimatedMin = estimatedMin, + estimatedMax = estimatedMin.copy(quantity = estimatedMin.quantity * 3) + ) } - final val fundPaymentsResponse = + final lazy val fundPaymentsResponse = FundPaymentsResponse(inputs = IndexedSeq(inAddress), outputs = Seq(outAddress)) + } diff --git a/src/test/scala/iog/psg/cardano/util/JsonFiles.scala b/src/test/scala/iog/psg/cardano/util/JsonFiles.scala index f89ab20..8876ad0 100644 --- a/src/test/scala/iog/psg/cardano/util/JsonFiles.scala +++ b/src/test/scala/iog/psg/cardano/util/JsonFiles.scala @@ -1,7 +1,6 @@ package iog.psg.cardano.util import io.circe.Decoder -import io.circe.generic.auto._ import io.circe.parser.decode import iog.psg.cardano.CardanoApiCodec._ import org.scalatest.Assertions @@ -11,13 +10,7 @@ import scala.io.Source trait JsonFiles { self: Assertions => final lazy val jsonFileWallet = decodeJsonFile[Wallet]("wallet.json") - final lazy val jsonFileWallets = decodeJsonFile[Seq[Wallet]]("wallets.json") - final lazy val jsonFileNetInfo = decodeJsonFile[NetworkInfo]("netinfo.json") - final lazy val jsonFileAddresses = decodeJsonFile[Seq[WalletAddressId]]("addresses.json") final lazy val jsonFileCreatedTransactionResponse = decodeJsonFile[CreateTransactionResponse]("transaction.json") - final lazy val jsonFileCreatedTransactionsResponse = decodeJsonFile[Seq[CreateTransactionResponse]]("transactions.json") - final lazy val jsonFileCoinSelectionRandom = decodeJsonFile[FundPaymentsResponse]("coin_selections_random.json") - final lazy val jsonFileEstimateFees = decodeJsonFile[EstimateFeeResponse]("estimate_fees.json") final def getJsonFromFile(file: String): String = { val source = Source.fromURL(getClass.getResource(s"/jsons/$file")) From da0c709923222cb7c93ab1ee42c353f802d0ec1e Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Thu, 8 Oct 2020 10:40:19 +0100 Subject: [PATCH 36/39] Task/psgs 38 prepare release (#18) * Prepare for release. * Add package of command line dist. --- .github/workflows/publish.yml | 10 ++++++++++ README.md | 27 ++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4f9734a..1798057 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,4 +28,14 @@ jobs: with: java-version: '11.0.8' - run: sbt publishSigned sonatypeRelease + - name: Package Command Line Jar + uses: actions/setup-java@v1.4.2 + with: + java-version: '11.0.8' + - run: sbt assembly + - name: Upload Command Line Jar + uses: actions/upload-artifact@v2 + with: + name: psg-cardano-wallet-api-assembly + path: target/scala-2.13/psg-cardano-wallet-api-assembly*.jar diff --git a/README.md b/README.md index 0015b55..9044579 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ It also provides an executable jar to provide rudimentary command line access. - [java](#usagejava) - [Command line executable jar](#cmdline) - [Examples](#examples) +- [Issues](#issues) ### Building @@ -55,12 +56,18 @@ it also uses [circe](https://circe.github.io/circe/) to marshal and unmarshal th The jar is published in Maven Central, the command line executable jar can be downloaded from the releases section of the [github repository](https://github.com/input-output-hk/psg-cardano-wallet-api) - + + +Before you can use this API you need a cardano wallet backend to contact, you can set one up following the instructions +[here](https://github.com/input-output-hk/cardano-wallet). The docker setup is recommended. + +Alternatively, for 'tire kicking' purposes you may try `http://cardano-wallet-testnet.iog.solutions:8090/v2/` + #### Scala Add the library to your dependencies -`libraryDependencies += "iog.psg" %% "psg-cardano-wallet-api" % "0.4.1"` +`libraryDependencies += "iog.psg" %% "psg-cardano-wallet-api" % "0.2.0"` The api calls return a HttpRequest set up to the correct url and a mapper to take the entity result and map it from Json to the corresponding case classes. Using `networkInfo` as an example... @@ -89,7 +96,16 @@ networkInfo match { #### Java -First, add the library to your dependencies, then using `getWallet` as an example... +First, add the library to your dependencies, +``` + + iog.psg + psg-cardano-wallet-api_2.13 + 0.2.0 + +``` + +Then, using `getWallet` as an example... ``` import iog.psg.cardano.jpi.*; @@ -122,3 +138,8 @@ For example, to see the [network information](https://input-output-hk.github.io/ #### Examples The best place to find working examples is in the [test](https://github.com/input-output-hk/psg-cardano-wallet-api/tree/develop/src/test) folder + +#### Issues + +This release does *not* cover the entire cardano-wallet API, it focuses on getting the shelley core functionality into the hands of developers, if you need another call covered please log +an [issue (or make a PR!)](https://github.com/input-output-hk/psg-cardano-wallet-api/issues) \ No newline at end of file From 30bf97d1d895ea3dd57930ab12f2a962db2e404e Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Thu, 8 Oct 2020 10:46:43 +0100 Subject: [PATCH 37/39] Update publish.yml Remove bracnh guard. --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1798057..a34dc83 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,6 @@ on: jobs: package: - if: contains(github.ref, 'master') || contains(github.ref, 'develop') runs-on: ubuntu-latest steps: - name: Configure GPG Key From 6ec91208689b76b5a25882a3bf829a4c61641e95 Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Thu, 8 Oct 2020 11:49:12 +0100 Subject: [PATCH 38/39] Force snapshot. --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index fbe625b..eec9542 100644 --- a/build.sbt +++ b/build.sbt @@ -23,6 +23,7 @@ lazy val rootProject = (project in file(".")) licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")), description := "A java/scala wrapper for the cardano wallet backend API", usePgpKeyHex("75E12F006A3F08C757EE8343927AE95EEEF4A02F"), + isSnapshot := true, publishTo := Some { // publish to the sonatype repository val sonaUrl = "https://oss.sonatype.org/" From d4c95fa8b8a69f6a95d7e2f33a568948a468b136 Mon Sep 17 00:00:00 2001 From: mcsherrylabs Date: Thu, 8 Oct 2020 12:05:23 +0100 Subject: [PATCH 39/39] Remove isSnapshot=true. --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index eec9542..fbe625b 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,6 @@ lazy val rootProject = (project in file(".")) licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")), description := "A java/scala wrapper for the cardano wallet backend API", usePgpKeyHex("75E12F006A3F08C757EE8343927AE95EEEF4A02F"), - isSnapshot := true, publishTo := Some { // publish to the sonatype repository val sonaUrl = "https://oss.sonatype.org/"