From c6800a1fc77879874a500f5384ebe2ff3b61b1fe Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 30 Mar 2019 11:49:31 -0700 Subject: [PATCH] server: Add the TPoSContract model --- .../xsn/explorer/models/TPoSContract.scala | 68 +++++++++++++++++++ .../explorer/models/TPoSContractSpec.scala | 50 ++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 server/app/com/xsn/explorer/models/TPoSContract.scala create mode 100644 server/test/com/xsn/explorer/models/TPoSContractSpec.scala diff --git a/server/app/com/xsn/explorer/models/TPoSContract.scala b/server/app/com/xsn/explorer/models/TPoSContract.scala new file mode 100644 index 0000000..ae8f002 --- /dev/null +++ b/server/app/com/xsn/explorer/models/TPoSContract.scala @@ -0,0 +1,68 @@ +package com.xsn.explorer.models + +import com.xsn.explorer.models.values.{Address, TransactionId} +import enumeratum._ + +import scala.util.Try + +case class TPoSContract( + id: TPoSContract.Id, + details: TPoSContract.Details, + time: Long, + state: TPoSContract.State) { + + val txid: TransactionId = id.txid +} + +object TPoSContract { + + case class Id(txid: TransactionId, index: Int) + class Commission private (val int: Int) extends AnyVal + object Commission { + + val range = 1 until 100 + + def from(int: Int): Option[Commission] = { + if (range contains int) Some(new Commission(int)) + else None + } + } + + case class Details(owner: Address, merchant: Address, merchantCommission: Commission) + object Details { + + /** + * Try to get the contract details from the output script ASM. + * + * expected format: + * - "OP_RETURN [hex_encoded_owner_address] [hex_encoded_merchant_address] [owner_commission] [signature] + * + * example: + * - "OP_RETURN 5869337351664d51737932437a4d5a54726e4b573648464770315671465468644c77 58794a4338786e664672484e634d696e68366778755052595939484361593944416f 99" + */ + def fromOutputScriptASM(asm: String): Option[Details] = { + val parts = asm.split(" ").toList + + parts match { + case op :: owner :: merchant :: commission :: signature :: Nil if op == "OP_RETURN" => + for { + ownerAddress <- Address.fromHex(owner) + merchantAddress <- Address.fromHex(merchant) + ownerCommission <- Try(commission.toInt).toOption + merchantCommission <- TPoSContract.Commission.from(100 - ownerCommission) + } yield Details(owner = ownerAddress, merchant = merchantAddress, merchantCommission = merchantCommission) + + case _ => None + } + } + } + + sealed abstract class State(override val entryName: String) extends EnumEntry + object State extends Enum[State] { + + val values = findValues + + final case object Active extends State("ACTIVE") + final case object Closed extends State("CLOSED") + } +} diff --git a/server/test/com/xsn/explorer/models/TPoSContractSpec.scala b/server/test/com/xsn/explorer/models/TPoSContractSpec.scala new file mode 100644 index 0000000..9cd9f9c --- /dev/null +++ b/server/test/com/xsn/explorer/models/TPoSContractSpec.scala @@ -0,0 +1,50 @@ +package com.xsn.explorer.models + +import com.xsn.explorer.models.values.Address +import javax.xml.bind.DatatypeConverter +import org.scalatest.MustMatchers._ +import org.scalatest.OptionValues._ +import org.scalatest.WordSpec + +class TPoSContractSpec extends WordSpec { + + val address1 = Address.from("Xi3sQfMQsy2CzMZTrnKW6HFGp1VqFThdLw").get + val address2 = Address.from("XyJC8xnfFrHNcMinh6gxuPRYY9HCaY9DAo").get + val address1Hex = DatatypeConverter.printHexBinary(address1.string.getBytes()) + val address2Hex = DatatypeConverter.printHexBinary(address2.string.getBytes()) + val commission = "99" + val signature = "1f60a6a385a4e5163ffef65dd873f17452bb0d9f89da701ffcc5a0f72287273c0571485c29123fef880d2d8169cfdb884bf95a18a0b36461517acda390ce4cf441" + + val failureCases = Map( + "fail when the signature is missing" -> s"OP_RETURN $address1Hex $address2Hex $commission", + "fail when there is an extra field" -> s"OP_RETURN $address1Hex $address2Hex $commission $signature $signature", + "fail if OP_RETURN is not present" -> s"OP_RTURN $address1Hex $address2Hex $commission $signature", + "fail if the commission is missing" -> s"OP_RETURN $address1Hex $address2Hex $signature", + "fail if the commission is corrupted" -> s"OP_RETURN $address1Hex $address2Hex $commission$commission $signature", + "fail if the commission is 0" -> s"OP_RETURN $address1Hex $address2Hex 0 $signature", + "fail if the commission is 100" -> s"OP_RETURN $address1Hex $address2Hex 100 $signature", + "fail if the owner address is malformed" -> s"OP_RETURN x$address1Hex $address2Hex $commission $signature", + "fail if the merchant address is malformed" -> s"OP_RETURN x$address1Hex $address2Hex $commission $signature" + ) + + "parsing details" should { + "succeed on a valid contract" in { + val asm = s"OP_RETURN $address1Hex $address2Hex $commission $signature" + + val expected = TPoSContract.Details( + owner = address1, + merchant = address2, + merchantCommission = TPoSContract.Commission.from(100 - commission.toInt).get) + + val result = TPoSContract.Details.fromOutputScriptASM(asm) + result.value must be(expected) + } + + failureCases.foreach { case (test, input) => + test in { + val result = TPoSContract.Details.fromOutputScriptASM(input) + result must be(empty) + } + } + } +}