From adeb55b724620d39bf79203a89f5e442d532a8ec Mon Sep 17 00:00:00 2001 From: alanmcsherry Date: Fri, 15 Jul 2022 13:41:57 +0100 Subject: [PATCH] Add PolicyBuilder (#56) * Add PolicyBuilder Make slot a Long (not Int) Add test for DISH policy * Ensure only one Bound of each type exists. * Correct the org name in sbt file Remove redundant test Make slots Long instead of Int --- build.sbt | 2 +- .../experimental/cli/api/CardanoCliApi.scala | 4 +- .../CardanoCliCmdTransactionBuildRaw.scala | 4 +- .../experimental/cli/model/Policy.scala | 18 +--- .../cli/model/PolicyBuilder.scala | 47 +++++++++ .../experimental/cli/CardanoCliApiSpec.scala | 14 +-- .../cli/model/MetadataJsonSpec.scala | 2 +- .../cli/model/PolicyBuilderSpec.scala | 98 +++++++++++++++++++ .../experimental/cli/model/PolicySpec.scala | 25 ++++- 9 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 src/main/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilder.scala create mode 100644 src/test/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilderSpec.scala diff --git a/build.sbt b/build.sbt index be1cdf0..647cffd 100644 --- a/build.sbt +++ b/build.sbt @@ -13,7 +13,7 @@ lazy val rootProject = (project in file(".")) IntegrationTest / dependencyClasspath := (IntegrationTest / dependencyClasspath).value ++ (Test / exportedProducts).value, name:= "psg-cardano-wallet-api", scalaVersion := "2.13.3", - organization := "solutions.iog", + organization := "iog.psg", 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( diff --git a/src/main/scala/iog/psg/cardano/experimental/cli/api/CardanoCliApi.scala b/src/main/scala/iog/psg/cardano/experimental/cli/api/CardanoCliApi.scala index 92db74d..32c6668 100644 --- a/src/main/scala/iog/psg/cardano/experimental/cli/api/CardanoCliApi.scala +++ b/src/main/scala/iog/psg/cardano/experimental/cli/api/CardanoCliApi.scala @@ -161,8 +161,8 @@ case class CardanoCliApi(cardanoCli: CardanoCli)(implicit networkChooser: Networ txOuts: NonEmptyList[TxOut], maybeMetadata: Option[MetadataJson] = None, maybeMinting: Option[(NonEmptyList[NativeAsset], Policy)] = None, - invalidBefore: Option[Int] = None, - invalidHereafter: Option[Int] = None, + invalidBefore: Option[Long] = None, + invalidHereafter: Option[Long] = None, ): CliApiRequest[Tx] = new CliApiRequest[Tx] { override def execute: Future[Tx] = Future { diff --git a/src/main/scala/iog/psg/cardano/experimental/cli/command/CardanoCliCmdTransactionBuildRaw.scala b/src/main/scala/iog/psg/cardano/experimental/cli/command/CardanoCliCmdTransactionBuildRaw.scala index 423c9f7..f08d2ed 100644 --- a/src/main/scala/iog/psg/cardano/experimental/cli/command/CardanoCliCmdTransactionBuildRaw.scala +++ b/src/main/scala/iog/psg/cardano/experimental/cli/command/CardanoCliCmdTransactionBuildRaw.scala @@ -57,13 +57,13 @@ case class CardanoCliCmdTransactionBuildRaw(protected val builder: ProcessBuilde /** * Time that transaction is valid from (in slots) */ - def invalidBefore(slot: Int): CardanoCliCmdTransactionBuildRaw = + def invalidBefore(slot: Long): CardanoCliCmdTransactionBuildRaw = withParam("--invalid-before", slot) /** * Time that transaction is valid until (in slots) */ - def invalidHereafter(slot: Int): CardanoCliCmdTransactionBuildRaw = + def invalidHereafter(slot: Long): CardanoCliCmdTransactionBuildRaw = withParam("--invalid-hereafter", slot) private def mintParam(assets: NonEmptyList[NativeAsset]): String = { diff --git a/src/main/scala/iog/psg/cardano/experimental/cli/model/Policy.scala b/src/main/scala/iog/psg/cardano/experimental/cli/model/Policy.scala index 005b6ef..6ce6833 100644 --- a/src/main/scala/iog/psg/cardano/experimental/cli/model/Policy.scala +++ b/src/main/scala/iog/psg/cardano/experimental/cli/model/Policy.scala @@ -9,6 +9,7 @@ import iog.psg.cardano.experimental.cli.util.RandomTempFolder case class PolicyId(value: String) extends AnyVal + case class Policy( scripts: NonEmptyList[Policy.Script], kind: Policy.Kind @@ -21,21 +22,6 @@ case class Policy( object Policy { - def all(scripts: NonEmptyList[Script])(implicit f: RandomTempFolder): Policy = - Policy(scripts, Policy.Kind.All) - - def any(scripts: NonEmptyList[Script])(implicit f: RandomTempFolder): Policy = - Policy(scripts, Policy.Kind.Any) - - def atLeast(value: Int, scripts: NonEmptyList[Script])(implicit f: RandomTempFolder): Policy = - Policy(scripts, Policy.Kind.AtLeast(value)) - - def after(slot: Int, signatures: NonEmptyList[Script.Signature])(implicit f: RandomTempFolder): Policy = - all(Script.Bound(slot, after = true) :: signatures) - - def before(slot: Int, signatures: NonEmptyList[Script.Signature])(implicit f: RandomTempFolder): Policy = - all(Script.Bound(slot, after = false) :: signatures) - sealed trait Kind object Kind { case object All extends Kind @@ -46,7 +32,7 @@ object Policy { sealed trait Script object Script { case class Signature(keyHash: KeyHash[_ <: KeyType]) extends Script - case class Bound(slot: Int, after: Boolean) extends Script + case class Bound(slot: Long, after: Boolean) extends Script } def asString(policy: Policy): String = codec(policy.rootFolder)(policy).noSpaces diff --git a/src/main/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilder.scala b/src/main/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilder.scala new file mode 100644 index 0000000..8f88c61 --- /dev/null +++ b/src/main/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilder.scala @@ -0,0 +1,47 @@ +package iog.psg.cardano.experimental.cli.model + +import cats.data.NonEmptyList +import iog.psg.cardano.experimental.cli.api.KeyType +import iog.psg.cardano.experimental.cli.model.Policy.Script +import iog.psg.cardano.experimental.cli.util.RandomTempFolder + +case class PolicyBuilder( + private val scripts: Seq[Script] = Seq.empty, + private val kind: Policy.Kind = Policy.Kind.All +) { + + def withAllSigsRequired(): PolicyBuilder = this.copy(kind = Policy.Kind.All) + def withAnySigRequired(): PolicyBuilder = this.copy(kind = Policy.Kind.Any) + + def withAtLeastSigsRequired(numberOfSigsRequired: Int): PolicyBuilder = + this.copy(kind = Policy.Kind.AtLeast(numberOfSigsRequired)) + + def withSignatureOf(keyHash: KeyHash[_ <: KeyType]): PolicyBuilder = + this.copy(scripts = scripts :+ Policy.Script.Signature(keyHash)) + + + def withBeforeConstraint(slot: Long): PolicyBuilder = + this.copy(scripts = scripts.filter { + case Policy.Script.Bound(_, false) => false + case _ => true + } :+ Policy.Script.Bound(slot, after = false)) + + def withAfterConstraint(slot: Long): PolicyBuilder = + this.copy(scripts = scripts.filter { + case Policy.Script.Bound(_, true) => false + case _ => true + } :+ Policy.Script.Bound(slot, after = true)) + + def build(implicit rootFolder: RandomTempFolder): Policy = { + require( + scripts.exists { + case _: Script.Signature => true + case _ => false + }, + "There must be a at least one script of type Signature!" + ) + + Policy(NonEmptyList.of[Policy.Script](scripts.head, scripts.tail: _*), kind) + + } +} diff --git a/src/test/scala/iog/psg/cardano/experimental/cli/CardanoCliApiSpec.scala b/src/test/scala/iog/psg/cardano/experimental/cli/CardanoCliApiSpec.scala index 2cb16c6..5ede15c 100644 --- a/src/test/scala/iog/psg/cardano/experimental/cli/CardanoCliApiSpec.scala +++ b/src/test/scala/iog/psg/cardano/experimental/cli/CardanoCliApiSpec.scala @@ -4,7 +4,7 @@ import cats.data.NonEmptyList import iog.psg.cardano.experimental.cli.api.Ops.CliApiReqOps import iog.psg.cardano.experimental.cli.api._ import iog.psg.cardano.experimental.cli.command.CardanoCli -import iog.psg.cardano.experimental.cli.model.{Key, Policy, TxIn, TxOut} +import iog.psg.cardano.experimental.cli.model.{Key, PolicyBuilder, TxIn, TxOut} import iog.psg.cardano.experimental.cli.processrunner.{BlockingProcessResult, BlockingProcessRunner} import iog.psg.cardano.experimental.cli.util.RandomFolderFactory import org.scalatest.BeforeAndAfterAll @@ -94,12 +94,12 @@ class CardanoCliApiSpec extends AnyFlatSpec with Matchers with ScalaFutures with s"[./cardano-cli, address, key-hash, --payment-verification-key-file, ${policyVerKey.file.toString}]", ) - val policy = Policy.all( - NonEmptyList.of( - Policy.Script.Signature(paymentVerKeyHash), - Policy.Script.Signature(policyVerKeyHash) - ) - ) + + val policy = PolicyBuilder() + .withSignatureOf(paymentVerKeyHash) + .withSignatureOf(policyVerKeyHash) + .withAllSigsRequired() + .build val policyId = sut .policyId(policy) diff --git a/src/test/scala/iog/psg/cardano/experimental/cli/model/MetadataJsonSpec.scala b/src/test/scala/iog/psg/cardano/experimental/cli/model/MetadataJsonSpec.scala index 058a663..ea3ec52 100644 --- a/src/test/scala/iog/psg/cardano/experimental/cli/model/MetadataJsonSpec.scala +++ b/src/test/scala/iog/psg/cardano/experimental/cli/model/MetadataJsonSpec.scala @@ -21,7 +21,7 @@ class MetadataJsonSpec extends AnyFlatSpec with Matchers with ScalaFutures { implicit val rootFolder = RandomTempFolder(Files.createTempDirectory("testNFTMeta")) - val meta = NftMetadataJson(policyId, nfts) + val meta = NftMetadataJson(policyId, nfts.toIndexedSeq) "the encoding" should "work as expected" in { val str = asString(meta) diff --git a/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilderSpec.scala b/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilderSpec.scala new file mode 100644 index 0000000..665760e --- /dev/null +++ b/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicyBuilderSpec.scala @@ -0,0 +1,98 @@ +package iog.psg.cardano.experimental.cli.model + +import iog.psg.cardano.experimental.cli.api.Verification +import iog.psg.cardano.experimental.cli.model.Policy.Script.{Bound, Signature} +import iog.psg.cardano.experimental.cli.util.RandomTempFolder +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.nio.file.Path + +class PolicyBuilderSpec extends AnyFlatSpec with Matchers with EitherValues { + + implicit val rootFolder: RandomTempFolder = RandomTempFolder(Path.of(".")) + + "PolicyBuilder" should "fail if no signature" in { + + intercept[IllegalArgumentException] { + PolicyBuilder().build + } + + intercept[IllegalArgumentException] { + PolicyBuilder().withAfterConstraint(3).build + } + + } + + it should "create a minimal policy with one key" in { + val kh = KeyHash[Verification]("somestring") + val p = PolicyBuilder().withSignatureOf(kh).build + p.scripts.toList shouldBe List(Signature(kh)) + p.kind shouldBe Policy.Kind.All + } + + it should "respect the `kind`" in { + val kh = KeyHash[Verification]("somestring") + var p = PolicyBuilder().withSignatureOf(kh).withAllSigsRequired().build + + p.scripts.toList shouldBe List(Signature(kh)) + p.kind shouldBe Policy.Kind.All + + p = PolicyBuilder().withSignatureOf(kh).withAnySigRequired().build + + p.scripts.toList shouldBe List(Signature(kh)) + p.kind shouldBe Policy.Kind.Any + + p = PolicyBuilder().withSignatureOf(kh).withAtLeastSigsRequired(2).build + + p.scripts.toList shouldBe List(Signature(kh)) + p.kind shouldBe Policy.Kind.AtLeast(2) + } + + it should "aggregrate signatures" in { + val kh = KeyHash[Verification]("somestring") + val kh2 = KeyHash[Verification]("somestring2") + val p = PolicyBuilder() + .withSignatureOf(kh) + .withSignatureOf(kh2) + .build + + p.scripts.toList should contain (Signature(kh)) + p.scripts.toList should contain (Signature(kh2)) + + } + + it should "respect slot bounds" in { + val kh = KeyHash[Verification]("somestring") + + val p = PolicyBuilder() + .withSignatureOf(kh) + .withAfterConstraint(400) + .withBeforeConstraint(800) + .build + + p.scripts.toList should contain (Signature(kh)) + p.scripts.toList should contain (Bound(400, after = true)) + p.scripts.toList should contain (Bound(800, after = false)) + p.scripts.toList.size shouldBe(3) + } + + it should "only allow one before and one after slot bound" in { + val kh = KeyHash[Verification]("somestring") + + val p = PolicyBuilder() + .withSignatureOf(kh) + .withAfterConstraint(400) + .withAfterConstraint(200) + .withBeforeConstraint(800) + .withBeforeConstraint(900) + .build + + p.scripts.toList should contain (Signature(kh)) + p.scripts.toList should contain (Bound(200, after = true)) + p.scripts.toList should contain (Bound(900, after = false)) + p.scripts.toList.size shouldBe(3) + } + +} diff --git a/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicySpec.scala b/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicySpec.scala index 9103984..3c92bea 100644 --- a/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicySpec.scala +++ b/src/test/scala/iog/psg/cardano/experimental/cli/model/PolicySpec.scala @@ -3,13 +3,15 @@ package iog.psg.cardano.experimental.cli.model import cats.data.NonEmptyList import io.circe.Json import io.circe.syntax._ +import iog.psg.cardano.experimental.cli.api.Verification +import iog.psg.cardano.experimental.cli.model.Policy.Script.{Bound, Signature} import iog.psg.cardano.experimental.cli.model.Policy.{Kind, Script, asString} import iog.psg.cardano.experimental.cli.util.RandomTempFolder -import org.scalatest.Assertion +import org.scalatest.{Assertion, EitherValues} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -class PolicySpec extends AnyFlatSpec with Matchers { +class PolicySpec extends AnyFlatSpec with Matchers with EitherValues { "Policy" should "be properly json encoded" in { @@ -34,4 +36,23 @@ class PolicySpec extends AnyFlatSpec with Matchers { List(Policy.Kind.All, Policy.Kind.AtLeast(10), Policy.Kind.Any).foreach(assert) } + + "A policy added by string" should "parse correctly" in { + + val keyHash = "3e6ea29f537e0783f469077723b9ef6f740993e84f616db36ad355a9" + val slot = 56894689 + + val dish = + s"""{\n \"type\": \"all\",\n \"scripts\":\n [\n {\n \"type\": \"after\",\n \"slot\": ${slot}\n },\n {\n \"type\": \"sig\",\n \"keyHash\": \"${keyHash}\"\n }\n ]\n }""" + implicit val dummy: RandomTempFolder = RandomTempFolder(null) + + val policyOrError = Policy.fromString(dish) + + val policy = policyOrError.value + + policy.kind shouldBe Policy.Kind.All + policy.scripts.toList should contain (Signature(KeyHash[Verification](keyHash))) + policy.scripts.toList should contain (Bound(slot, after = true)) + policy.scripts.toList.size shouldBe 2 + } }