diff --git a/LICENSE b/LICENSE
index 7a1866a..dc5d448 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2018 0bsNetwork
+Copyright (c) 2019 0bsNetwork
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 6a3c9ca..52e667a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-# Zbs [![Build Status](https://travis-ci.org/0bsnetwork/Zbs.svg?branch=master)](https://travis-ci.org/0bsnetwork/Zbs)
-
+# 0bsNetwork Full Node
In the master branch there is a code with functions that is under development. The latest release for each network can be found in the [Releases section](https://github.com/0bsnetwork/Zbs/releases), you can switch to the corresponding tag and build the application.
@@ -48,10 +47,13 @@ Mutiple features can be seperated by commas;
features = [9,10,11]
```
-Note: Features < 9 have been pre-activated on this current node version.
+Note: Features <= 11 have been pre-activated on the chain.
# Tests & Coverage
```
- sbt -J-XX:MaxMetaspaceSize=512M -J-XX:MetaspaceSize=512M -J-Xms2048M -J-Xmx2048M -J-Xss6M -J-XX:MaxPermSize=512M ";coverage;checkPR;coverageReport"
+unset _JAVA_OPTIONS
+unset SBT_OPTS
+export JAVA_TOOL_OPTIONS="-Xmx1548m"
+sbt -J-Xms128m -J-Xmx1248m -J-XX:+UseConcMarkSweepGC -J-XX:+CMSClassUnloadingEnabled ";coverage;checkPR;coverageReport"
```
\ No newline at end of file
diff --git a/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala b/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala
new file mode 100644
index 0000000..a3efbdb
--- /dev/null
+++ b/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala
@@ -0,0 +1,75 @@
+package com.zbsnetwork.serialization.protobuf
+
+import java.util.concurrent.TimeUnit
+
+import com.zbsnetwork.account.PublicKeyAccount
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.Base58
+import com.zbsnetwork.protobuf.transaction.PBTransactions
+import com.zbsnetwork.transaction.Proofs
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction.Transfer
+import org.openjdk.jmh.annotations._
+import org.openjdk.jmh.infra.Blackhole
+
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@BenchmarkMode(Array(Mode.AverageTime))
+@Threads(1)
+@Fork(1)
+@Warmup(iterations = 10)
+@Measurement(iterations = 10)
+class ProtoBufBenchmark {
+
+ @Benchmark
+ def serializeMassTransferPB_test(bh: Blackhole): Unit = {
+ val vanillaTx = {
+ val transfers = MassTransferTransaction
+ .parseTransfersList(
+ List(Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 100000000L), Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 200000000L)))
+ .right
+ .get
+
+ MassTransferTransaction
+ .create(
+ None,
+ PublicKeyAccount.fromBase58String("FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z").right.get,
+ transfers,
+ 1518091313964L,
+ 200000,
+ Base58.decode("59QuUcqP6p").get,
+ Proofs(Seq(ByteStr.decodeBase58("FXMNu3ecy5zBjn9b69VtpuYRwxjCbxdkZ3xZpLzB8ZeFDvcgTkmEDrD29wtGYRPtyLS3LPYrL2d5UM6TpFBMUGQ").get))
+ )
+ .right
+ .get
+ }
+
+ val tx = PBTransactions.protobuf(vanillaTx)
+ bh.consume(tx.toByteArray)
+ }
+
+ @Benchmark
+ def serializeMassTransferVanilla_test(bh: Blackhole): Unit = {
+ val vanillaTx = {
+ val transfers = MassTransferTransaction
+ .parseTransfersList(
+ List(Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 100000000L), Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 200000000L)))
+ .right
+ .get
+
+ MassTransferTransaction
+ .create(
+ None,
+ PublicKeyAccount.fromBase58String("FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z").right.get,
+ transfers,
+ 1518091313964L,
+ 200000,
+ Base58.decode("59QuUcqP6p").get,
+ Proofs(Seq(ByteStr.decodeBase58("FXMNu3ecy5zBjn9b69VtpuYRwxjCbxdkZ3xZpLzB8ZeFDvcgTkmEDrD29wtGYRPtyLS3LPYrL2d5UM6TpFBMUGQ").get))
+ )
+ .right
+ .get
+ }
+
+ bh.consume(vanillaTx.bytes())
+ }
+}
diff --git a/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala b/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala
index 9e18f02..6ff03ab 100644
--- a/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala
+++ b/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala
@@ -11,6 +11,7 @@ import com.zbsnetwork.database.LevelDBWriter
import com.zbsnetwork.db.LevelDBFactory
import com.zbsnetwork.settings.{ZbsSettings, loadConfig}
import com.zbsnetwork.state.LevelDBWriterBenchmark._
+import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.utils.Implicits.SubjectOps
import monix.reactive.subjects.Subject
import org.iq80.leveldb.{DB, Options}
@@ -91,8 +92,10 @@ object LevelDBWriterBenchmark {
LevelDBFactory.factory.open(dir, new Options)
}
- private val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address]
- val db = new LevelDBWriter(rawDB, ignorePortfolioChanged, zbsSettings.blockchainSettings.functionalitySettings, 100000, 2000, 120 * 60 * 1000)
+ private val ignoreSpendableBalanceChanged = Subject.empty[(Address, Option[AssetId])]
+
+ val db =
+ new LevelDBWriter(rawDB, ignoreSpendableBalanceChanged, zbsSettings.blockchainSettings.functionalitySettings, 100000, 2000, 120 * 60 * 1000)
@TearDown
def close(): Unit = {
diff --git a/build.sbt b/build.sbt
index 0bfdd9f..dd0fb66 100644
--- a/build.sbt
+++ b/build.sbt
@@ -15,7 +15,7 @@ val versionSource = Def.task {
// Please, update the fallback version every major and minor releases.
// This version is used then building from sources without Git repository
// In case of not updating the version nodes build from headless sources will fail to connect to newer versions
- val FallbackVersion = (0, 16, 0)
+ val FallbackVersion = (0, 16, 2)
val versionFile = (sourceManaged in Compile).value / "com" / "zbsnetwork" / "Version.scala"
val versionExtractor = """(\d+)\.(\d+)\.(\d+).*""".r
@@ -41,7 +41,7 @@ name := "zbs"
normalizedName := s"${name.value}${network.value.packageSuffix}"
git.useGitDescribe := true
-git.uncommittedSignifier := Some("")
+git.uncommittedSignifier := Some("MT")
logBuffered := false
inThisBuild(
@@ -49,7 +49,13 @@ inThisBuild(
scalaVersion := "2.12.8",
organization := "com.zbsnetwork",
crossPaths := false,
- scalacOptions ++= Seq("-feature", "-deprecation", "-language:higherKinds", "-language:implicitConversions", "-Ywarn-unused:-implicits", "-Xlint")
+ scalacOptions ++= Seq("-feature",
+ "-deprecation",
+ "-language:higherKinds",
+ "-language:implicitConversions",
+ "-Ywarn-unused:-implicits",
+ "-Xlint",
+ "-Ywarn-unused-import")
))
resolvers ++= Seq(
@@ -67,7 +73,7 @@ val java9Options = Seq(
fork in run := true
javaOptions in run ++= java9Options
-Test / fork := true
+Test / fork := false
Test / javaOptions ++= java9Options
Jmh / javaOptions ++= java9Options
@@ -110,7 +116,9 @@ inConfig(Compile)(
mainClass := Some("com.zbsnetwork.Application"),
publishArtifact in packageDoc := false,
publishArtifact in packageSrc := false,
- sourceGenerators += versionSource
+ sourceGenerators += versionSource,
+ PB.targets += scalapb.gen(flatPackage = true) -> (sourceManaged in Compile).value,
+ PB.deleteTargetDirectory := false
))
inConfig(Test)(
@@ -213,8 +221,8 @@ def allProjects: List[ProjectReference] = ReflectUtilities.allVals[Project](this
addCommandAlias(
"checkPR",
+ // set scalacOptions in ThisBuild ++= Seq("-Xfatal-warnings");
""";
- |set scalacOptions in ThisBuild ++= Seq("-Xfatal-warnings");
|Global / checkPRRaw;
|set scalacOptions in ThisBuild -= "-Xfatal-warnings";
""".stripMargin
@@ -233,6 +241,7 @@ checkPRRaw in Global := {
lazy val common = crossProject(JSPlatform, JVMPlatform)
.withoutSuffixFor(JVMPlatform)
+ .disablePlugins(ProtocPlugin)
.settings(
libraryDependencies ++= Dependencies.scalatest
)
@@ -243,6 +252,7 @@ lazy val commonJVM = common.jvm
lazy val lang =
crossProject(JSPlatform, JVMPlatform)
.withoutSuffixFor(JVMPlatform)
+ .disablePlugins(ProtocPlugin)
.settings(
version := "1.0.0",
coverageExcludedPackages := ".*",
@@ -308,7 +318,7 @@ lazy val node = project
Dependencies.http ++
Dependencies.akka ++
Dependencies.serialization ++
- Dependencies.testKit.map(_ % "test") ++
+ Dependencies.testKit.map(_ % Test) ++
Dependencies.logging ++
Dependencies.matcher ++
Dependencies.metrics ++
@@ -317,11 +327,18 @@ lazy val node = project
Dependencies.ficus ++
Dependencies.scorex ++
Dependencies.commons_net ++
- Dependencies.monix.value
+ Dependencies.monix.value ++
+ Dependencies.protobuf.value ++
+ Dependencies.grpc,
+ dependencyOverrides ++= Seq(
+ Dependencies.AkkaActor,
+ Dependencies.AkkaStream,
+ Dependencies.AkkaHTTP
+ )
)
.dependsOn(langJVM, commonJVM)
-lazy val discovery = project
+///lazy val discovery = project
lazy val it = project
.dependsOn(node)
diff --git a/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala b/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala
index 5f27493..2faf824 100644
--- a/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala
+++ b/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala
@@ -5,14 +5,6 @@ import com.zbsnetwork.common.utils.{Base58, Base64}
import scala.util.Try
case class ByteStr(arr: Array[Byte]) {
-
- override def equals(a: Any): Boolean = a match {
- case other: ByteStr => arr.sameElements(other.arr)
- case _ => false
- }
-
- override def hashCode(): Int = java.util.Arrays.hashCode(arr)
-
lazy val base58: String = Base58.encode(arr)
lazy val base64: String = "base64:" + Base64.encode(arr)
@@ -52,23 +44,27 @@ case class ByteStr(arr: Array[Byte]) {
def dropRight(n: Long): ByteStr = take(arr.length.toLong - n.max(0))
+ override def equals(a: Any): Boolean = a match {
+ case other: ByteStr => arr.sameElements(other.arr)
+ case _ => false
+ }
+
+ override def hashCode(): Int = java.util.Arrays.hashCode(arr)
}
object ByteStr {
-
val empty: ByteStr = ByteStr(Array.emptyByteArray)
- def fromBytes(bytes: Byte*): ByteStr = {
-
- val buf = new Array[Byte](bytes.size)
- var i = 0
+ implicit def fromByteArray(arr: Array[Byte]): ByteStr = {
+ new ByteStr(arr)
+ }
- bytes.foreach { b =>
- buf(i) = b
- i += 1
- }
+ implicit def toByteArray(bs: ByteStr): Array[Byte] = {
+ bs.arr
+ }
- ByteStr(buf)
+ def fromBytes(bytes: Byte*): ByteStr = {
+ ByteStr(bytes.toArray)
}
def fromLong(l: Long): ByteStr = {
diff --git a/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala b/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala
index 3aa7775..b62f0e8 100644
--- a/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala
+++ b/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala
@@ -1,12 +1,20 @@
package com.zbsnetwork.common
+import scala.util.{Failure, Success, Try}
package object utils {
implicit class EitherExt2[A, B](ei: Either[A, B]) {
+
def explicitGet(): B = ei match {
case Left(value) => throw new Exception(value.toString)
case Right(value) => value
}
- }
+ def foldToTry: Try[B] = {
+ ei.fold(
+ left => Failure(new Exception(left.toString)),
+ right => Success(right)
+ )
+ }
+ }
}
diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala
index abf628f..84872c3 100644
--- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala
+++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala
@@ -3,11 +3,11 @@ package com.zbsnetwork.dexgen
import java.util.concurrent.Executors
import cats.implicits.showInterpolator
-import cats.instances.byte
import com.typesafe.config.ConfigFactory
import com.zbsnetwork.account.{AddressOrAlias, AddressScheme, PrivateKeyAccount}
import com.zbsnetwork.api.http.assets.{SignedIssueV2Request, SignedMassTransferRequest}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.dexgen.cli.ScoptImplicits
import com.zbsnetwork.dexgen.config.FicusImplicits
import com.zbsnetwork.dexgen.utils.{ApiRequests, GenOrderType}
@@ -19,7 +19,6 @@ import com.zbsnetwork.transaction.assets.IssueTransactionV2
import com.zbsnetwork.transaction.transfer.MassTransferTransaction
import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer
import com.zbsnetwork.utils.LoggerFacade
-import com.zbsnetwork.common.utils.EitherExt2
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.ceedubs.ficus.readers.{EnumerationReader, NameMapper}
@@ -79,6 +78,7 @@ object DexGenApp extends App with ScoptImplicits with FicusImplicits with Enumer
def issueAssets(endpoint: String, richAddressSeed: String, n: Int)(implicit tag: String): Seq[AssetId] = {
val node = api.to(endpoint)
+ val now = System.currentTimeMillis()
val assetsTx: Seq[IssueTransactionV2] = (1 to n).map { i =>
IssueTransactionV2
.selfSigned(
@@ -89,7 +89,7 @@ object DexGenApp extends App with ScoptImplicits with FicusImplicits with Enumer
decimals = 2,
reissuable = false,
fee = 100000000,
- timestamp = System.currentTimeMillis(),
+ timestamp = now + i,
sender = PrivateKeyAccount.fromSeed(richAddressSeed).explicitGet(),
script = None
)
diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala
index 89a48ba..cba2f65 100644
--- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala
+++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala
@@ -122,7 +122,7 @@ class Worker(workerSettings: Settings,
implicit val signedTransferRequestWrites: Writes[SignedTransferV1Request] =
Json.writes[SignedTransferV1Request].transform((jsobj: JsObject) => jsobj + ("type" -> JsNumber(TransferTransactionV1.typeId.toInt)))
- def transfer(sender: PrivateKeyAccount, assetId: Option[AssetId], recipient: PrivateKeyAccount, halfBalance: Boolean)(
+ def transfer(i: Long, sender: PrivateKeyAccount, assetId: Option[AssetId], recipient: PrivateKeyAccount, halfBalance: Boolean)(
implicit tag: String): Future[Transaction] =
to(endpoint).balance(sender.address, assetId).flatMap { balance =>
val halfAmount = if (halfBalance) balance / 2 else balance
@@ -132,7 +132,7 @@ class Worker(workerSettings: Settings,
sender,
AddressOrAlias.fromString(PublicKeyAccount(recipient.publicKey).address).right.get,
transferAmount,
- now,
+ now + i,
None,
fee,
Array.emptyByteArray) match {
@@ -147,7 +147,7 @@ class Worker(workerSettings: Settings,
}
}
- def send(orderType: GenOrderType.Value): Future[Any] = {
+ def send(i: Long, orderType: GenOrderType.Value): Future[Any] = {
implicit val tag: String = s"$orderType, ${Random.nextInt(1, 1000000)}"
val tradingAssetsSize = tradingAssets.size
@@ -196,9 +196,9 @@ class Worker(workerSettings: Settings,
for {
_ <- cancelAllOrders(fakeAccounts)
_ <- sellOrder(DefaultAmount, DefaultPrice, seller, pair)._2
- _ <- transfer(seller, pair.amountAsset, buyer, halfBalance = false)
+ _ <- transfer(i, seller, pair.amountAsset, buyer, halfBalance = false)
_ <- buyOrder(DefaultAmount, DefaultPrice, buyer, pair)._2
- _ <- transfer(buyer, pair.amountAsset, seller, halfBalance = true)
+ _ <- transfer(i, buyer, pair.amountAsset, seller, halfBalance = true)
_ <- cancelAllOrders(fakeAccounts)
} yield ()
@@ -209,9 +209,9 @@ class Worker(workerSettings: Settings,
for {
_ <- cancelAllOrders(fakeAccounts)
_ <- buyOrder(DefaultAmount, DefaultPrice, buyer, pair)._2
- _ <- transfer(buyer, pair.amountAsset, seller, halfBalance = false)
+ _ <- transfer(i, buyer, pair.amountAsset, seller, halfBalance = false)
_ <- sellOrder(DefaultAmount, DefaultPrice, seller, pair)._2
- _ <- transfer(seller, pair.amountAsset, buyer, halfBalance = true)
+ _ <- transfer(i, seller, pair.amountAsset, buyer, halfBalance = true)
} yield ()
}
@@ -220,22 +220,22 @@ class Worker(workerSettings: Settings,
}
}
- private def serial(times: Int)(f: => Future[Any]): Future[Unit] = {
- def loop(rest: Int, acc: Future[Unit]): Future[Unit] = {
+ private def serial(times: Int)(f: Int => Future[Any]): Future[Unit] = {
+ def loop(rest: Int, i: Int, acc: Future[Unit]): Future[Unit] = {
if (rest <= 0) acc
else {
- val newAcc = acc.flatMap(_ => f).map(_ => ())
- loop(rest - 1, newAcc)
+ val newAcc = acc.flatMap(_ => f(i)).map(_ => ())
+ loop(rest - 1, i + 1, newAcc)
}
}
- loop(times, Future.successful(()))
+ loop(times, 0, Future.successful(()))
}
private def placeOrders(maxIterations: Int): Future[Unit] = {
def sendAll(step: Int): Future[Unit] = {
log.info(s"Step $step")
- serial(ordersCount)(send(orderType)) // @TODO Should work in parallel, but now it leads to invalid transfers
+ serial(ordersCount)(send(_, orderType)) // @TODO Should work in parallel, but now it leads to invalid transfers
}
def runStepsFrom(step: Int): Future[Unit] = sendAll(step).flatMap { _ =>
diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala
index 6810f36..1985316 100644
--- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala
+++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala
@@ -21,12 +21,14 @@ object Gen {
}
def transfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], feeGen: Iterator[Long]): Iterator[Transaction] = {
+ val now = System.currentTimeMillis()
senderGen
.zip(recipientGen)
.zip(feeGen)
+ .zipWithIndex
.map {
- case ((src, dst), fee) =>
- TransferTransactionV1.selfSigned(None, src, dst, fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray)
+ case (((src, dst), fee), i) =>
+ TransferTransactionV1.selfSigned(None, src, dst, fee, now + i, None, fee, Array.emptyByteArray)
}
.collect { case Right(x) => x }
}
diff --git a/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala
index f94d8fe..1a218a0 100644
--- a/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala
+++ b/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala
@@ -30,19 +30,12 @@ class MultisigTransactionGenerator(settings: MultisigTransactionGenerator.Settin
val script: Script = Gen.multiSigScript(owners, 3)
- val setScript = SetScriptTransaction.selfSigned(bank, Some(script), enoughFee, System.currentTimeMillis()).explicitGet()
+ val now = System.currentTimeMillis()
+ val setScript = SetScriptTransaction.selfSigned(bank, Some(script), enoughFee, now).explicitGet()
val res = Range(0, settings.transactions).map { i =>
val tx = TransferTransactionV2
- .create(None,
- bank,
- owners(1),
- totalAmountOnNewAccount - 2 * enoughFee - i,
- System.currentTimeMillis(),
- None,
- enoughFee,
- Array.emptyByteArray,
- Proofs.empty)
+ .create(None, bank, owners(1), totalAmountOnNewAccount - 2 * enoughFee - i, now + i, None, enoughFee, Array.emptyByteArray, Proofs.empty)
.explicitGet()
val signatures = owners.map(crypto.sign(_, tx.bodyBytes())).map(ByteStr(_))
tx.copy(proofs = Proofs(signatures))
diff --git a/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala
index 1e88cb8..1339fad 100644
--- a/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala
+++ b/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala
@@ -43,6 +43,8 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe
def generate(n: Int): Seq[Transaction] = {
val issueTransactionSender = randomFrom(accounts).get
+
+ val now = System.currentTimeMillis()
val tradeAssetIssue = IssueTransactionV1
.selfSigned(
issueTransactionSender,
@@ -52,7 +54,7 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe
2,
reissuable = false,
100000000L + r.nextInt(100000000),
- System.currentTimeMillis()
+ now
)
.right
.get
@@ -81,10 +83,10 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe
Seq.empty[LeaseTransactionV1],
Seq.empty[CreateAliasTransaction]
)) {
- case ((allTxsWithValid, validIssueTxs, reissuableIssueTxs, activeLeaseTransactions, aliases), _) =>
+ case ((allTxsWithValid, validIssueTxs, reissuableIssueTxs, activeLeaseTransactions, aliases), i) =>
def moreThatStandartFee = 100000L + r.nextInt(100000)
- def ts = System.currentTimeMillis()
+ val ts = now + i
val tx = typeGen.getRandom match {
case IssueTransactionV1 =>
diff --git a/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala
index a851e06..7b9870b 100644
--- a/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala
+++ b/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala
@@ -32,13 +32,12 @@ class OracleTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe
.selfSigned(oracle, settings.requiredData.toList, enoughFee, System.currentTimeMillis())
.explicitGet()
- val transactions: List[Transaction] =
- List
- .fill(settings.transactions) {
- TransferTransactionV2
- .selfSigned(None, scriptedAccount, oracle, 1.zbs, System.currentTimeMillis(), None, enoughFee, Array.emptyByteArray)
- .explicitGet()
- }
+ val now = System.currentTimeMillis()
+ val transactions: List[Transaction] = (1 to settings.transactions).map { i =>
+ TransferTransactionV2
+ .selfSigned(None, scriptedAccount, oracle, 1.zbs, now + i, None, enoughFee, Array.emptyByteArray)
+ .explicitGet()
+ }.toList
setScript +: setDataTx +: transactions
}
diff --git a/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala
index 24237bb..24b6273 100644
--- a/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala
+++ b/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala
@@ -20,8 +20,6 @@ class SmartGenerator(settings: SmartGenerator.Settings, val accounts: Seq[Privat
private def r = ThreadLocalRandom.current
private def randomFrom[T](c: Seq[T]): Option[T] = if (c.nonEmpty) Some(c(r.nextInt(c.size))) else None
- def ts = System.currentTimeMillis()
-
override def next(): Iterator[Transaction] = {
generate(settings).toIterator
}
@@ -38,13 +36,16 @@ class SmartGenerator(settings: SmartGenerator.Settings, val accounts: Seq[Privat
SetScriptTransaction.selfSigned(i, Some(script), 1.zbs, System.currentTimeMillis()).explicitGet()
})
+ val now = System.currentTimeMillis()
val txs = Range(0, settings.transfers).map { i =>
TransferTransactionV2
- .selfSigned(None, bank, bank, 1.zbs - 2 * fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray)
+ .selfSigned(None, bank, bank, 1.zbs - 2 * fee, now + i, None, fee, Array.emptyByteArray)
.explicitGet()
}
val extxs = Range(0, settings.exchange).map { i =>
+ val ts = now + i
+
val matcher = randomFrom(accounts).get
val seller = randomFrom(accounts).get
val buyer = randomFrom(accounts).get
diff --git a/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala b/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala
index 415ff6b..917be92 100644
--- a/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala
+++ b/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala
@@ -112,25 +112,30 @@ object Gen {
}
def transfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], feeGen: Iterator[Long]): Iterator[Transaction] = {
+ val now = System.currentTimeMillis()
+
senderGen
.zip(recipientGen)
.zip(feeGen)
+ .zipWithIndex
.map {
- case ((src, dst), fee) =>
- TransferTransactionV1.selfSigned(None, src, dst, fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray)
+ case (((src, dst), fee), i) =>
+ TransferTransactionV1.selfSigned(None, src, dst, fee, now + i, None, fee, Array.emptyByteArray)
}
.collect { case Right(x) => x }
}
def massTransfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], amountGen: Iterator[Long]): Iterator[Transaction] = {
+ val now = System.currentTimeMillis()
val transferCountGen = Iterator.continually(random.nextInt(MassTransferTransaction.MaxTransferCount + 1))
senderGen
.zip(transferCountGen)
+ .zipWithIndex
.map {
- case (sender, count) =>
+ case ((sender, count), i) =>
val transfers = List.tabulate(count)(_ => ParsedTransfer(recipientGen.next(), amountGen.next()))
val fee = 100000 + count * 50000
- MassTransferTransaction.selfSigned(None, sender, transfers, System.currentTimeMillis, fee, Array.emptyByteArray)
+ MassTransferTransaction.selfSigned(None, sender, transfers, now + i, fee, Array.emptyByteArray)
}
.collect { case Right(tx) => tx }
}
diff --git a/it/build.sbt b/it/build.sbt
index fb6c299..ecbb10c 100644
--- a/it/build.sbt
+++ b/it/build.sbt
@@ -22,7 +22,7 @@ inTask(docker)(
val withAspectJ = Option(System.getenv("WITH_ASPECTJ")).fold(false)(_.toBoolean)
val aspectjAgentUrl = "http://search.maven.org/remotecontent?filepath=org/aspectj/aspectjweaver/1.9.1/aspectjweaver-1.9.1.jar"
- val yourKitArchive = "YourKit-JavaProfiler-2018.04-docker.zip"
+ val yourKitArchive = "YourKit-JavaProfiler-2019.1-docker.zip"
new Dockerfile {
from("anapsix/alpine-java:8_server-jre")
@@ -31,9 +31,9 @@ inTask(docker)(
// Install YourKit
runRaw(s"""apk update && \\
|apk add --no-cache openssl ca-certificates && \\
- |wget https://www.yourkit.com/download/docker/$yourKitArchive -P /tmp/ && \\
+ |wget --quiet https://www.yourkit.com/download/docker/$yourKitArchive -P /tmp/ && \\
|unzip /tmp/$yourKitArchive -d /usr/local && \\
- |rm /tmp/$yourKitArchive""".stripMargin)
+ |rm -f /tmp/$yourKitArchive""".stripMargin)
if (withAspectJ) run("wget", "--quiet", aspectjAgentUrl, "-O", "/opt/zbs/aspectjweaver.jar")
diff --git a/it/src/main/resources/template.conf b/it/src/main/resources/template.conf
index 4c07f5a..cd0b18b 100644
--- a/it/src/main/resources/template.conf
+++ b/it/src/main/resources/template.conf
@@ -39,6 +39,7 @@ zbs {
9 = 0
10 = 0
11 = 0
+ 12 = 0
}
double-features-periods-after-height = 100000000
max-transaction-time-back-offset = 120m
diff --git a/it/src/main/scala/com/zbsnetwork/it/Docker.scala b/it/src/main/scala/com/zbsnetwork/it/Docker.scala
index 801bfeb..6ecd22a 100644
--- a/it/src/main/scala/com/zbsnetwork/it/Docker.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/Docker.scala
@@ -236,8 +236,9 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
s"-Dlogback.stdout.level=TRACE -Dlogback.file.level=OFF -Dzbs.network.declared-address=$ip:$networkPort $ntpServer $maxCacheSize $kafkaServer"
if (enableProfiling) {
- config += s"-agentpath:/usr/local/YourKit-JavaProfiler-2018.04/bin/linux-x86-64/libyjpagent.so=port=$ProfilerPort,listen=all," +
- s"sampling,monitors,sessionname=ZbsNode,dir=$ContainerRoot/profiler,logdir=$ContainerRoot "
+ // https://www.yourkit.com/docs/java/help/startup_options.jsp
+ config += s"-agentpath:/usr/local/YourKit-JavaProfiler-2019.1/bin/linux-x86-64/libyjpagent.so=port=$ProfilerPort,listen=all," +
+ s"sampling,monitors,sessionname=ZbsNode,dir=$ContainerRoot/profiler,logdir=$ContainerRoot,onexit=snapshot "
}
val withAspectJ = Option(System.getenv("WITH_ASPECTJ")).fold(false)(_.toBoolean)
@@ -313,7 +314,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
def stopContainer(node: DockerNode): Unit = {
val id = node.containerId
log.info(s"Stopping container with id: $id")
- takeProfileSnapshot(node)
client.stopContainer(node.containerId, 10)
saveProfile(node)
saveLog(node)
@@ -328,7 +328,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
def killAndStartContainer(node: DockerNode): DockerNode = {
val id = node.containerId
log.info(s"Killing container with id: $id")
- takeProfileSnapshot(node)
client.killContainer(id, DockerClient.Signal.SIGINT)
saveProfile(node)
saveLog(node)
@@ -364,8 +363,8 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
log.info("Stopping containers")
nodes.asScala.foreach { node =>
- takeProfileSnapshot(node)
- client.stopContainer(node.containerId, 0)
+ client.stopContainer(node.containerId, if (enableProfiling) 60 else 0)
+ log.debug(s"Container ${node.name} stopped with exit status: ${client.waitContainer(node.containerId).statusCode()}")
saveProfile(node)
saveLog(node)
@@ -416,28 +415,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
}
}
- private def takeProfileSnapshot(node: DockerNode): Unit = if (enableProfiling) {
- val task = client.execCreate(
- node.containerId,
- Array(
- "java",
- "-jar",
- ProfilerController.toString,
- "127.0.0.1",
- ProfilerPort.toString,
- "capture-performance-snapshot"
- ),
- DockerClient.ExecCreateParam.attachStdout(),
- DockerClient.ExecCreateParam.attachStderr()
- )
- Option(task.warnings()).toSeq.flatMap(_.asScala).foreach(log.warn(_))
- client.execStart(task.id())
- while (client.execInspect(task.id()).running()) {
- log.trace(s"Snapshot of ${node.name} has not been took yet, wait...")
- blocking(Thread.sleep(1000))
- }
- }
-
private def saveProfile(node: DockerNode): Unit = if (enableProfiling) {
try {
val profilerDirStream = client.archiveContainer(node.containerId, ContainerRoot.resolve("profiler").toString)
@@ -542,7 +519,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
def runMigrationToolInsideContainer(node: DockerNode): DockerNode = {
val id = node.containerId
- takeProfileSnapshot(node)
updateStartScript(node)
stopContainer(node)
saveProfile(node)
@@ -589,11 +565,10 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo
}
object Docker {
- private val ContainerRoot = Paths.get("/opt/zbs")
- private val ProfilerController = ContainerRoot.resolve("yjp-controller-api-redist.jar")
- private val ProfilerPort = 10001
- private val jsonMapper = new ObjectMapper
- private val propsMapper = new JavaPropsMapper
+ private val ContainerRoot = Paths.get("/opt/zbs")
+ private val ProfilerPort = 10001
+ private val jsonMapper = new ObjectMapper
+ private val propsMapper = new JavaPropsMapper
val configTemplate = parseResources("template.conf")
def genesisOverride = {
diff --git a/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala
index 87046a5..689f5f8 100644
--- a/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala
@@ -190,6 +190,10 @@ object AsyncHttpApi extends Assertions {
def transactionsByAddress(address: String, limit: Int): Future[Seq[Seq[TransactionInfo]]] =
get(s"/transactions/address/$address/limit/$limit").as[Seq[Seq[TransactionInfo]]]
+ def transactionsByAddress(address: String, limit: Int, after: String): Future[Seq[Seq[TransactionInfo]]] = {
+ get(s"/transactions/address/$address/limit/$limit?after=$after").as[Seq[Seq[TransactionInfo]]]
+ }
+
def assetDistributionAtHeight(asset: String, height: Int, limit: Int, maybeAfter: Option[String] = None): Future[AssetDistributionPage] = {
val after = maybeAfter.fold("")(a => s"?after=$a")
val url = s"/assets/$asset/distribution/$height/limit/$limit$after"
diff --git a/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala
index 2a5d57e..3a6dfec 100644
--- a/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala
@@ -274,10 +274,10 @@ object AsyncMatcherHttpApi extends Assertions {
def getAllSnapshotOffsets: Future[Map[String, QueueEventWithMeta.Offset]] =
matcherGetWithApiKey("/matcher/debug/allSnapshotOffsets").as[Map[String, QueueEventWithMeta.Offset]]
- def waitForStableOffset(confirmations: Int, maxTries: Int, interval: FiniteDuration): Future[Either[Unit, QueueEventWithMeta.Offset]] = {
- def loop(n: Int, currConfirmations: Int, currOffset: QueueEventWithMeta.Offset): Future[Either[Unit, QueueEventWithMeta.Offset]] =
- if (currConfirmations >= confirmations) Future.successful(Right(currOffset))
- else if (n > maxTries) Future.successful(Left(()))
+ def waitForStableOffset(confirmations: Int, maxTries: Int, interval: FiniteDuration): Future[QueueEventWithMeta.Offset] = {
+ def loop(n: Int, currConfirmations: Int, currOffset: QueueEventWithMeta.Offset): Future[QueueEventWithMeta.Offset] =
+ if (currConfirmations >= confirmations) Future.successful(currOffset)
+ else if (n > maxTries) Future.failed(new IllegalStateException(s"Offset is not stable: $maxTries tries is out"))
else
GlobalTimer.instance
.sleep(interval)
diff --git a/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala
index 0136cd3..74f8c6d 100644
--- a/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala
@@ -169,6 +169,9 @@ object SyncHttpApi extends Assertions {
def transactionsByAddress(address: String, limit: Int): Seq[Seq[TransactionInfo]] =
sync(async(n).transactionsByAddress(address, limit))
+ def transactionsByAddress(address: String, limit: Int, after: String): Seq[Seq[TransactionInfo]] =
+ sync(async(n).transactionsByAddress(address, limit, after))
+
def scriptCompile(code: String): CompiledScript =
sync(async(n).scriptCompile(code))
@@ -269,6 +272,9 @@ object SyncHttpApi extends Assertions {
def waitForHeight(expectedHeight: Int, requestAwaitTime: FiniteDuration = RequestAwaitTime): Int =
sync(async(n).waitForHeight(expectedHeight), requestAwaitTime)
+ def blacklist(address: InetSocketAddress): Unit =
+ sync(async(n).blacklist(address))
+
def debugMinerInfo(): Seq[State] =
sync(async(n).debugMinerInfo())
diff --git a/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala
index b2edfcc..0963d0d 100644
--- a/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala
@@ -106,6 +106,29 @@ object SyncMatcherHttpApi extends Assertions {
waitTime: Duration = OrderRequestAwaitTime): MatcherStatusResponse =
sync(async(m).waitOrderStatusAndAmount(assetPair, orderId, expectedStatus, expectedFilledAmount), waitTime)
+ def waitOrderProcessed(assetPair: AssetPair, orderId: String, checkTimes: Int = 5, retryInterval: FiniteDuration = 1.second): Unit = {
+ val fixedStatus = sync {
+ async(m).waitFor[MatcherStatusResponse](s"$orderId processed")(
+ _.orderStatus(orderId, assetPair),
+ _.status != "NotFound",
+ retryInterval
+ )
+ }
+
+ // Wait until something changed or not :)
+ def loop(n: Int): Unit =
+ if (n == 0) m.log.debug(s"$orderId wasn't changed (tried $checkTimes times)")
+ else {
+ val currStatus = orderStatus(orderId, assetPair)
+ if (currStatus == fixedStatus) {
+ Thread.sleep(retryInterval.toMillis)
+ loop(n - 1)
+ } else m.log.debug(s"$orderId was changed on ${checkTimes - n} step")
+ }
+
+ loop(checkTimes)
+ }
+
def waitOrderInBlockchain(orderId: String,
retryInterval: FiniteDuration = 1.second,
waitTime: Duration = OrderRequestAwaitTime): Seq[TransactionInfo] =
@@ -213,7 +236,7 @@ object SyncMatcherHttpApi extends Assertions {
def waitForStableOffset(confirmations: Int,
maxTries: Int,
interval: FiniteDuration,
- waitTime: Duration = RequestAwaitTime): Either[Unit, QueueEventWithMeta.Offset] =
+ waitTime: Duration = RequestAwaitTime): QueueEventWithMeta.Offset =
sync(async(m).waitForStableOffset(confirmations, maxTries, interval), (maxTries + 1) * interval)
def matcherState(assetPairs: Seq[AssetPair],
diff --git a/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala b/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala
index 76bc510..31b8fd3 100644
--- a/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala
+++ b/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala
@@ -21,11 +21,11 @@ abstract class MatcherSuiteBase
val defaultAssetQuantity = 999999999999L
val smartFee = 0.004.zbs
- val minFee = 0.001.zbs + smartFee
- val issueFee = 1.zbs
+ val minFee = 0.02.zbs + smartFee
+ val issueFee = 500.zbs
val smartIssueFee = 1.zbs + smartFee
- val leasingFee = 0.002.zbs + smartFee
- val tradeFee = 0.003.zbs
+ val leasingFee = 5.zbs + smartFee
+ val tradeFee = 0.02.zbs
val smartTradeFee = tradeFee + smartFee
val twoSmartTradeFee = tradeFee + 2 * smartFee
diff --git a/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala b/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala
new file mode 100644
index 0000000..5a585a6
--- /dev/null
+++ b/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala
@@ -0,0 +1,135 @@
+package com.zbsnetwork.it.sync.transactions
+
+import com.typesafe.config.Config
+import com.zbsnetwork.account.Address
+import com.zbsnetwork.common.utils._
+import com.zbsnetwork.it.api.SyncHttpApi._
+import com.zbsnetwork.it.api.TransactionInfo
+import com.zbsnetwork.it.transactions.NodesFromDocker
+import com.zbsnetwork.it.{Node, NodeConfigs, ReportingTestName}
+import com.zbsnetwork.transaction.transfer.{TransferTransaction, TransferTransactionV1}
+import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers}
+import play.api.libs.json.JsNumber
+import scala.concurrent.duration._
+
+class TransactionAPISuite extends FreeSpec with NodesFromDocker with Matchers with ReportingTestName with CancelAfterFailure {
+
+ override def nodeConfigs: Seq[Config] =
+ NodeConfigs.newBuilder
+ .overrideBase(_.quorum(0))
+ .overrideBase(_.raw("zbs.rest-api.transactions-by-address-limit=10"))
+ .withDefault(1)
+ .withSpecial(1, _.nonMiner)
+ .buildNonConflicting()
+
+ val sender: Node = nodes.head
+ val recipient: Address = Address.fromString(sender.createAddress()).explicitGet()
+
+ val Zbs: Long = 100000000L
+
+ val AMT: Long = 1 * Zbs
+ val FEE: Long = (0.05 * Zbs).toLong
+
+ val transactions: List[TransferTransaction] =
+ (for (i <- 0 to 30) yield {
+ TransferTransactionV1
+ .selfSigned(
+ None,
+ sender.privateKey,
+ recipient,
+ AMT,
+ System.currentTimeMillis() + i,
+ None,
+ FEE + i * 100,
+ Array.emptyByteArray
+ )
+ .explicitGet()
+ }).toList
+
+ val transactionIds = transactions.map(_.id().base58)
+
+ "should accept transactions" in {
+ transactions.foreach { tx =>
+ sender.broadcastRequest(tx.json() + ("type" -> JsNumber(tx.builder.typeId.toInt)))
+ }
+
+ val h = sender.height
+
+ sender.waitForHeight(h + 3, 2.minutes)
+ }
+
+ "should return correct N txs on request without `after`" in {
+
+ def checkForLimit(limit: Int): Unit = {
+ val expected =
+ transactionIds
+ .take(limit)
+
+ val received =
+ sender
+ .transactionsByAddress(recipient.address, limit)
+ .flatten
+ .map(_.id)
+
+ expected shouldEqual received
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ "should return correct N txs on request with `after`" in {
+
+ def checkForLimit(limit: Int): Unit = {
+ val expected =
+ transactionIds
+ .slice(limit, limit + limit)
+
+ val afterParam =
+ transactions
+ .drop(limit - 1)
+ .head
+ .id()
+ .base58
+
+ val received =
+ sender
+ .transactionsByAddress(recipient.address, limit, afterParam)
+ .flatten
+ .map(_.id)
+
+ expected shouldEqual received
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ "should return all transactions" in {
+ def checkForLimit(limit: Int): Unit = {
+ val received =
+ loadAll(sender, recipient.address, limit, None, Nil)
+ .map(_.id)
+
+ received shouldEqual transactionIds
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ def loadAll(node: Node, address: String, limit: Int, maybeAfter: Option[String], acc: List[TransactionInfo]): List[TransactionInfo] = {
+ val txs = maybeAfter match {
+ case None => node.transactionsByAddress(address, limit).flatten.toList
+ case Some(lastId) => node.transactionsByAddress(address, limit, lastId).flatten.toList
+ }
+
+ txs.lastOption match {
+ case None => acc ++ txs
+ case Some(tx) => loadAll(node, address, limit, Some(tx.id), acc ++ txs)
+ }
+ }
+}
diff --git a/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala b/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala
index 0ecc11b..dd56c0e 100644
--- a/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala
@@ -29,7 +29,6 @@ class WideStateGenerationSuite extends FreeSpec with WaitForHeight2 with Matcher
| ignore-rx-messages = [1, 2, 25]
| }
| miner.minimal-block-generation-offset = 10s
- | utx.cleanup-interval = 1m
| synchronization.utx-synchronizer {
| max-buffer-size = 500
| max-buffer-time = 100ms
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala
new file mode 100644
index 0000000..1f9301b
--- /dev/null
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala
@@ -0,0 +1,45 @@
+package com.zbsnetwork.it.sync
+
+import com.typesafe.config.Config
+import com.zbsnetwork.it.api.SyncHttpApi._
+import com.zbsnetwork.it.api._
+import com.zbsnetwork.it.transactions.NodesFromDocker
+import com.zbsnetwork.it.{NodeConfigs, ReportingTestName}
+import org.scalatest._
+import scala.concurrent.duration._
+
+class BlacklistTestSuite extends FreeSpec with Matchers with CancelAfterFailure with ReportingTestName with NodesFromDocker {
+
+ override protected def nodeConfigs: Seq[Config] =
+ NodeConfigs.newBuilder
+ .overrideBase(_.quorum(1))
+ .withDefault(2)
+ .withSpecial(_.quorum(0))
+ .buildNonConflicting()
+
+ private def primaryNode = dockerNodes().last
+
+ private def otherNodes = dockerNodes().init
+
+ "primary node should blacklist other nodes" in {
+ otherNodes.foreach(n => primaryNode.blacklist(n.containerNetworkAddress))
+
+ val expectedBlacklistedPeers = nodes.size - 1
+
+ primaryNode.waitFor[Seq[BlacklistedPeer]](s"blacklistedPeers.size == $expectedBlacklistedPeers")(
+ _ => primaryNode.blacklistedPeers,
+ _.lengthCompare(expectedBlacklistedPeers) == 0,
+ 1.second
+ )
+ }
+
+ "sleep while nodes are blocked" in {
+ primaryNode.waitFor[Seq[BlacklistedPeer]](s"blacklistedPeers is empty")(_.blacklistedPeers, _.isEmpty, 5.second)
+ }
+
+ "and sync again" in {
+ val baseHeight = nodes.map(_.height).max
+ nodes.waitForSameBlockHeadesAt(baseHeight + 5)
+ }
+
+}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala
index 909b75a..f460ca1 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala
@@ -25,7 +25,7 @@ class FeatureActivationTestSuite
NodeConfigs.newBuilder
.overrideBase(_.raw(s"""zbs {
| blockchain.custom.functionality {
- | pre-activated-features = null
+ | pre-activated-features = {}
| feature-check-blocks-period = $votingInterval
| blocks-for-feature-activation = $blocksForActivation
| }
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala
new file mode 100644
index 0000000..02f7ab1
--- /dev/null
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala
@@ -0,0 +1,105 @@
+package com.zbsnetwork.it.sync.activation
+import com.typesafe.config.{Config, ConfigFactory}
+import com.zbsnetwork.features.api.NodeFeatureStatus
+import com.zbsnetwork.features.{BlockchainFeatureStatus, BlockchainFeatures}
+import com.zbsnetwork.it.{Docker, ReportingTestName}
+import com.zbsnetwork.it.api.SyncHttpApi._
+import com.zbsnetwork.it.transactions.NodesFromDocker
+import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers}
+class PreActivatedFeaturesTestSuite
+ extends FreeSpec
+ with Matchers
+ with CancelAfterFailure
+ with NodesFromDocker
+ with ActivationStatusRequest
+ with ReportingTestName {
+ override protected def nodeConfigs: Seq[Config] = PreActivatedFeaturesTestSuite.Configs
+
+ nodes.foreach(n => n.accountBalances(n.address))
+
+ "before activation check" in {
+ nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval / 2)
+
+ val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)
+ mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertVotingStatus(mainNodeStatus, mainNodeStatus.supportingBlocks.get, BlockchainFeatureStatus.Undefined, NodeFeatureStatus.Voted)
+
+ val otherNodes = nodes.tail.map(_.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum))
+ otherNodes.foreach { s =>
+ s.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertActivatedStatus(s, 0, NodeFeatureStatus.Voted)
+ }
+ }
+ "on activation height check" in {
+ nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval + 3)
+
+ val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)
+ mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertApprovedStatus(mainNodeStatus, PreActivatedFeaturesTestSuite.votingInterval * 2, NodeFeatureStatus.Voted)
+
+ val otherNodes = nodes.tail
+ otherNodes.foreach { node =>
+ val feature = node.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)
+ feature.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertActivatedStatus(feature, 0, NodeFeatureStatus.Voted)
+
+ val node1 = docker.restartNode(node.asInstanceOf[Docker.DockerNode])
+
+ val feature2 = node1.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)
+ assertActivatedStatus(feature2, 0, NodeFeatureStatus.Voted)
+ }
+ }
+ "after activation height check" in {
+ nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval * 2 + 4)
+
+ val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)
+ mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertActivatedStatus(mainNodeStatus, PreActivatedFeaturesTestSuite.votingInterval * 2, NodeFeatureStatus.Voted)
+
+ val otherNodes = nodes.tail.map(_.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum))
+ otherNodes.foreach { s =>
+ s.description shouldBe PreActivatedFeaturesTestSuite.featureDescr
+ assertActivatedStatus(s, 0, NodeFeatureStatus.Voted)
+ }
+ }
+}
+object PreActivatedFeaturesTestSuite {
+ import com.zbsnetwork.it.NodeConfigs._
+ val votingInterval = 10
+ val featureNum: Short = BlockchainFeatures.SmallerMinimalGeneratingBalance.id
+ val featureDescr = BlockchainFeatures.SmallerMinimalGeneratingBalance.description
+ private val supportedConfig = ConfigFactory.parseString(s"""zbs {
+ | blockchain.custom.functionality {
+ | pre-activated-features = {}
+ | feature-check-blocks-period = $votingInterval
+ | blocks-for-feature-activation = 1
+ | }
+ | features.supported = [$featureNum]
+ | miner.quorum = 1
+ |}""".stripMargin)
+ private val preactivatedConfig = ConfigFactory.parseString(s"""zbs {
+ | blockchain.custom.functionality {
+ | feature-check-blocks-period = $votingInterval
+ | pre-activated-features {
+ | 1: 0
+ | 2: 100
+ | 3: 100
+ | 4: 100
+ | 5: 100
+ | 6: 100
+ | 7: 100
+ | 8: 100
+ | 9: 100
+ | 10: 100
+ | 11: 100
+ | }
+ | }
+ | features.supported = [$featureNum]
+ | miner.quorum = 1
+ |}""".stripMargin)
+ val Configs: Seq[Config] = Seq(
+ supportedConfig.withFallback(Default.last),
+ preactivatedConfig.withFallback(Default.head),
+ preactivatedConfig.withFallback(Default(1))
+ )
+}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala
index 71a830f..6f19bbd 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala
@@ -1,10 +1,30 @@
package com.zbsnetwork.it.sync.debug
+import com.typesafe.config.Config
+import com.zbsnetwork.it.{Node, NodeConfigs}
import com.zbsnetwork.it.api.SyncHttpApi._
-import com.zbsnetwork.it.transactions.BaseTransactionSuite
+import com.zbsnetwork.it.transactions.NodesFromDocker
import com.zbsnetwork.it.util._
+import com.zbsnetwork.it.sync._
+import org.scalatest.FunSuite
-class DebugPortfoliosSuite extends BaseTransactionSuite {
+class DebugPortfoliosSuite extends FunSuite with NodesFromDocker {
+ override protected def nodeConfigs: Seq[Config] =
+ NodeConfigs.newBuilder
+ .overrideBase(_.quorum(0))
+ .withDefault(entitiesNumber = 1)
+ .buildNonConflicting()
+
+ private def sender: Node = nodes.head
+
+ private val firstAddress = sender.createAddress()
+ private val secondAddress = sender.createAddress()
+
+ override protected def beforeAll(): Unit = {
+ super.beforeAll()
+ sender.transfer(sender.address, firstAddress, 20.zbs, minFee, waitForTx = true)
+ sender.transfer(sender.address, secondAddress, 20.zbs, minFee, waitForTx = true)
+ }
test("getting a balance considering pessimistic transactions from UTX pool - changed after UTX") {
val portfolioBefore = sender.debugPortfoliosFor(firstAddress, considerUnspent = true)
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala
index ed93243..10a3583 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala
@@ -31,12 +31,14 @@ class MatcherRecoveryTestSuite extends MatcherSuiteBase {
Seq(issue1, issue2).map(matcherNode.signedIssue).map(x => nodes.waitForTransaction(x.id))
- private val orders = Gen.containerOfN[Vector, Order](placesNumber, orderGen(matcherNode.publicKey, aliceAcc, assetPairs)).sample.get
+ private val orders = Gen.containerOfN[Vector, Order](placesNumber, orderGen(matcherNode.publicKey, aliceAcc, assetPairs)).sample.get
+ private val lastOrder = orderGen(matcherNode.publicKey, aliceAcc, assetPairs).sample.get
"Place, fill and cancel a lot of orders" in {
val cancels = (1 to cancelsNumber).map(_ => choose(orders))
val commands = Random.shuffle(orders.map(MatcherCommand.Place(matcherNode, _))) ++ cancels.map(MatcherCommand.Cancel(matcherNode, aliceAcc, _))
executeCommands(commands)
+ executeCommands(List(MatcherCommand.Place(matcherNode, lastOrder)))
}
"Wait until all requests are processed - 1" in matcherNode.waitForStableOffset(10, 100, 200.millis)
@@ -56,7 +58,10 @@ class MatcherRecoveryTestSuite extends MatcherSuiteBase {
"Restart the matcher" in docker.restartContainer(matcherNode.asInstanceOf[DockerNode])
"Wait until all requests are processed - 2" in {
- matcherNode.waitFor[QueueEventWithMeta.Offset]("all requests are processed")(_.getCurrentOffset, _ == stateBefore.offset, 300.millis)
+ matcherNode.waitFor[QueueEventWithMeta.Offset]("all events are consumed")(_.getCurrentOffset, _ == stateBefore.offset, 300.millis)
+ withClue("Last command processed") {
+ matcherNode.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr())
+ }
}
"Verify the state" in {
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala
index 6580e80..d259121 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala
@@ -2,6 +2,7 @@ package com.zbsnetwork.it.sync.matcher
import com.typesafe.config.Config
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.it.api.OrderBookResponse
import com.zbsnetwork.it.api.SyncHttpApi._
import com.zbsnetwork.it.api.SyncMatcherHttpApi._
import com.zbsnetwork.it.matcher.MatcherSuiteBase
@@ -65,8 +66,8 @@ class MatcherRestartTestSuite extends MatcherSuiteBase {
matcherNode.placeOrder(aliceAcc, aliceZbsPair, OrderType.SELL, 500, 2.zbs * Order.PriceConstant, matcherFee, orderVersion, 5.minutes)
aliceSecondOrder.status shouldBe "OrderAccepted"
- val orders2 = matcherNode.orderBook(aliceZbsPair)
- orders2.asks.head.amount shouldBe 1000
+ val orders2 =
+ matcherNode.waitFor[OrderBookResponse]("Top ask has 1000 amount")(_.orderBook(aliceZbsPair), _.asks.head.amount == 1000, 1.second)
orders2.asks.head.price shouldBe 2.zbs * Order.PriceConstant
val cancel = matcherNode.cancelOrder(aliceAcc, aliceZbsPair, firstOrder)
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala
index 632e1a5..bb3290d 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala
@@ -21,11 +21,12 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase {
| snapshots-interval = 51
|}""".stripMargin)
+ private def matcher1NodeConfig = Default.last
private def matcher2NodeConfig = ConfigFactory.parseString("""zbs.network.node-name = node11
- |akka.kafka.consumer.kafka-clients.group.id = 1""".stripMargin).withFallback(Default.last)
+ |akka.kafka.consumer.kafka-clients.group.id = 1""".stripMargin).withFallback(matcher1NodeConfig)
override protected def nodeConfigs: Seq[Config] =
- List(Default.last, matcher2NodeConfig, Default(2 + Random.nextInt(Default.size - 2)))
+ (List(matcher1NodeConfig, matcher2NodeConfig) ++ Random.shuffle(Default.init).take(1))
.zip(Seq(matcherConfig, matcherConfig, minerEnabled))
.map { case (n, o) => o.withFallback(n) }
.map(configOverrides.withFallback)
@@ -59,6 +60,7 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase {
private val aliceOrders = mkOrders(aliceAcc)
private val bobOrders = mkOrders(aliceAcc)
private val orders = aliceOrders ++ bobOrders
+ private val lastOrder = orderGen(matcherPublicKey, aliceAcc, assetPairs).sample.get
"Place, fill and cancel a lot of orders" in {
val alicePlaces = aliceOrders.map(MatcherCommand.Place(matcher1Node, _))
@@ -70,11 +72,17 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase {
val cancels = Random.shuffle(aliceCancels ++ bobCancels)
executeCommands(places ++ cancels)
+ executeCommands(List(MatcherCommand.Place(matcher1Node, lastOrder)))
}
"Wait until all requests are processed" in {
- matcher1Node.waitForStableOffset(10, 100, 200.millis)
- matcher2Node.waitForStableOffset(10, 100, 200.millis)
+ val offset1 = matcher1Node.waitForStableOffset(10, 100, 200.millis)
+ matcher2Node.waitFor[Long](s"Offset is $offset1")(_.getCurrentOffset, _ == offset1, 2.seconds)
+
+ withClue("Last command processed") {
+ matcher1Node.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr())
+ matcher2Node.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr())
+ }
}
"States on both matcher should be equal" in {
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala
index 1827a6b..ac06b61 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala
@@ -35,7 +35,8 @@ object MatcherDefaultConfig {
| rest-order-limit=$orderLimit
|}""".stripMargin)
- val Configs: Seq[Config] = (Default.last +: Random.shuffle(Default.init).take(2))
+ val Configs: Seq[Config] = List(9, 5, 7)
+ .map(Default)
.zip(Seq(matcherConfig, minerDisabled, minerEnabled))
.map { case (n, o) => o.withFallback(n) }
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala
index a1fd209..7f160f6 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala
@@ -4,32 +4,31 @@ import java.nio.charset.StandardCharsets
import com.typesafe.config.Config
import com.zbsnetwork.account.Address
-import com.zbsnetwork.it._
import com.zbsnetwork.it.api.SyncHttpApi._
import com.zbsnetwork.it.api.AsyncNetworkApi._
-import com.zbsnetwork.it.api._
import com.zbsnetwork.it.transactions.BaseTransactionSuite
import com.zbsnetwork.network.{RawBytes, TransactionSpec}
import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.it.NodeConfigs
import com.zbsnetwork.transaction.transfer._
import org.scalatest._
-import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
+import com.zbsnetwork.it.sync._
+
import scala.concurrent.duration._
import scala.language.postfixOps
-class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers with ScalaFutures with IntegrationPatience with RecoverMethods {
-
+class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers {
override protected def nodeConfigs: Seq[Config] =
NodeConfigs.newBuilder
- .overrideBase(_.quorum(2))
- .withDefault(3)
- .build()
+ .overrideBase(_.quorum(0))
+ .withDefault(entitiesNumber = 1)
+ .buildNonConflicting()
private def node = nodes.head
test("valid tx send by network to node should be in blockchain") {
val tx = TransferTransactionV1
- .selfSigned(None, node.privateKey, Address.fromString(node.address).explicitGet(), 1L, System.currentTimeMillis(), None, 100000L, Array())
+ .selfSigned(None, node.privateKey, Address.fromString(node.address).explicitGet(), 1L, System.currentTimeMillis(), None, minFee, Array())
.right
.get
@@ -46,7 +45,7 @@ class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers with Sc
1L,
System.currentTimeMillis() + (1 days).toMillis,
None,
- 100000L,
+ minFee,
Array())
.right
.get
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/package.scala b/it/src/test/scala/com/zbsnetwork/it/sync/package.scala
index 0ec8278..7cc671b 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/package.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/package.scala
@@ -9,7 +9,7 @@ import com.zbsnetwork.transaction.smart.script.{Script, ScriptCompiler}
package object sync {
val smartFee: Long = 0.004.zbs
- val minFee: Long = 0.001.zbs
+ val minFee: Long = 0.005.zbs
val leasingFee: Long = 0.002.zbs
val issueFee: Long = 1.zbs
val burnFee: Long = 1.zbs
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala
index a390070..d7cd47f 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala
@@ -66,10 +66,12 @@ class ContractInvocationTransactionSuite extends BaseTransactionSuite with Cance
test("set contract to contract account") {
val scriptText =
"""
+ |{-# STDLIB_VERSION 3 #-}
+ |{-# CONTENT_TYPE CONTRACT #-}
|
| @Callable(inv)
- | func foo(a:ByteStr) = {
- | WriteSet(List(DataEntry("a", a), DataEntry("sender", inv.caller.bytes)))
+ | func foo(a:ByteVector) = {
+ | WriteSet([DataEntry("a", a), DataEntry("sender", inv.caller.bytes)])
| }
|
| @Verifier(t)
@@ -80,7 +82,7 @@ class ContractInvocationTransactionSuite extends BaseTransactionSuite with Cance
|
""".stripMargin
- val script = ScriptCompiler.contract(scriptText).explicitGet()
+ val script = ScriptCompiler.compile(scriptText).explicitGet()._1
val setScriptTransaction = SetScriptTransaction
.selfSigned(contract, Some(script), setScriptFee, System.currentTimeMillis())
.explicitGet()
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala
index 462831a..d89186a 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala
@@ -10,7 +10,6 @@ import com.zbsnetwork.state._
import com.zbsnetwork.transaction.DataTransaction
import com.zbsnetwork.transaction.assets.exchange._
import org.scalatest.CancelAfterFailure
-import play.api.libs.json._
import scorex.crypto.encode.Base64
class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFailure with NTPTime {
@@ -80,10 +79,15 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa
(contr2, acc1),
(mcontr, acc2),
)
+ for ((o1ver, o2ver) <- Seq(
+ (2: Byte, 2: Byte),
+ (2: Byte, 3: Byte),
+ )) {
- sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2), waitForTx = true)
+ sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2), waitForTx = true)
- //TODO : add assert balances
+ //TODO : add assert balances
+ }
}
setContracts(
@@ -105,10 +109,14 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa
(contr2, acc1),
(mcontr, acc2),
)
-
- assertBadRequestAndMessage(sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2)),
- "Transaction is not allowed by account-script")
- //TODO : add assert balances
+ for ((o1ver, o2ver) <- Seq(
+ (2: Byte, 2: Byte),
+ (3: Byte, 3: Byte),
+ )) {
+ assertBadRequestAndMessage(sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2)),
+ "Transaction is not allowed by account-script")
+ //TODO : add assert balances
+ }
}
setContracts(
(None, acc0),
@@ -126,10 +134,14 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa
(contr2, acc1),
(mcontr, acc2),
)
-
- val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2)
- assertBadRequestAndMessage(sender.signedBroadcast(tx), "Error while executing account-script: Some generic error")
- //TODO : add assert balances
+ for ((o1ver, o2ver) <- Seq(
+ (2: Byte, 2: Byte),
+ (3: Byte, 3: Byte),
+ )) {
+ val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2)
+ assertBadRequestAndMessage(sender.signedBroadcast(tx), "Error while executing account-script: Some generic error")
+ //TODO : add assert balances
+ }
}
setContracts(
(None, acc0),
@@ -152,8 +164,53 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa
val matcher = acc2
val sellPrice = (0.50 * Order.PriceConstant).toLong
- val buy = orders(pair, 1, orderFee, ntpTime, acc1, acc0, acc2)._1
- val sell = orders(pair, 2, orderFee, ntpTime, acc1, acc0, acc2)._2
+ for ((o1ver, o2ver) <- Seq(
+ (1: Byte, 2: Byte),
+ (1: Byte, 3: Byte),
+ )) {
+
+ val (buy, sell) = orders(pair, o1ver, o2ver, orderFee, ntpTime, acc1, acc0, acc2)
+
+ val amount = math.min(buy.amount, sell.amount)
+ val tx = ExchangeTransactionV2
+ .create(
+ matcher = matcher,
+ buyOrder = buy,
+ sellOrder = sell,
+ amount = amount,
+ price = sellPrice,
+ buyMatcherFee = (BigInt(orderFee) * amount / buy.amount).toLong,
+ sellMatcherFee = (BigInt(orderFee) * amount / sell.amount).toLong,
+ fee = smartMatcherFee,
+ timestamp = ntpTime.correctedTime()
+ )
+ .explicitGet()
+ .json()
+
+ val txId = sender.signedBroadcast(tx).id
+ nodes.waitForHeightAriseAndTxPresent(txId)
+
+ //TODO : add assert balances
+ }
+ }
+ setContracts(
+ (None, acc0),
+ (None, acc1),
+ (None, acc2),
+ )
+ }
+
+ test("negative - exchange tx v2 and order v1 from scripted acc") {
+ setContracts((sc1, acc0))
+
+ val matcher = acc2
+ val sellPrice = (0.50 * Order.PriceConstant).toLong
+
+ for ((o1ver, o2ver) <- Seq(
+ (2: Byte, 1: Byte),
+ (3: Byte, 1: Byte),
+ )) {
+ val (buy, sell) = orders(pair, o1ver, o2ver, orderFee, ntpTime, acc1, acc0, acc2)
val amount = math.min(buy.amount, sell.amount)
val tx = ExchangeTransactionV2
@@ -171,50 +228,7 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa
.explicitGet()
.json()
- val txId = sender.signedBroadcast(tx).id
- nodes.waitForHeightAriseAndTxPresent(txId)
-
- //TODO : add assert balances
+ assertBadRequestAndMessage(sender.signedBroadcast(tx), "Reason: Can't process order with signature from scripted account")
}
- setContracts(
- (None, acc0),
- (None, acc1),
- (None, acc2),
- )
}
-
- test("negative - check orders v2 with exchange tx v1") {
- val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2)
- val sig = (Json.parse(tx.toString()) \ "proofs").as[Seq[JsString]].head
- val changedTx = tx + ("version" -> JsNumber(1)) + ("signature" -> sig)
- assertBadRequestAndMessage(sender.signedBroadcast(changedTx), "can only contain orders of version 1", 400)
- }
-
- test("negative - exchange tx v2 and order v1 from scripted acc") {
- setContracts((sc1, acc0))
-
- val matcher = acc2
- val sellPrice = (0.50 * Order.PriceConstant).toLong
- val buy = orders(pair, 2, orderFee, ntpTime, acc1, acc0, acc2)._1
- val sell = orders(pair, 1, orderFee, ntpTime, acc1, acc0, acc2)._2
-
- val amount = math.min(buy.amount, sell.amount)
- val tx = ExchangeTransactionV2
- .create(
- matcher = matcher,
- buyOrder = buy,
- sellOrder = sell,
- amount = amount,
- price = sellPrice,
- buyMatcherFee = (BigInt(orderFee) * amount / buy.amount).toLong,
- sellMatcherFee = (BigInt(orderFee) * amount / sell.amount).toLong,
- fee = smartMatcherFee,
- timestamp = ntpTime.correctedTime()
- )
- .explicitGet()
- .json()
-
- assertBadRequestAndMessage(sender.signedBroadcast(tx), "Reason: Can't process order with signature from scripted account")
- }
-
}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala
index cbb5114..e0b01a0 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala
@@ -64,6 +64,8 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter
test("set contract to contract account") {
val scriptText =
"""
+ |{-# STDLIB_VERSION 3 #-}
+ |{-# CONTENT_TYPE CONTRACT #-}
|
| @Callable(i)
| func deposit() = {
@@ -76,7 +78,7 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter
| case _ => 0
| }
| let newAmount = currentAmount + pmt.amount
- | WriteSet(List(DataEntry(currentKey, newAmount)))
+ | WriteSet([DataEntry(currentKey, newAmount)])
|
| }
| }
@@ -94,8 +96,8 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter
| else if (newAmount < 0)
| then throw("Not enough balance")
| else ContractResult(
- | WriteSet(List(DataEntry(currentKey, newAmount))),
- | TransferSet(List(ContractTransfer(i.caller, amount, unit)))
+ | WriteSet([DataEntry(currentKey, newAmount)]),
+ | TransferSet([ContractTransfer(i.caller, amount, unit)])
| )
| }
|
@@ -103,7 +105,7 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter
|
""".stripMargin
- val script = ScriptCompiler.contract(scriptText).explicitGet()
+ val script = ScriptCompiler.compile(scriptText).explicitGet()._1
val setScriptTransaction = SetScriptTransaction
.selfSigned(contract, Some(script), setScriptFee, System.currentTimeMillis())
.explicitGet()
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala
index 8b993d0..7f7e264 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala
@@ -1,7 +1,9 @@
package com.zbsnetwork.it.sync.smartcontract
+import com.typesafe.config.Config
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.it.NodeConfigs
import com.zbsnetwork.it.api.SyncHttpApi._
import com.zbsnetwork.it.sync._
import com.zbsnetwork.it.transactions.BaseTransactionSuite
@@ -12,6 +14,12 @@ import com.zbsnetwork.transaction.transfer.TransferTransactionV2
import org.scalatest.CancelAfterFailure
class RIDEFuncSuite extends BaseTransactionSuite with CancelAfterFailure {
+ override protected def nodeConfigs: Seq[Config] =
+ NodeConfigs.newBuilder
+ .overrideBase(_.quorum(0))
+ .withDefault(entitiesNumber = 1)
+ .buildNonConflicting()
+
private val acc0 = pkByAddress(firstAddress)
test("assetBalance() verification") {
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala
index 9d29646..43075f5 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala
@@ -106,12 +106,12 @@ package object smartcontract {
| }
""".stripMargin
- def exchangeTx(pair: AssetPair, exTxFee: Long, orderFee: Long, time: Time, accounts: PrivateKeyAccount*): JsObject = {
+ def exchangeTx(pair: AssetPair, exTxFee: Long, orderFee: Long, time: Time, ord1Ver: Byte, ord2Ver: Byte, accounts: PrivateKeyAccount*): JsObject = {
val buyer = accounts.head // first one
val seller = accounts.tail.head // second one
val matcher = accounts.last
val sellPrice = (0.50 * Order.PriceConstant).toLong
- val (buy, sell) = orders(pair, 2, orderFee, time, buyer, seller, matcher)
+ val (buy, sell) = orders(pair, ord1Ver, ord2Ver, orderFee, time, buyer, seller, matcher)
val amount = math.min(buy.amount, sell.amount)
@@ -138,7 +138,7 @@ package object smartcontract {
tx
}
- def orders(pair: AssetPair, version: Byte, fee: Long, time: Time, accounts: PrivateKeyAccount*): (Order, Order) = {
+ def orders(pair: AssetPair, ord1Ver: Byte, ord2Ver: Byte, fee: Long, time: Time, accounts: PrivateKeyAccount*): (Order, Order) = {
val buyer = accounts.head // first one
val seller = accounts.tail.head // second one
val matcher = accounts.last
@@ -149,8 +149,8 @@ package object smartcontract {
val buyAmount = 2
val sellAmount = 3
- val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, fee, version)
- val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, fee, version)
+ val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, fee, ord1Ver)
+ val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, fee, ord2Ver)
(buy, sell)
}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala
index 4a91954..a5b207e 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala
@@ -41,10 +41,10 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail
val s = Some(
ScriptCompiler(
s"""
- |match tx {
- |case s : SetAssetScriptTransaction => true
- |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc2.publicKey).base58}')
- |case _ => false}""".stripMargin,
+ |match tx {
+ |case s : SetAssetScriptTransaction => true
+ |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc2.publicKey).base58}')
+ |case _ => false}""".stripMargin,
isAssetScript = true
).explicitGet()._1.bytes.value.base64)
@@ -61,24 +61,24 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail
setContracts((contr1, acc0), (contr2, acc1), (mcontr, acc2))
- sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2),
+ sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 2, 3, acc1, acc0, acc2),
waitForTx = true)
}
val sUpdated = Some(
ScriptCompiler(
s"""
- |match tx {
- |case s : SetAssetScriptTransaction => true
- |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc1.publicKey).base58}')
- |case _ => false}""".stripMargin,
+ |match tx {
+ |case s : SetAssetScriptTransaction => true
+ |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc1.publicKey).base58}')
+ |case _ => false}""".stripMargin,
isAssetScript = true
).explicitGet()._1.bytes.value.base64)
sender.setAssetScript(sAsset, firstAddress, setAssetScriptFee, sUpdated, waitForTx = true)
assertBadRequestAndMessage(
- sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2)),
+ sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 3, 2, acc1, acc0, acc2)),
errNotAllowedByToken)
setContracts((None, acc0), (None, acc1), (None, acc2))
@@ -116,19 +116,20 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail
priceAsset = Some(ByteStr.decodeBase58(assetB).get)
)
- sender.signedBroadcast(exchangeTx(smartAssetPair, matcherFee + 2 * smartFee, matcherFee + 2 * smartFee, ntpTime, acc1, acc0, acc2),
+ sender.signedBroadcast(exchangeTx(smartAssetPair, matcherFee + 2 * smartFee, matcherFee + 2 * smartFee, ntpTime, 3, 2, acc1, acc0, acc2),
waitForTx = true)
withClue("check fee for smart accounts and smart AssetPair - extx.fee == 0.015.zbs") {
setContracts((sc1, acc0), (sc1, acc1), (sc1, acc2))
assertBadRequestAndMessage(
- sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2)),
+ sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 2, 2, acc1, acc0, acc2)),
"com.zbsnetwork.transaction.assets.exchange.ExchangeTransactionV2 does not exceed minimal value of 1500000"
)
- sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + 2 * smartFee, smartMatcherFee + 2 * smartFee, ntpTime, acc1, acc0, acc2),
- waitForTx = true)
+ sender.signedBroadcast(
+ exchangeTx(smartAssetPair, smartMatcherFee + 2 * smartFee, smartMatcherFee + 2 * smartFee, ntpTime, 2, 3, acc1, acc0, acc2),
+ waitForTx = true)
setContracts((None, acc0), (None, acc1), (None, acc2))
}
@@ -138,7 +139,7 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail
priceAsset = None
)
assertBadRequestAndMessage(
- sender.signedBroadcast(exchangeTx(incorrectSmartAssetPair, smartMatcherFee, smartMatcherFee, ntpTime, acc1, acc0, acc2)),
+ sender.signedBroadcast(exchangeTx(incorrectSmartAssetPair, smartMatcherFee, smartMatcherFee, ntpTime, 3, 2, acc1, acc0, acc2)),
errNotAllowedByToken)
}
@@ -157,7 +158,7 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail
val smartPair = AssetPair(ByteStr.decodeBase58(asset).toOption, None)
- sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee, smartMatcherFee, ntpTime, acc1, acc0, acc2), waitForTx = true)
+ sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee, smartMatcherFee, ntpTime, 2, 3, acc1, acc0, acc2), waitForTx = true)
}
}
}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala
index 1a53bd7..caeef3f 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala
@@ -238,15 +238,15 @@ class DataTransactionSuite extends BaseTransactionSuite {
"Duplicate keys found")
val extraValueData = List(BinaryDataEntry("key", ByteStr(Array.fill(MaxValueSize + 1)(1.toByte))))
- assertBadRequestAndResponse(sender.putData(firstAddress, extraValueData, calcDataFee(extraValueData)), TooBig)
+ assertBadRequestAndResponse(sender.putData(firstAddress, extraValueData, 1.zbs), TooBig)
nodes.waitForHeightArise()
val largeBinData = List.tabulate(5)(n => BinaryDataEntry(extraKey, ByteStr(Array.fill(MaxValueSize)(n.toByte))))
- assertBadRequestAndResponse(sender.putData(firstAddress, largeBinData, calcDataFee(largeBinData)), TooBig)
+ assertBadRequestAndResponse(sender.putData(firstAddress, largeBinData, 1.zbs), TooBig)
nodes.waitForHeightArise()
val largeStrData = List.tabulate(5)(n => StringDataEntry(extraKey, "A" * MaxValueSize))
- assertBadRequestAndResponse(sender.putData(firstAddress, largeStrData, calcDataFee(largeStrData)), TooBig)
+ assertBadRequestAndResponse(sender.putData(firstAddress, largeStrData, 1.zbs), TooBig)
nodes.waitForHeightArise()
val tooManyEntriesData = List.tabulate(MaxEntryCount + 1)(n => IntegerDataEntry("key", 88))
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala
index 7e95254..144f507 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala
@@ -3,51 +3,59 @@ package com.zbsnetwork.it.sync.transactions
import com.zbsnetwork.it.NTPTime
import com.zbsnetwork.it.api.SyncHttpApi._
import com.zbsnetwork.it.sync._
+import com.zbsnetwork.it.sync.smartcontract.exchangeTx
import com.zbsnetwork.it.transactions.BaseTransactionSuite
import com.zbsnetwork.it.util._
import com.zbsnetwork.transaction.assets.IssueTransactionV1
import com.zbsnetwork.transaction.assets.exchange._
+import play.api.libs.json.{JsNumber, JsString, Json}
class ExchangeTransactionSuite extends BaseTransactionSuite with NTPTime {
+ var exchAsset: IssueTransactionV1 = IssueTransactionV1
+ .selfSigned(
+ sender = sender.privateKey,
+ name = "myasset".getBytes(),
+ description = "my asset description".getBytes(),
+ quantity = someAssetAmount,
+ decimals = 2,
+ reissuable = true,
+ fee = 1.zbs,
+ timestamp = System.currentTimeMillis()
+ )
+ .right
+ .get
+
+ var pair: AssetPair = _
+
+ private val acc0 = pkByAddress(firstAddress)
+ private val acc1 = pkByAddress(secondAddress)
+ private val acc2 = pkByAddress(thirdAddress)
+
+ val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported
+ val transactionV2versions = for {
+ o1ver <- 1 to 3
+ o2ver <- 1 to 3
+ } yield (o1ver.toByte, o2ver.toByte, 2.toByte)
+
+ val versions = transactionV1versions +: transactionV2versions
+
test("cannot exchange non-issued assets") {
- for ((o1ver, o2ver, tver) <- Seq(
- (1: Byte, 1: Byte, 1: Byte),
- (1: Byte, 1: Byte, 2: Byte),
- (1: Byte, 2: Byte, 2: Byte),
- (2: Byte, 1: Byte, 2: Byte),
- (2: Byte, 2: Byte, 2: Byte)
- )) {
- val assetName = "myasset"
- val assetDescription = "my asset description"
-
- val IssueTx: IssueTransactionV1 = IssueTransactionV1
- .selfSigned(
- sender = sender.privateKey,
- name = assetName.getBytes(),
- description = assetDescription.getBytes(),
- quantity = someAssetAmount,
- decimals = 2,
- reissuable = true,
- fee = 1.zbs,
- timestamp = System.currentTimeMillis()
- )
- .right
- .get
+ for ((o1ver, o2ver, tver) <- versions) {
- val assetId = IssueTx.id().base58
+ val assetId = exchAsset.id().base58
- val buyer = pkByAddress(firstAddress)
- val seller = pkByAddress(secondAddress)
- val matcher = pkByAddress(thirdAddress)
+ val buyer = acc0
+ val seller = acc1
+ val matcher = acc2
val ts = ntpTime.correctedTime()
val expirationTimestamp = ts + Order.MaxLiveTime
val buyPrice = 2 * Order.PriceConstant
val sellPrice = 2 * Order.PriceConstant
val buyAmount = 1
val sellAmount = 1
- val assetPair = AssetPair.createAssetPair("ZBS", assetId).get
- val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver)
- val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver)
+ pair = AssetPair.createAssetPair("ZBS", assetId).get
+ val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver)
+ val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver)
val amount = 1
if (tver != 1) {
@@ -95,4 +103,98 @@ class ExchangeTransactionSuite extends BaseTransactionSuite with NTPTime {
}
+ test("negative - check orders v2 and v3 with exchange tx v1") {
+ if (sender.findTransactionInfo(exchAsset.id().base58).isEmpty) sender.postJson("/transactions/broadcast", exchAsset.json())
+ pair = AssetPair.createAssetPair("ZBS", exchAsset.id().base58).get
+
+ for ((o1ver, o2ver) <- Seq(
+ (2: Byte, 1: Byte),
+ (2: Byte, 3: Byte),
+ )) {
+ val tx = exchangeTx(pair, matcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2)
+ val sig = (Json.parse(tx.toString()) \ "proofs").as[Seq[JsString]].head
+ val changedTx = tx + ("version" -> JsNumber(1)) + ("signature" -> sig)
+ assertBadRequestAndMessage(sender.signedBroadcast(changedTx), "can only contain orders of version 1", 400)
+ }
+ }
+
+ test("exchange tx with orders v3") {
+ val buyer = acc0
+ val seller = acc1
+
+ val assetDescription = "my asset description"
+
+ val IssueTx: IssueTransactionV1 = IssueTransactionV1
+ .selfSigned(
+ sender = buyer,
+ name = "myasset".getBytes(),
+ description = assetDescription.getBytes(),
+ quantity = someAssetAmount,
+ decimals = 8,
+ reissuable = true,
+ fee = 1.zbs,
+ timestamp = System.currentTimeMillis()
+ )
+ .right
+ .get
+
+ val assetId = IssueTx.id()
+
+ sender.postJson("/transactions/broadcast", IssueTx.json())
+
+ nodes.waitForHeightAriseAndTxPresent(assetId.base58)
+
+ for ((o1ver, o2ver, matcherFeeOrder1, matcherFeeOrder2) <- Seq(
+ (1: Byte, 3: Byte, None, Some(assetId)),
+ (1: Byte, 3: Byte, None, None),
+ (2: Byte, 3: Byte, None, Some(assetId)),
+ (3: Byte, 1: Byte, Some(assetId), None),
+ (2: Byte, 3: Byte, None, None),
+ (3: Byte, 2: Byte, Some(assetId), None),
+ )) {
+
+ val matcher = pkByAddress(thirdAddress)
+ val ts = ntpTime.correctedTime()
+ val expirationTimestamp = ts + Order.MaxLiveTime
+ var assetBalanceBefore: Long = 0l
+
+ if (matcherFeeOrder1.isEmpty && matcherFeeOrder2.isDefined) {
+ assetBalanceBefore = sender.assetBalance(secondAddress, assetId.base58).balance
+ sender.transfer(buyer.address, seller.address, 100000, minFee, Some(assetId.base58), waitForTx = true)
+ }
+
+ val buyPrice = 500000
+ val sellPrice = 500000
+ val buyAmount = 40000000
+ val sellAmount = 40000000
+ val assetPair = AssetPair.createAssetPair("ZBS", assetId.base58).get
+ val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver, matcherFeeOrder1)
+ val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver, matcherFeeOrder2)
+ val amount = 40000000
+
+ val tx =
+ ExchangeTransactionV2
+ .create(
+ matcher = matcher,
+ buyOrder = buy,
+ sellOrder = sell,
+ amount = amount,
+ price = sellPrice,
+ buyMatcherFee = (BigInt(matcherFee) * amount / buy.amount).toLong,
+ sellMatcherFee = (BigInt(matcherFee) * amount / sell.amount).toLong,
+ fee = matcherFee,
+ timestamp = ntpTime.correctedTime()
+ )
+ .right
+ .get
+
+ sender.postJson("/transactions/broadcast", tx.json())
+
+ nodes.waitForHeightAriseAndTxPresent(tx.id().base58)
+
+ if (matcherFeeOrder1.isEmpty && matcherFeeOrder2.isDefined) {
+ sender.assetBalance(secondAddress, assetId.base58).balance shouldBe assetBalanceBefore
+ }
+ }
+ }
}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala
index e6ec24d..5725121 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala
@@ -11,7 +11,8 @@ import com.zbsnetwork.it.sync.{someAssetAmount, _}
import com.zbsnetwork.it.transactions.BaseTransactionSuite
import com.zbsnetwork.it.util._
import com.zbsnetwork.state._
-import com.zbsnetwork.transaction.{CreateAliasTransaction, DataTransaction, GenesisTransaction, PaymentTransaction}
+import com.zbsnetwork.transaction.assets.exchange.AssetPair.extractAssetId
+import com.zbsnetwork.transaction._
import com.zbsnetwork.transaction.assets.{BurnTransaction, IssueTransaction, ReissueTransaction, SponsorFeeTransaction}
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order, _}
import com.zbsnetwork.transaction.lease.{LeaseCancelTransaction, LeaseTransaction}
@@ -285,7 +286,7 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime {
"version" -> 1,
"sender" -> firstAddress,
"assetId" -> assetId,
- "minSponsoredAssetFee" -> 100
+ "minSponsoredAssetFee" -> 500
),
usesProofs = true,
version = 1
@@ -343,13 +344,25 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime {
version = 1
)
- for ((o1ver, o2ver, tver) <- Seq(
- (1: Byte, 1: Byte, 1: Byte),
- (1: Byte, 1: Byte, 2: Byte),
- (1: Byte, 2: Byte, 2: Byte),
- (2: Byte, 1: Byte, 2: Byte),
- (2: Byte, 2: Byte, 2: Byte)
- )) {
+ val assetId = extractAssetId(issueTx).get
+
+ val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported
+ val transactionV2versions = for {
+ o1ver <- 1 to 3
+ o2ver <- 1 to 3
+ } yield (o1ver.toByte, o2ver.toByte, 2.toByte)
+
+ val versionsWithZbsFee =
+ (transactionV1versions +: transactionV2versions)
+ .map { case (o1ver, o2ver, tver) => (o1ver, o2ver, tver, Option.empty[AssetId], Option.empty[AssetId]) }
+
+ val versionsWithAssetFee = for {
+ o2ver <- 1 to 3
+ buyMatcherFeeAssetId = assetId
+ sellMatcherFeeAssetId = Option.empty[AssetId]
+ } yield (3.toByte, o2ver.toByte, 2.toByte, buyMatcherFeeAssetId, sellMatcherFeeAssetId)
+
+ for ((o1ver, o2ver, tver, matcherFeeOrder1, matcherFeeOrder2) <- versionsWithZbsFee ++ versionsWithAssetFee) {
val buyer = pkByAddress(firstAddress)
val seller = pkByAddress(secondAddress)
val matcher = pkByAddress(thirdAddress)
@@ -361,8 +374,8 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime {
val buyAmount = 2
val sellAmount = 3
val assetPair = AssetPair.createAssetPair("ZBS", issueTx).get
- val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, mf, o1ver)
- val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, mf, o2ver)
+ val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, mf, o1ver, matcherFeeOrder1)
+ val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, mf, o2ver, matcherFeeOrder2)
val amount = math.min(buy.amount, sell.amount)
val tx =
@@ -397,6 +410,9 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime {
.explicitGet()
.json()
}
+ val s = sell.getReceiveAmount(amount, sellPrice).right.get
+ log.info(s"SELLER: ${s}")
+ log.info(s"BUYER: ${buy.getReceiveAmount(amount, sellPrice).right.get}")
val txId = sender.signedBroadcast(tx).id
sender.waitForTransaction(txId)
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala
new file mode 100644
index 0000000..987ea02
--- /dev/null
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala
@@ -0,0 +1,135 @@
+package com.zbsnetwork.it.sync.transactions
+
+import com.typesafe.config.Config
+import com.zbsnetwork.account.Address
+import com.zbsnetwork.common.utils._
+import com.zbsnetwork.it.api.SyncHttpApi._
+import com.zbsnetwork.it.api.TransactionInfo
+import com.zbsnetwork.it.transactions.NodesFromDocker
+import com.zbsnetwork.it.{Node, NodeConfigs, ReportingTestName}
+import com.zbsnetwork.transaction.transfer.{TransferTransaction, TransferTransactionV1}
+import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers}
+import play.api.libs.json.JsNumber
+import scala.concurrent.duration._
+
+class TransactionAPISuite extends FreeSpec with NodesFromDocker with Matchers with ReportingTestName with CancelAfterFailure {
+
+ override def nodeConfigs: Seq[Config] =
+ NodeConfigs.newBuilder
+ .overrideBase(_.quorum(0))
+ .overrideBase(_.raw("zbs.rest-api.transactions-by-address-limit=10"))
+ .withDefault(1)
+ .withSpecial(1, _.nonMiner)
+ .buildNonConflicting()
+
+ val sender: Node = nodes.head
+ val recipient: Address = Address.fromString(sender.createAddress()).explicitGet()
+
+ val Zbs: Long = 100000000L
+
+ val AMT: Long = 1 * Zbs
+ val FEE: Long = (0.001 * Zbs).toLong
+
+ val transactions: List[TransferTransaction] =
+ (for (i <- 0 to 30) yield {
+ TransferTransactionV1
+ .selfSigned(
+ None,
+ sender.privateKey,
+ recipient,
+ AMT,
+ System.currentTimeMillis() + i,
+ None,
+ FEE + i * 100,
+ Array.emptyByteArray
+ )
+ .explicitGet()
+ }).toList
+
+ val transactionIds = transactions.map(_.id().base58)
+
+ "should accept transactions" in {
+ transactions.foreach { tx =>
+ sender.broadcastRequest(tx.json() + ("type" -> JsNumber(tx.builder.typeId.toInt)))
+ }
+
+ val h = sender.height
+
+ sender.waitForHeight(h + 3, 2.minutes)
+ }
+
+ "should return correct N txs on request without `after`" in {
+
+ def checkForLimit(limit: Int): Unit = {
+ val expected =
+ transactionIds
+ .take(limit)
+
+ val received =
+ sender
+ .transactionsByAddress(recipient.address, limit)
+ .flatten
+ .map(_.id)
+
+ expected shouldEqual received
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ "should return correct N txs on request with `after`" in {
+
+ def checkForLimit(limit: Int): Unit = {
+ val expected =
+ transactionIds
+ .slice(limit, limit + limit)
+
+ val afterParam =
+ transactions
+ .drop(limit - 1)
+ .head
+ .id()
+ .base58
+
+ val received =
+ sender
+ .transactionsByAddress(recipient.address, limit, afterParam)
+ .flatten
+ .map(_.id)
+
+ expected shouldEqual received
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ "should return all transactions" in {
+ def checkForLimit(limit: Int): Unit = {
+ val received =
+ loadAll(sender, recipient.address, limit, None, Nil)
+ .map(_.id)
+
+ received shouldEqual transactionIds
+ }
+
+ for (limit <- 2 to 10 by 1) {
+ checkForLimit(limit)
+ }
+ }
+
+ def loadAll(node: Node, address: String, limit: Int, maybeAfter: Option[String], acc: List[TransactionInfo]): List[TransactionInfo] = {
+ val txs = maybeAfter match {
+ case None => node.transactionsByAddress(address, limit).flatten.toList
+ case Some(lastId) => node.transactionsByAddress(address, limit, lastId).flatten.toList
+ }
+
+ txs.lastOption match {
+ case None => acc ++ txs
+ case Some(tx) => loadAll(node, address, limit, Some(tx.id), acc ++ txs)
+ }
+ }
+}
diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala
index fa6a971..ee620a4 100644
--- a/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala
+++ b/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala
@@ -14,7 +14,11 @@ import scorex.crypto.encode.Base64
import com.zbsnetwork.common.utils.Base58
import com.zbsnetwork.it.sync._
import com.zbsnetwork.it.util._
+import com.zbsnetwork.lang.v1.FunctionHeader
+import com.zbsnetwork.lang.v1.compiler.Terms
+import com.zbsnetwork.lang.v1.compiler.Terms.TRUE
import com.zbsnetwork.transaction.smart.SetScriptTransaction
+import com.zbsnetwork.transaction.smart.ContractInvocationTransaction
import com.zbsnetwork.transaction.smart.script.Script
import com.zbsnetwork.transaction.transfer.{MassTransferTransaction, TransferTransactionV1, TransferTransactionV2}
import com.zbsnetwork.transaction.transfer.MassTransferTransaction.Transfer
@@ -349,6 +353,22 @@ class TransactionSerializeSuite extends BaseTransactionSuite with TableDrivenPro
.right
.get
+ private val contractInvocation = ContractInvocationTransaction
+ .create(
+ PublicKeyAccount.fromBase58String("BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ").right.get,
+ PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").right.get,
+ Terms.FUNCTION_CALL(
+ function = FunctionHeader.User("testfunc"),
+ args = List(TRUE)
+ ),
+ Some(ContractInvocationTransaction.Payment(7, Some(ByteStr.decodeBase58("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").get))),
+ smartMinFee,
+ ts,
+ Proofs(Seq(ByteStr.decodeBase58("4bfDaqBcnK3hT8ywFEFndxtS1DTSYfncUqd4s5Vyaa66PZHawtC73rDswUur6QZu5RpqM7L9NFgBHT1vhCoox4vi").get))
+ )
+ .right
+ .get
+
forAll(
Table(
("tx", "name"),
@@ -373,6 +393,7 @@ class TransactionSerializeSuite extends BaseTransactionSuite with TableDrivenPro
(sponsor, "sponsor"),
(transferV1, "transferV1"),
(transferV2, "transferV2"),
+ (contractInvocation, "contractInvocation")
)) { (tx, name) =>
test(s"Serialize check of $name transaction") {
val r = sender.transactionSerializer(tx.json()).bytes.map(_.toByte)
diff --git a/lang/js/src/main/scala/JsAPI.scala b/lang/js/src/main/scala/JsAPI.scala
index 380ba1a..8c54a86 100644
--- a/lang/js/src/main/scala/JsAPI.scala
+++ b/lang/js/src/main/scala/JsAPI.scala
@@ -2,8 +2,9 @@ import cats.kernel.Monoid
import com.zbsnetwork.lang.StdLibVersion.{StdLibVersion, _}
import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.lang.directives.DirectiveParser
-import com.zbsnetwork.lang.utils.{extractScriptType, extractStdLibVersion}
+import com.zbsnetwork.lang.utils.{extractContentType, extractScriptType, extractStdLibVersion}
import com.zbsnetwork.lang.v1.CTX
+import com.zbsnetwork.lang.v1.ContractLimits
import com.zbsnetwork.lang.v1.FunctionHeader.{Native, User}
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.Types._
@@ -11,7 +12,7 @@ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
import com.zbsnetwork.lang.v1.traits.domain.{Recipient, Tx}
import com.zbsnetwork.lang.v1.traits.{DataType, Environment}
-import com.zbsnetwork.lang.{Global, ScriptType}
+import com.zbsnetwork.lang.{ContentType, Global, ScriptType, StdLibVersion}
import scala.scalajs.js
import scala.scalajs.js.Dynamic.{literal => jObj}
@@ -84,52 +85,72 @@ object JsAPI {
Monoid.combineAll(Seq(PureContext.build(v), cryptoContext, zbsContext(V3)))
}
- @JSExportTopLevel("fullContext")
- val fullContext: CTX =
- buildScriptContext(V3, isTokenContext = false)
-
@JSExportTopLevel("getTypes")
- def getTypes() = fullContext.types.map(v => js.Dynamic.literal("name" -> v.name, "type" -> typeRepr(v.typeRef))).toJSArray
+ def getTypes(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] =
+ buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).types
+ .map(v => js.Dynamic.literal("name" -> v.name, "type" -> typeRepr(v.typeRef)))
+ .toJSArray
@JSExportTopLevel("getVarsDoc")
- def getVarsDoc() = fullContext.vars.map(v => js.Dynamic.literal("name" -> v._1, "type" -> typeRepr(v._2._1._1), "doc" -> v._2._1._2)).toJSArray
+ def getVarsDoc(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] =
+ buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).vars
+ .map(v => js.Dynamic.literal("name" -> v._1, "type" -> typeRepr(v._2._1._1), "doc" -> v._2._1._2))
+ .toJSArray
@JSExportTopLevel("getFunctionsDoc")
- def getFunctionnsDoc() =
- fullContext.functions
- .map(
- f =>
- js.Dynamic.literal(
- "name" -> f.name,
- "doc" -> f.docString,
- "resultType" -> typeRepr(f.signature.result),
- "args" -> ((f.argsDoc zip f.signature.args) map { arg =>
- js.Dynamic.literal("name" -> arg._1._1, "type" -> typeRepr(arg._2._2), "doc" -> arg._1._2)
- }).toJSArray
- ))
+ def getFunctionsDoc(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] =
+ buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).functions
+ .map(f =>
+ js.Dynamic.literal(
+ "name" -> f.name,
+ "doc" -> f.docString,
+ "resultType" -> typeRepr(f.signature.result),
+ "args" -> ((f.argsDoc zip f.signature.args) map { arg =>
+ js.Dynamic.literal("name" -> arg._1._1, "type" -> typeRepr(arg._2._2), "doc" -> arg._1._2)
+ }).toJSArray
+ ))
.toJSArray
- @JSExportTopLevel("compilerContext")
- val compilerContext = fullContext.compilerContext
+ @JSExportTopLevel("contractLimits")
+ def contractLimits(): js.Dynamic = js.Dynamic.literal(
+ "MaxExprComplexity" -> ContractLimits.MaxExprComplexity,
+ "MaxExprSizeInBytes" -> ContractLimits.MaxExprSizeInBytes,
+ "MaxContractComplexity" -> ContractLimits.MaxContractComplexity,
+ "MaxContractSizeInBytes" -> ContractLimits.MaxContractSizeInBytes,
+ "MaxContractInvocationArgs" -> ContractLimits.MaxContractInvocationArgs,
+ "MaxContractInvocationSizeInBytes" -> ContractLimits.MaxContractInvocationSizeInBytes,
+ "MaxWriteSetSizeInBytes" -> ContractLimits.MaxWriteSetSizeInBytes,
+ "MaxPaymentAmount" -> ContractLimits.MaxPaymentAmount
+ )
- @JSExportTopLevel("compile")
- def compile(input: String, isTokenScript: Boolean): js.Dynamic = {
+ @JSExportTopLevel("scriptInfo")
+ def scriptInfo(input: String): js.Dynamic = {
val directives = DirectiveParser(input)
+ val info = for {
+ ver <- extractStdLibVersion(directives)
+ contentType <- extractContentType(directives)
+ scriptType <- extractScriptType(directives)
+ } yield js.Dynamic.literal("stdLibVersion" -> ver, "contentType" -> contentType, "scriptType" -> scriptType)
- val scriptWithoutDirectives =
- input.linesIterator
- .filter(str => !str.contains("{-#"))
- .mkString("\n")
+ info.fold(
+ err => js.Dynamic.literal("error" -> err),
+ identity
+ )
+ }
+ @JSExportTopLevel("compile")
+ def compile(input: String): js.Dynamic = {
+ val directives = DirectiveParser(input)
val compiled = for {
- ver <- extractStdLibVersion(directives)
- tpe <- extractScriptType(directives)
+ ver <- extractStdLibVersion(directives)
+ contentType <- extractContentType(directives)
+ scriptType <- extractScriptType(directives)
} yield {
- tpe match {
- case ScriptType.Expression =>
- val ctx = buildScriptContext(ver, isTokenScript)
+ contentType match {
+ case ContentType.Expression =>
+ val ctx = buildScriptContext(ver, scriptType == ScriptType.Asset)
Global
- .compileScript(scriptWithoutDirectives, ctx.compilerContext)
+ .compileScript(input, ctx.compilerContext)
.fold(
err => {
js.Dynamic.literal("error" -> err)
@@ -138,10 +159,10 @@ object JsAPI {
js.Dynamic.literal("result" -> Global.toBuffer(bytes), "ast" -> toJs(ast))
}
)
- case ScriptType.Contract =>
+ case ContentType.Contract =>
// Just ignore stdlib version here
Global
- .compileContract(scriptWithoutDirectives, fullContractContext.compilerContext)
+ .compileContract(input, fullContractContext.compilerContext)
.fold(
err => {
js.Dynamic.literal("error" -> err)
diff --git a/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala b/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala
index e6f81c2..e696072 100644
--- a/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala
+++ b/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala
@@ -4,7 +4,7 @@ import com.zbsnetwork.lang.v1.CTX
import com.zbsnetwork.lang.v1.compiler.Types._
import com.zbsnetwork.lang.v1.evaluator.ctx._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext, _}
+import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
import com.zbsnetwork.lang.v1.traits.domain.{Recipient, Tx}
import com.zbsnetwork.lang.v1.traits.{DataType, Environment}
import com.zbsnetwork.lang.{Global, StdLibVersion}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala
index e549044..5d97fda 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala
@@ -2,6 +2,7 @@ package com.zbsnetwork.lang
import cats.syntax.monoid._
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.state.diffs.ProduceError._
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.lang.Common.{NoShrink, sampleTypes}
import com.zbsnetwork.lang.v1.compiler.{ContractCompiler, Terms}
@@ -18,13 +19,13 @@ import org.scalatest.{Matchers, PropSpec}
class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGen with Matchers with NoShrink {
- property("Simple test") {
- val ctx: CTX =
- PureContext.build(StdLibVersion.V3) |+|
- CTX(sampleTypes, Map.empty, Array.empty) |+|
- ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false)
+ val ctx: CTX =
+ PureContext.build(StdLibVersion.V3) |+|
+ CTX(sampleTypes, Map.empty, Array.empty) |+|
+ ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false)
- val src =
+ property("Simple call") {
+ parseCompileAndEvaluate(
"""
|
|func fooHelper2() = {
@@ -36,11 +37,11 @@ class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGe
|}
|
|@Callable(invocation)
- |func foo(a:ByteStr) = {
+ |func foo(a:ByteVector) = {
| let x = invocation.caller.bytes
| if (fooHelper())
- | then WriteSet(List(DataEntry("b", 1), DataEntry("sender", x)))
- | else WriteSet(List(DataEntry("a", a), DataEntry("sender", x)))
+ | then WriteSet([DataEntry("b", 1), DataEntry("sender", x)])
+ | else WriteSet([DataEntry("a", a), DataEntry("sender", x)])
|}
|
|@Verifier(t)
@@ -48,27 +49,55 @@ class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGe
| true
|}
|
- """.stripMargin
-
- val parsed = Parser.parseContract(src).get.value
-
- val compiled = ContractCompiler(ctx.compilerContext, parsed).explicitGet()
-
- val expectedResult = ContractResult(
+ """.stripMargin,
+ "foo"
+ ).explicitGet() shouldBe ContractResult(
List(
DataItem.Bin("a", ByteStr.empty),
DataItem.Bin("sender", ByteStr.empty)
),
List()
)
+ }
+
+ property("Callable can have 22 args") {
+ parseCompileAndEvaluate(
+ """
+ |@Callable(invocation)
+ |func foo(a1:Int, a2:Int, a3:Int, a4:Int, a5:Int, a6:Int, a7:Int, a8:Int, a9:Int, a10:Int,
+ | a11:Int, a12:Int, a13:Int, a14:Int, a15:Int, a16:Int, a17:Int, a18:Int, a19:Int, a20:Int,
+ | a21:Int, a22:Int) = { WriteSet([DataEntry(toString(a1), a22)]) }
+ """.stripMargin,
+ "foo",
+ Range(1, 23).map(i => Terms.CONST_LONG(i)).toList
+ ).explicitGet() shouldBe ContractResult(List(DataItem.Lng("1", 22)), List())
+ }
- val result = ContractEvaluator(
+ property("Callable can't have more than 22 args") {
+ val src =
+ """
+ |@Callable(invocation)
+ |func foo(a1:Int, a2:Int, a3:Int, a4:Int, a5:Int, a6:Int, a7:Int, a8:Int, a9:Int, a10:Int,
+ | a11:Int, a12:Int, a13:Int, a14:Int, a15:Int, a16:Int, a17:Int, a18:Int, a19:Int, a20:Int,
+ | a21:Int, a22:Int, a23:Int) = { throw() }
+ """.stripMargin
+
+ val parsed = Parser.parseContract(src).get.value
+
+ ContractCompiler(ctx.compilerContext, parsed) should produce("no more than 22 arguments")
+ }
+
+ def parseCompileAndEvaluate(script: String,
+ func: String,
+ args: List[Terms.EXPR] = List(Terms.CONST_BYTESTR(ByteStr.empty))): Either[ExecutionError, ContractResult] = {
+ val parsed = Parser.parseContract(script).get.value
+ val compiled = ContractCompiler(ctx.compilerContext, parsed).explicitGet()
+
+ ContractEvaluator(
ctx.evaluationContext,
compiled,
- Invocation(Terms.FUNCTION_CALL(FunctionHeader.User("foo"), List(Terms.CONST_BYTESTR(ByteStr.empty))), ByteStr.empty, None, ByteStr.empty)
- ).explicitGet()
-
- result shouldBe expectedResult
+ Invocation(Terms.FUNCTION_CALL(FunctionHeader.User(func), args), ByteStr.empty, None, ByteStr.empty)
+ )
}
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala
index f8a386f..25c3c6c 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala
@@ -185,4 +185,37 @@ class ContractParserTest extends PropSpec with PropertyChecks with Matchers with
|""".stripMargin
parse(code)
}
+
+ property("parse directives as comments (ignore)") {
+ val code =
+ """
+ | # comment
+ | {-# STDLIB_VERSION 3 #-}
+ | {-# TEST_TEST 123 #-}
+ | # comment
+ |
+ | @Ann(foo)
+ | func bar(arg:Baz) = {
+ | 3
+ | }
+ |
+ |
+ |""".stripMargin
+ parse(code) shouldBe CONTRACT(
+ AnyPos,
+ List.empty,
+ List(
+ ANNOTATEDFUNC(
+ AnyPos,
+ List(Expressions.ANNOTATION(AnyPos, PART.VALID(AnyPos, "Ann"), List(PART.VALID(AnyPos, "foo")))),
+ Expressions.FUNC(
+ AnyPos,
+ PART.VALID(AnyPos, "bar"),
+ List((PART.VALID(AnyPos, "arg"), List(PART.VALID(AnyPos, "Baz")))),
+ CONST_LONG(AnyPos, 3)
+ )
+ )
+ )
+ )
+ }
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala
index 33213e4..3122a30 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala
@@ -9,7 +9,7 @@ class DirectiveParserTest extends PropSpec with PropertyChecks with Matchers {
def parse(s: String): List[Directive] = DirectiveParser(s)
- property("parse STDLIB_VERSION directive") {
+ property("parse directives") {
parse("{-# STDLIB_VERSION 10 #-}") shouldBe List(Directive(STDLIB_VERSION, "10"))
parse("""
|
@@ -18,8 +18,13 @@ class DirectiveParserTest extends PropSpec with PropertyChecks with Matchers {
""".stripMargin) shouldBe List(Directive(STDLIB_VERSION, "10"))
parse("""
|
- |{-# SCRIPT_TYPE FOO #-}
+ |{-# CONTENT_TYPE FOO #-}
|
- """.stripMargin) shouldBe List(Directive(SCRIPT_TYPE, "FOO"))
+ """.stripMargin) shouldBe List(Directive(CONTENT_TYPE, "FOO"))
+ parse("""
+ |
+ |{-# SCRIPT_TYPE BAR #-}
+ |
+ """.stripMargin) shouldBe List(Directive(SCRIPT_TYPE, "BAR"))
}
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala
index 34d6efb..3090b17 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala
@@ -7,8 +7,8 @@ import cats.kernel.Monoid
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.{Base58, Base64, EitherExt2}
import com.zbsnetwork.lang.Common._
-import com.zbsnetwork.lang.Testing._
import com.zbsnetwork.lang.StdLibVersion._
+import com.zbsnetwork.lang.Testing._
import com.zbsnetwork.lang.v1.compiler.ExpressionCompiler
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.Types._
@@ -36,6 +36,8 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc
private val defaultCryptoContext = CryptoContext.build(Global)
+ val blockBuilder: Gen[(LET, EXPR) => EXPR] = Gen.oneOf(true, false).map(if (_) (BLOCK.apply _) else (LET_BLOCK.apply _))
+
private def defaultFullContext(environment: Environment): CTX = Monoid.combineAll(
Seq(
defaultCryptoContext,
@@ -49,7 +51,7 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc
private def ev[T <: EVALUATED](context: EvaluationContext = pureEvalContext, expr: EXPR): Either[ExecutionError, T] =
EvaluatorV1[T](context, expr)
- private def simpleDeclarationAndUsage(i: Int) = BLOCK(LET("x", CONST_LONG(i)), REF("x"))
+ private def simpleDeclarationAndUsage(i: Int, blockBuilder: (LET, EXPR) => EXPR) = blockBuilder(LET("x", CONST_LONG(i)), REF("x"))
property("successful on very deep expressions (stack overflow check)") {
val term = (1 to 100000).foldLeft[EXPR](CONST_LONG(0))((acc, _) => FUNCTION_CALL(sumLong.header, List(acc, CONST_LONG(1))))
@@ -58,80 +60,101 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc
}
property("return error and log of failed evaluation") {
- val (log, Left(err)) = EvaluatorV1.applywithLogging[EVALUATED](
- pureEvalContext,
- expr = BLOCK(
- LET("x", CONST_LONG(3)),
- BLOCK(
- LET("x", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))),
- FUNCTION_CALL(PureContext.eq.header, List(REF("z"), CONST_LONG(1)))
+ forAll(blockBuilder) { block =>
+ val (log, Left(err)) = EvaluatorV1.applywithLogging[EVALUATED](
+ pureEvalContext,
+ expr = block(
+ LET("x", CONST_LONG(3)),
+ block(
+ LET("x", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))),
+ FUNCTION_CALL(PureContext.eq.header, List(REF("z"), CONST_LONG(1)))
+ )
)
)
- )
- val expectedError = "A definition of 'z' not found"
+ val expectedError = "A definition of 'z' not found"
+ err shouldBe expectedError
+ log.isEmpty shouldBe true
+ }
- err shouldBe expectedError
- log.isEmpty shouldBe true
}
property("successful on unused let") {
- ev[EVALUATED](
- expr = BLOCK(
- LET("x", CONST_LONG(3)),
- CONST_LONG(3)
- )) shouldBe evaluated(3)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ expr = block(
+ LET("x", CONST_LONG(3)),
+ CONST_LONG(3)
+ )) shouldBe evaluated(3)
+ }
}
property("successful on x = y") {
- ev[EVALUATED](
- expr = BLOCK(LET("x", CONST_LONG(3)),
- BLOCK(
- LET("y", REF("x")),
- FUNCTION_CALL(sumLong.header, List(REF("x"), REF("y")))
- ))) shouldBe evaluated(6)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ expr = block(LET("x", CONST_LONG(3)),
+ block(
+ LET("y", REF("x")),
+ FUNCTION_CALL(sumLong.header, List(REF("x"), REF("y")))
+ ))) shouldBe evaluated(6)
+ }
}
property("successful on simple get") {
- ev[EVALUATED](expr = simpleDeclarationAndUsage(3)) shouldBe evaluated(3)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](expr = simpleDeclarationAndUsage(3, block)) shouldBe evaluated(3)
+ }
}
property("successful on get used further in expr") {
- ev[EVALUATED](
- expr = BLOCK(
- LET("x", CONST_LONG(3)),
- FUNCTION_CALL(PureContext.eq.header, List(REF("x"), CONST_LONG(2)))
- )) shouldBe evaluated(false)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ expr = block(
+ LET("x", CONST_LONG(3)),
+ FUNCTION_CALL(PureContext.eq.header, List(REF("x"), CONST_LONG(2)))
+ )) shouldBe evaluated(false)
+ }
}
property("successful on multiple lets") {
- ev[EVALUATED](
- expr = BLOCK(
- LET("x", CONST_LONG(3)),
- BLOCK(LET("y", CONST_LONG(3)), FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y"))))
- )) shouldBe evaluated(true)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ expr = block(
+ LET("x", CONST_LONG(3)),
+ block(LET("y", CONST_LONG(3)), FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y"))))
+ )) shouldBe evaluated(true)
+ }
}
property("successful on multiple lets with expression") {
- ev[EVALUATED](
- expr = BLOCK(
- LET("x", CONST_LONG(3)),
- BLOCK(
- LET("y", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))),
- FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y")))
- )
- )) shouldBe evaluated(true)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ expr = block(
+ LET("x", CONST_LONG(3)),
+ block(
+ LET("y", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))),
+ FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y")))
+ )
+ )) shouldBe evaluated(true)
+ }
}
property("successful on deep type resolution") {
- ev[EVALUATED](expr = IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), simpleDeclarationAndUsage(3), CONST_LONG(4))) shouldBe evaluated(
- 4)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](expr = IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))),
+ simpleDeclarationAndUsage(3, block),
+ CONST_LONG(4))) shouldBe evaluated(4)
+ }
}
property("successful on same value names in different branches") {
- val expr =
- IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), simpleDeclarationAndUsage(3), simpleDeclarationAndUsage(4))
- ev[EVALUATED](expr = expr) shouldBe evaluated(4)
+ forAll(blockBuilder) { block =>
+ val expr =
+ IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))),
+ simpleDeclarationAndUsage(3, block),
+ simpleDeclarationAndUsage(4, block))
+ ev[EVALUATED](expr = expr) shouldBe evaluated(4)
+ }
}
property("fails if definition not found") {
@@ -173,32 +196,37 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc
functions = Map.empty
)
)
- ev[EVALUATED](
- context = context,
- expr = BLOCK(LET("Z", REF("badVal")), FUNCTION_CALL(sumLong.header, List(GETTER(REF("p"), "X"), CONST_LONG(2))))
- ) shouldBe evaluated(5)
+ forAll(blockBuilder) { block =>
+ ev[EVALUATED](
+ context = context,
+ expr = block(LET("Z", REF("badVal")), FUNCTION_CALL(sumLong.header, List(GETTER(REF("p"), "X"), CONST_LONG(2))))
+ ) shouldBe evaluated(5)
+ }
}
property("let is evaluated maximum once") {
- var functionEvaluated = 0
+ forAll(blockBuilder) { block =>
+ var functionEvaluated = 0
- val f = NativeFunction("F", 1: Long, 258: Short, LONG: TYPE, "test function", Seq(("_", LONG, "")): _*) { _ =>
- functionEvaluated = functionEvaluated + 1
- evaluated(1L)
- }
+ val f = NativeFunction("F", 1: Long, 258: Short, LONG: TYPE, "test function", Seq(("_", LONG, "")): _*) { _ =>
+ functionEvaluated = functionEvaluated + 1
+ evaluated(1L)
+ }
- val context = Monoid.combine(pureEvalContext,
- EvaluationContext(
- typeDefs = Map.empty,
- letDefs = Map.empty,
- functions = Map(f.header -> f)
- ))
- ev[EVALUATED](
- context = context,
- expr = BLOCK(LET("X", FUNCTION_CALL(f.header, List(CONST_LONG(1000)))), FUNCTION_CALL(sumLong.header, List(REF("X"), REF("X"))))
- ) shouldBe evaluated(2L)
+ val context = Monoid.combine(pureEvalContext,
+ EvaluationContext(
+ typeDefs = Map.empty,
+ letDefs = Map.empty,
+ functions = Map(f.header -> f)
+ ))
- functionEvaluated shouldBe 1
+ ev[EVALUATED](
+ context = context,
+ expr = block(LET("X", FUNCTION_CALL(f.header, List(CONST_LONG(1000)))), FUNCTION_CALL(sumLong.header, List(REF("X"), REF("X"))))
+ ) shouldBe evaluated(2L)
+
+ functionEvaluated shouldBe 1
+ }
}
property("successful on ref getter evaluation") {
@@ -261,15 +289,16 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc
)
)
- val expr = GETTER(
- BLOCK(
- LET("fooInstance", FUNCTION_CALL(fooCtor.header, List.empty)),
- FUNCTION_CALL(fooTransform.header, List(REF("fooInstance")))
- ),
- "bar"
- )
-
- ev[EVALUATED](context, expr) shouldBe evaluated("TRANSFORMED_BAR")
+ forAll(blockBuilder) { block =>
+ val expr = GETTER(
+ block(
+ LET("fooInstance", FUNCTION_CALL(fooCtor.header, List.empty)),
+ FUNCTION_CALL(fooTransform.header, List(REF("fooInstance")))
+ ),
+ "bar"
+ )
+ ev[EVALUATED](context, expr) shouldBe evaluated("TRANSFORMED_BAR")
+ }
}
property("successful on simple function evaluation") {
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala
index d84aced..a7ccfb8 100755
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala
@@ -359,12 +359,28 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M
ev[CONST_LONG](context, expr) shouldBe evaluated(8)
}
- property("list constructor primitive") {
+ property("listN constructor primitive") {
val src =
"""
- |List(1,2)
+ |cons(1, cons(2, cons(3, cons(4, cons(5, nil)))))
""".stripMargin
- eval[EVALUATED](src) shouldBe evaluated(List(1, 2))
+ eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5))
+ }
+
+ property("listN constructor binary op") {
+ val src =
+ """
+ |1::2::3::4::5::nil
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5))
+ }
+
+ property("list syntax sugar") {
+ val src =
+ """
+ |[1,2,3, 4, 5]
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5))
}
property("list constructor for different data entries") {
@@ -373,7 +389,7 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M
|let x = DataEntry("foo",1)
|let y = DataEntry("bar","2")
|let z = DataEntry("baz","2")
- |List(x,y,z)
+ |[x,y,z]
""".stripMargin
eval[EVALUATED](src) shouldBe Right(
ARR(Vector(
@@ -383,4 +399,147 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M
)))
}
+ property("allow 'throw' in '==' arguments") {
+ val src =
+ """true == throw("test passed")"""
+ eval[EVALUATED](src) shouldBe Left("test passed")
+ }
+
+ property("ban to compare different types") {
+ val src =
+ """true == "test passed" """
+ eval[EVALUATED](src) should produce("Compilation failed: Can't match inferred types")
+ }
+
+ property("ensure user function: success") {
+ val src =
+ """
+ |let x = true
+ |ensure(x, "test fail")
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe Right(TRUE)
+ }
+
+ property("ensure user function: fail") {
+ val src =
+ """
+ |let x = false
+ |ensure(x, "test fail")
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe Left("test fail")
+ }
+
+ property("postfix syntax (one argument)") {
+ val src =
+ """
+ |let x = true
+ |x.ensure("test fail")
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe Right(TRUE)
+ }
+
+ property("postfix syntax (no arguments)") {
+ val src =
+ """unit.isDefined()"""
+ eval[EVALUATED](src) shouldBe Right(FALSE)
+ }
+
+ property("postfix syntax (many argument)") {
+ val src =
+ """ 5.fraction(7,2) """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(17L))
+ }
+
+ property("postfix syntax (users defined function)") {
+ val src =
+ """
+ |func dub(s:String) = { s+s }
+ |"qwe".dub()
+ """.stripMargin
+ eval[EVALUATED](src) shouldBe Right(CONST_STRING("qweqwe"))
+ }
+
+ property("extract UTF8 string") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toUtf8String() """
+ eval[EVALUATED](src) shouldBe Right(CONST_STRING("abcdefghi"))
+ }
+
+ property("extract Long") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toInt() """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(0x6162636465666768l))
+ }
+
+ property("extract Long by offset") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toInt(1) """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(0x6263646566676869l))
+ }
+
+ property("extract Long by offset (patrial)") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toInt(2) """
+ eval[EVALUATED](src) should produce("IndexOutOfBounds")
+ }
+
+ property("extract Long by offset (out of bounds)") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toInt(10) """
+ eval[EVALUATED](src) should produce("IndexOutOfBounds")
+ }
+
+ property("extract Long by offset (negative)") {
+ val src =
+ """ base58'2EtvziXsJaBRS'.toInt(-2) """
+ eval[EVALUATED](src) should produce("IndexOutOfBounds")
+ }
+
+ property("indexOf") {
+ val src =
+ """ "qweqwe".indexOf("we") """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(1L))
+ }
+
+ property("indexOf with start offset") {
+ val src =
+ """ "qweqwe".indexOf("we", 2) """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(4L))
+ }
+
+ property("indexOf (not present)") {
+ val src =
+ """ "qweqwe".indexOf("ww") """
+ eval[EVALUATED](src) shouldBe Right(unit)
+ }
+
+ property("split") {
+ val src =
+ """ "q:we:.;q;we:x;q.we".split(":.;") """
+ eval[EVALUATED](src) shouldBe Right(ARR(IndexedSeq(CONST_STRING("q:we"), CONST_STRING("q;we:x;q.we"))))
+ }
+
+ property("parseInt") {
+ val src =
+ """ "42".parseInt() """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(42L))
+ }
+
+ property("parseIntValue") {
+ val src =
+ """ "42".parseInt() """
+ eval[EVALUATED](src) shouldBe Right(CONST_LONG(42L))
+ }
+
+ property("parseInt fail") {
+ val src =
+ """ "x42".parseInt() """
+ eval[EVALUATED](src) shouldBe Right(unit)
+ }
+
+ property("parseIntValue fail") {
+ val src =
+ """ "x42".parseIntValue() """
+ eval[EVALUATED](src) shouldBe 'left
+ }
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala
index 8ff3014..76e1d0d 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala
@@ -5,7 +5,6 @@ import org.scalatest.{FreeSpec, Matchers}
import Common._
import com.zbsnetwork.lang.v1.compiler.TypeInferrer
import com.zbsnetwork.lang.v1.evaluator.ctx.CaseType
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl._
class TypeInferrerTest extends FreeSpec with Matchers {
@@ -125,6 +124,11 @@ class TypeInferrerTest extends FreeSpec with Matchers {
TypeInferrer(Seq((LONG, PARAMETERIZEDUNION(List(typeparamT, typeparamG))))) should produce("Can't resolve correct type")
}
}
+
+ "Lists" in {
+ TypeInferrer(Seq( /*(LONG, typeparamT),*/ (LIST(NOTHING), PARAMETERIZEDLIST(typeparamG)))) shouldBe Right(
+ Map( /*typeparamT -> LONG,*/ typeparamG -> NOTHING))
+ }
}
}
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala
index f17f578..cda1aad 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala
@@ -27,9 +27,9 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
"""
|
| @Callable(invocation)
- | func foo(a:ByteStr) = {
+ | func foo(a:ByteVector) = {
| let sender0 = invocation.caller.bytes
- | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0)))
+ | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)])
| }
|
| @Verifier(t)
@@ -49,14 +49,16 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
Terms.FUNC(
"foo",
List("a"),
- BLOCK(
+ LET_BLOCK(
LET("sender0", GETTER(GETTER(REF("invocation"), "caller"), "bytes")),
FUNCTION_CALL(
User(FieldNames.WriteSet),
List(FUNCTION_CALL(
- Native(1102),
- List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))),
- FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("sender0"))))
+ Native(1100),
+ List(
+ FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))),
+ FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("sender0"))), REF("nil")))
+ )
))
)
)
@@ -78,13 +80,13 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
"""
|
| @Callable(invocation)
- | func foo(a:ByteStr) = {
+ | func foo(a:ByteVector) = {
| let sender0 = invocation.caller.bytes
- | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0)))
+ | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)])
| }
|
| @Callable(invocation)
- | func foo1(a:ByteStr) = {
+ | func foo1(a:ByteVector) = {
| foo(a)
| }
|
@@ -107,10 +109,10 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
| }
|
| @Callable(invocation)
- | func foo(a:ByteStr) = {
+ | func foo(a:ByteVector) = {
| let aux = bar()
| let sender0 = invocation.caller.bytes
- | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0)))
+ | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)])
| }
|
|
@@ -127,7 +129,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
"""
|
| @Callable(invocation)
- | func foo(a:ByteStr) = {
+ | func foo(a:ByteVector) = {
| a + invocation.caller.bytes
| }
|
@@ -138,6 +140,24 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
compiler.ContractCompiler(ctx, expr) should produce(FieldNames.Error)
}
+ property("annotation binding can have the same name as annotated function") {
+ val ctx = compilerContext
+ val expr = {
+ val script =
+ """
+ |
+ |@Callable(sameName)
+ |func sameName() = {
+ | throw()
+ |}
+ |
+ |
+ """.stripMargin
+ Parser.parseContract(script).get.value
+ }
+ compiler.ContractCompiler(ctx, expr) shouldBe 'right
+ }
+
property("contract compiles fails if has more than one verifier function") {
val ctx = compilerContext
val expr = {
@@ -215,7 +235,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
| case _ => 0
| }
| let newAmount = currentAmount + pmt.amount
- | WriteSet(List(DataEntry(currentKey, newAmount)))
+ | WriteSet([DataEntry(currentKey, newAmount)])
|
| }
| }
@@ -233,8 +253,8 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
| else if (newAmount < 0)
| then throw("Not enough balance")
| else ContractResult(
- | WriteSet(List(DataEntry(currentKey, newAmount))),
- | TransferSet(List(ContractTransfer(i.caller, amount, unit)))
+ | WriteSet([DataEntry(currentKey, newAmount)]),
+ | TransferSet([ContractTransfer(i.caller, amount, unit)])
| )
| }
|
@@ -253,14 +273,14 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
"""
|
| @Callable(invocation)
- | func foo(a:ByteStr) = {
+ | func foo(a:ByteVector) = {
| throw()
| }
|
| @Callable(i)
| func bar() = {
- | if (true) then WriteSet(List(DataEntry("entr1","entr2")))
- | else TransferSet(List(ContractTransfer(i.caller, zbsBalance(i.contractAddress), base58'somestr')))
+ | if (true) then WriteSet([DataEntry("entr1","entr2")])
+ | else TransferSet([ContractTransfer(i.caller, zbsBalance(i.contractAddress), base58'somestr')])
| }
|
| @Verifier(t)
@@ -282,12 +302,12 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
|
|@Callable(i)
|func sameName() = {
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
|}
|
|@Callable(i)
|func sameName() = {
- | WriteSet(List(DataEntry("b", "b")))
+ | WriteSet([DataEntry("b", "b")])
|}
|
|@Verifier(i)
@@ -298,7 +318,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
""".stripMargin
Parser.parseContract(script).get.value
}
- compiler.ContractCompiler(ctx, expr) should produce("Contract functions must have unique names")
+ compiler.ContractCompiler(ctx, expr) should produce("is already defined")
}
property("contract compilation fails if declaration and annotation bindings has the same name") {
@@ -311,7 +331,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
|
|@Callable(x)
|func some(i: Int) = {
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
|}
|
""".stripMargin
@@ -329,9 +349,9 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
|@Callable(i)
|func some(i: Int) = {
| if (i.contractAddress == "abc") then
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
| else
- | WriteSet(List(DataEntry("a", "b")))
+ | WriteSet([DataEntry("a", "b")])
|}
|
""".stripMargin
@@ -348,12 +368,12 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
|
|@Callable(x)
|func foo(i: Int) = {
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
|}
|
|@Callable(i)
|func bar(x: Int) = {
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
|}
|
""".stripMargin
@@ -372,7 +392,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
|
|@Callable(i)
|func some(x: Int) = {
- | WriteSet(List(DataEntry("a", "a")))
+ | WriteSet([DataEntry("a", "a")])
|}
|
""".stripMargin
@@ -380,4 +400,5 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi
}
compiler.ContractCompiler(ctx, expr) shouldBe 'right
}
+
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala
index ad10ecd..82ad380 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala
@@ -3,48 +3,29 @@ package com.zbsnetwork.lang.compiler
import cats.kernel.Monoid
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.Base58
+import com.zbsnetwork.lang.Global
import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.lang.contract.Contract._
import com.zbsnetwork.lang.v1.FunctionHeader.{Native, User}
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.{Decompiler, Terms}
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
-import com.zbsnetwork.lang.v1.parser.BinaryOperation
-import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, compiler}
-import com.zbsnetwork.lang.{Common, Global, StdLibVersion}
+import com.zbsnetwork.lang.v1.parser.BinaryOperation.NE_OP
+import com.zbsnetwork.lang.v1.{CTX, FunctionHeader}
import org.scalatest.prop.PropertyChecks
import org.scalatest.{Matchers, PropSpec}
class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
+ implicit class StringCmp(s1: String) {
+ def shouldEq(s2: String) = s1.replace("\r\n", "\n") shouldEqual s2.replace("\r\n", "\n")
+ }
+
val CTX: CTX =
Monoid.combineAll(Seq(PureContext.build(com.zbsnetwork.lang.StdLibVersion.V3), CryptoContext.build(Global)))
val decompilerContext = CTX.decompilerContext
- property("ctx debug test") {
- val ctx = Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false).compilerContext)
- val defs = ctx.functionDefs
- .filterKeys(BinaryOperation.opsByPriority.flatten.map(x => BinaryOperation.opsToFunctions(x) -> x).toMap.keys.toList.contains(_))
- .mapValues(_.map(_.header)
- .filter(_.isInstanceOf[Native])
- .map(_.asInstanceOf[Native].name))
- .toList
- .flatMap { case (name, codes) => codes.map((_, name)) }
- defs.mkString("\n").toString shouldBe
- """(104,*)
- |(106,%)
- |(103,>=)
- |(101,-)
- |(0,==)
- |(100,+)
- |(300,+)
- |(203,+)
- |(105,/)
- |(102,>)""".stripMargin
- }
-
property("successful on very deep expressions (stack overflow check)") {
val expr = (1 to 10000).foldLeft[EXPR](CONST_LONG(0)) { (acc, _) =>
FUNCTION_CALL(function = FunctionHeader.Native(100), List(CONST_LONG(1), acc))
@@ -53,16 +34,31 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
}
property("simple let") {
- val expr = Terms.LET_BLOCK(LET("a", CONST_LONG(1)), TRUE)
- Decompiler(expr, decompilerContext) shouldBe "{ let a = 1; true }"
+ val expr = Terms.LET_BLOCK(LET("a", CONST_LONG(1)), Terms.LET_BLOCK(LET("b", CONST_LONG(2)), Terms.LET_BLOCK(LET("c", CONST_LONG(3)), TRUE)))
+ Decompiler(expr, decompilerContext) shouldEq
+ """let a = 1
+ |let b = 2
+ |let c = 3
+ |true""".stripMargin
}
+ property("let in let") {
+ val expr =
+ Terms.LET_BLOCK(LET("a", Terms.LET_BLOCK(LET("x", CONST_LONG(0)), TRUE)), Terms.LET_BLOCK(LET("c", CONST_LONG(3)), TRUE))
+ Decompiler(expr, decompilerContext) shouldEq
+ """let a = {
+ | let x = 0
+ | true
+ | }
+ |let c = 3
+ |true""".stripMargin
+ }
property("native function call with one arg") {
val expr = Terms.FUNCTION_CALL(
function = FunctionHeader.Native(500),
args = List(TRUE)
)
- Decompiler(expr, decompilerContext) shouldBe "sigVerify(true)"
+ Decompiler(expr, decompilerContext) shouldEq "sigVerify(true)"
}
property("native function call with two arg (binary operations)") {
@@ -70,12 +66,12 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
function = FunctionHeader.Native(100),
args = List(CONST_LONG(1), CONST_LONG(2))
)
- Decompiler(expr, decompilerContext) shouldBe "(1 + 2)"
+ Decompiler(expr, decompilerContext) shouldEq "(1 + 2)"
}
property("nested binary operations") {
val expr = FUNCTION_CALL(Native(105), List(FUNCTION_CALL(Native(101), List(REF("height"), REF("startHeight"))), REF("interval")))
- Decompiler(expr, decompilerContext) shouldBe "((height - startHeight) / interval)"
+ Decompiler(expr, decompilerContext) shouldEq "((height - startHeight) / interval)"
}
property("unknown native function call") {
@@ -83,7 +79,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
function = FunctionHeader.Native(254),
args = List(CONST_LONG(1), CONST_LONG(2))
)
- Decompiler(expr, decompilerContext) shouldBe "Native<254>(1, 2)"
+ Decompiler(expr, decompilerContext) shouldEq "Native<254>(1, 2)"
}
property("user function call with one args") {
@@ -91,7 +87,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
function = FunctionHeader.User("foo"),
args = List(TRUE)
)
- Decompiler(expr, decompilerContext) shouldBe "foo(true)"
+ Decompiler(expr, decompilerContext) shouldEq "foo(true)"
}
property("user function call with empty args") {
@@ -99,7 +95,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
function = FunctionHeader.User("foo"),
args = List.empty
)
- Decompiler(expr, decompilerContext) shouldBe "foo()"
+ Decompiler(expr, decompilerContext) shouldEq "foo()"
}
property("v2 with LET in BLOCK") {
@@ -107,12 +103,10 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
LET("vari", REF("p")),
TRUE
)
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let vari =
- | p;
- | true
- |}""".stripMargin
+ val actual = Decompiler(expr, decompilerContext)
+ val expected = """|let vari = p
+ |true""".stripMargin
+ actual shouldEq expected
}
property("let and function call in block") {
@@ -121,12 +115,43 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
function = FunctionHeader.Native(100),
args = List(REF("v"), CONST_LONG(2))
))
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let v =
- | 1;
- | (v + 2)
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """let v = 1
+ |(v + 2)""".stripMargin
+ }
+
+ ignore("neq binary op") {
+ val expr =
+ Terms.FUNCTION_CALL(
+ function = FunctionHeader.User(NE_OP.func),
+ args = List(CONST_LONG(4), CONST_LONG(2))
+ )
+ Decompiler(expr, decompilerContext) shouldEq
+ """4 != 2""".stripMargin
+ }
+
+ property("function with complex args") {
+ val expr = BLOCK(
+ LET(
+ "x",
+ BLOCK(LET("y",
+ Terms.FUNCTION_CALL(
+ function = FunctionHeader.User("foo"),
+ args = List(BLOCK(LET("a", CONST_LONG(4)), REF("a")), CONST_LONG(2))
+ )),
+ TRUE)
+ ),
+ FALSE
+ )
+ Decompiler(expr, decompilerContext) shouldEq
+ """let x = {
+ | let y = foo({
+ | let a = 4
+ | a
+ | }, 2)
+ | true
+ | }
+ |false""".stripMargin
}
property("complicated let in let and function call in block") {
@@ -136,16 +161,12 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
Terms.BLOCK(Terms.LET("v", CONST_LONG(1)), Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(2))))),
Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("p"), CONST_LONG(3)))
)
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let p =
- | {
- | let v =
- | 1;
- | (v + 2)
- | };
- | (p + 3)
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """let p = {
+ | let v = 1
+ | (v + 2)
+ | }
+ |(p + 3)""".stripMargin
}
property("old match") {
@@ -161,101 +182,31 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
FALSE
)
)
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let v =
- | 1;
- | {
- | if (
- | {
- | if (
- | (v + 2)
- | )
- | then
- | true
- | else
- | (v + 3)
- | }
- | )
- | then
- | {
- | let p =
- | v;
- | true
- | }
- | else
- | false
- | }
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """let v = 1
+ |if (if ((v + 2))
+ | then true
+ | else (v + 3))
+ | then {
+ | let p = v
+ | true
+ | }
+ | else false""".stripMargin
}
- property("new match") {
- val expr = Terms.BLOCK(
- Terms.LET("v", CONST_LONG(1)),
- Terms.IF(
- Terms.IF(
- Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(2))),
- TRUE,
- Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(3)))
- ),
- Terms.BLOCK(Terms.LET("z", CONST_LONG(4)), TRUE),
- FALSE
- )
- )
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let v =
- | 1;
- | {
- | if (
- | {
- | if (
- | (v + 2)
- | )
- | then
- | true
- | else
- | (v + 3)
- | }
- | )
- | then
- | {
- | let z =
- | 4;
- | true
- | }
- | else
- | false
- | }
- |}""".stripMargin
+ property("ref getter idents") {
+ val expr = GETTER(REF("a"), "foo")
+ Decompiler(expr, decompilerContext) shouldEq
+ """a.foo""".stripMargin
}
- property("Invoke contract compilation") {
- val scriptText =
- """
- | @Callable(i)
- | func testfunc(amount: Int) = {
- | let pmt = 1
- |
- | if (false)
- | then
- | throw("impossible")
- | else {
- | ContractResult(
- | WriteSet(List(DataEntry("1", "1"))),
- | TransferSet(List(ContractTransfer(i.caller, amount, unit)))
- | )
- | }
- | }
- """.stripMargin
- val parsedScript = com.zbsnetwork.lang.v1.parser.Parser.parseContract(scriptText).get.value
-
- val ctx = Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false).compilerContext)
- val compledContract = compiler.ContractCompiler(ctx, parsedScript)
-
- compledContract.getOrElse("error").toString shouldBe
- """Contract(List(),List(CallableFunction(CallableAnnotation(i),FUNC(testfunc,List(amount),BLOCK(LET(pmt,CONST_LONG(1)),IF(FALSE,FUNCTION_CALL(Native(2),List(CONST_STRING(impossible))),FUNCTION_CALL(User(ContractResult),List(FUNCTION_CALL(User(WriteSet),List(FUNCTION_CALL(Native(1101),List(FUNCTION_CALL(User(DataEntry),List(CONST_STRING(1), CONST_STRING(1))))))), FUNCTION_CALL(User(TransferSet),List(FUNCTION_CALL(Native(1101),List(FUNCTION_CALL(User(ContractTransfer),List(GETTER(REF(i),caller), REF(amount), REF(unit)))))))))))))),None)"""
-
+ property("block getter idents") {
+ val expr = GETTER(BLOCK(LET("a", FALSE), REF("a")), "foo")
+ Decompiler(expr, decompilerContext) shouldEq
+ """{
+ | let a = false
+ | a
+ | }.foo""".stripMargin
}
property("Invoke contract with verifier decompilation") {
@@ -274,17 +225,21 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
FUNCTION_CALL(
User("WriteSet"),
List(FUNCTION_CALL(
- Native(1102),
- List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("b"), CONST_LONG(1))),
- FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))))
+ Native(1100),
+ List(
+ FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("b"), CONST_LONG(1))),
+ FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))), REF("nil")))
+ )
))
),
FUNCTION_CALL(
User("WriteSet"),
List(FUNCTION_CALL(
- Native(1102),
- List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))),
- FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))))
+ Native(1100),
+ List(
+ FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))),
+ FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))), REF("nil")))
+ )
))
)
)
@@ -293,46 +248,27 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
)),
Some(VerifierFunction(VerifierAnnotation("t"), FUNC("verify", List(), TRUE)))
)
- Decompiler(contract: Contract, decompilerContext) shouldBe
- """func foo () = {
- | false
- |}
- |
- |
- |func bar () = {
- | {
- | if (
- | foo()
- | )
- | then
- | true
- | else
- | false
- | }
- |}
- |
- |@Callable(invocation)
- |func baz (a) = {
- | {
- | let x =
- | invocation.caller.bytes;
- | {
- | if (
- | foo()
- | )
- | then
- | WriteSet(List(DataEntry("b", 1), DataEntry("sender", x)))
- | else
- | WriteSet(List(DataEntry("a", a), DataEntry("sender", x)))
- | }
- | }
- |}
- |
- |@Verifier(t)
- |func verify () = {
- | true
- |}
- |""".stripMargin
+ Decompiler(contract: Contract, decompilerContext) shouldEq
+ """|func foo () = false
+ |
+ |
+ |func bar () = if (foo())
+ | then true
+ | else false
+ |
+ |
+ |@Callable(invocation)
+ |func baz (a) = {
+ | let x = invocation.caller.bytes
+ | if (foo())
+ | then WriteSet(cons(DataEntry("b", 1), cons(DataEntry("sender", x), nil)))
+ | else WriteSet(cons(DataEntry("a", a), cons(DataEntry("sender", x), nil)))
+ | }
+ |
+ |
+ |@Verifier(t)
+ |func verify () = true
+ |""".stripMargin
}
property("Invoke contract decompilation") {
@@ -346,62 +282,33 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
List("amount"),
BLOCK(
LET("pmt", CONST_LONG(1)),
- IF(
- FALSE,
- FUNCTION_CALL(Native(2), List(CONST_STRING("impossible"))),
- FUNCTION_CALL(
- User("ContractResult"),
- List(
- FUNCTION_CALL(
- User("WriteSet"),
- List(FUNCTION_CALL(Native(1101), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("1"), CONST_STRING("1"))))))),
- FUNCTION_CALL(
- User("TransferSet"),
- List(FUNCTION_CALL(Native(1101),
- List(FUNCTION_CALL(User("ContractTransfer"), List(GETTER(REF("i"), "caller"), REF("amount"), REF("unit"))))))
- )
- )
- )
- )
+ TRUE
)
)
)),
None
)
- Decompiler(contract: Contract, decompilerContext) shouldBe
- """func foo (bar,buz) = {
- | true
- |}
+ Decompiler(contract, decompilerContext) shouldEq
+ """func foo (bar,buz) = true
+ |
|
|@Callable(i)
|func testfunc (amount) = {
- | {
- | let pmt =
- | 1;
- | {
- | if (
- | false
- | )
- | then
- | throw("impossible")
- | else
- | ContractResult(WriteSet(List(DataEntry("1", "1"))), TransferSet(List(ContractTransfer(i.caller, amount, unit))))
- | }
+ | let pmt = 1
+ | true
| }
- |}
+ |
|""".stripMargin
+
}
property("bytestring") {
val test = Base58.encode("abc".getBytes("UTF-8"))
// ([REVIEW]: may be i`am make a mistake here)
val expr = Terms.BLOCK(Terms.LET("param", CONST_BYTESTR(ByteStr(test.getBytes()))), REF("param"))
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let param =
- | base58'3K3F4C';
- | param
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """let param = base58'3K3F4C'
+ |param""".stripMargin
}
property("getter") {
@@ -410,66 +317,36 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
args = List(TRUE)
),
"testfield")
- Decompiler(expr, decompilerContext) shouldBe
+ Decompiler(expr, decompilerContext) shouldEq
"""testfunc(true).testfield""".stripMargin
}
property("simple if") {
val expr = IF(TRUE, CONST_LONG(1), CONST_STRING("XXX"))
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | if (
- | true
- | )
- | then
- | 1
- | else
- | "XXX"
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """if (true)
+ | then 1
+ | else "XXX"""".stripMargin
}
property("if with complicated else branch") {
val expr = IF(TRUE, CONST_LONG(1), IF(TRUE, CONST_LONG(1), CONST_STRING("XXX")))
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | if (
- | true
- | )
- | then
- | 1
- | else
- | {
- | if (
- | true
- | )
- | then
- | 1
- | else
- | "XXX"
- | }
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """if (true)
+ | then 1
+ | else if (true)
+ | then 1
+ | else "XXX"""".stripMargin
}
property("if with complicated then branch") {
val expr = IF(TRUE, IF(TRUE, CONST_LONG(1), CONST_STRING("XXX")), CONST_LONG(1))
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | if (
- | true
- | )
- | then
- | {
- | if (
- | true
- | )
- | then
- | 1
- | else
- | "XXX"
- | }
- | else
- | 1
- |}""".stripMargin
+ Decompiler(expr, decompilerContext) shouldEq
+ """if (true)
+ | then if (true)
+ | then 1
+ | else "XXX"
+ | else 1""".stripMargin
}
property("Surge smart accet") {
@@ -535,80 +412,30 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers {
)
)
)
- Decompiler(expr, decompilerContext) shouldBe
- """{
- | let startHeight =
- | 1375557;
- | {
- | let startPrice =
- | 100000;
- | {
- | let interval =
- | (24 * 60);
- | {
- | let exp =
- | ((100 * 60) * 1000);
- | {
- | let $match0 =
- | tx;
- | {
- | if (
- | _isInstanceOf($match0, "ExchangeTransaction")
- | )
- | then
- | {
- | let e =
- | $match0;
- | {
- | let days =
- | ((height - startHeight) / interval);
- | {
- | if (
- | {
- | if (
- | {
- | if (
- | (e.price >= (startPrice * (1 + (days * days))))
- | )
- | then
- | !(isDefined(e.sellOrder.assetPair.priceAsset))
- | else
- | false
- | }
- | )
- | then
- | (exp >= (e.sellOrder.expiration - e.sellOrder.timestamp))
- | else
- | false
- | }
- | )
- | then
- | (exp >= (e.buyOrder.expiration - e.buyOrder.timestamp))
- | else
- | false
- | }
- | }
- | }
- | else
- | {
- | if (
- | _isInstanceOf($match0, "BurnTransaction")
- | )
- | then
- | {
- | let tx =
- | $match0;
- | true
- | }
- | else
- | false
- | }
- | }
- | }
- | }
+ Decompiler(expr, decompilerContext) shouldEq
+ """let startHeight = 1375557
+ |let startPrice = 100000
+ |let interval = (24 * 60)
+ |let exp = ((100 * 60) * 1000)
+ |let $match0 = tx
+ |if (_isInstanceOf($match0, "ExchangeTransaction"))
+ | then {
+ | let e = $match0
+ | let days = ((height - startHeight) / interval)
+ | if (if (if ((e.price >= (startPrice * (1 + (days * days)))))
+ | then !(isDefined(e.sellOrder.assetPair.priceAsset))
+ | else false)
+ | then (exp >= (e.sellOrder.expiration - e.sellOrder.timestamp))
+ | else false)
+ | then (exp >= (e.buyOrder.expiration - e.buyOrder.timestamp))
+ | else false
| }
- | }
- |}""".stripMargin
+ | else if (_isInstanceOf($match0, "BurnTransaction"))
+ | then {
+ | let tx = $match0
+ | true
+ | }
+ | else false""".stripMargin
}
}
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala
index 49d8768..5ab4573 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala
@@ -15,11 +15,6 @@ class ErrorTest extends PropSpec with PropertyChecks with Matchers with ScriptGe
import com.zbsnetwork.lang.v1.parser.Expressions._
errorTests(
- "can't define LET with the same name as already defined in scope" -> "already defined in the scope" -> BLOCK(
- AnyPos,
- LET(AnyPos, PART.VALID(AnyPos, "X"), CONST_LONG(AnyPos, 1), Seq.empty),
- BLOCK(AnyPos, LET(AnyPos, PART.VALID(AnyPos, "X"), CONST_LONG(AnyPos, 2), Seq.empty), TRUE(AnyPos))
- ),
"can't define LET with the same name as predefined constant" -> "already defined in the scope" -> BLOCK(
AnyPos,
LET(AnyPos, PART.VALID(AnyPos, "unit"), CONST_LONG(AnyPos, 2), Seq.empty),
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala
index 92fc87f..2adc715 100644
--- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala
@@ -7,7 +7,7 @@ import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.Types._
import com.zbsnetwork.lang.v1.compiler.{CompilerContext, ExpressionCompiler}
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext._
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{PureContext, _}
+import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext
import com.zbsnetwork.lang.v1.parser.BinaryOperation.SUM_OP
import com.zbsnetwork.lang.v1.parser.Expressions.Pos
import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos
@@ -140,7 +140,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
)
),
expectedResult = Right(
- (BLOCK(LET("a", IF(TRUE, CONST_LONG(1), CONST_STRING(""))), FUNCTION_CALL(PureContext.eq.header, List(REF("a"), CONST_LONG(3)))), BOOLEAN))
+ (LET_BLOCK(LET("a", IF(TRUE, CONST_LONG(1), CONST_STRING(""))), FUNCTION_CALL(PureContext.eq.header, List(REF("a"), CONST_LONG(3)))), BOOLEAN))
)
treeTypeTest("idOptionLong(())")(
@@ -174,7 +174,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
)
),
expectedResult = Right(
- (BLOCK(
+ (LET_BLOCK(
LET("$match0", REF("p")),
IF(
IF(
@@ -188,7 +188,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
List(REF("$match0"), CONST_STRING("PointA"))
)
),
- BLOCK(LET("p", REF("$match0")), TRUE),
+ LET_BLOCK(LET("p", REF("$match0")), TRUE),
FALSE
)
),
@@ -215,9 +215,10 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
},
expectedResult = {
Right(
- (BLOCK(
- LET("a", BLOCK(LET("$match0", REF("p")), BLOCK(LET("$match1", REF("p")), CONST_LONG(1)))),
- BLOCK(LET("b", BLOCK(LET("$match0", REF("p")), CONST_LONG(2))), FUNCTION_CALL(FunctionHeader.Native(100), List(REF("a"), REF("b"))))
+ (LET_BLOCK(
+ LET("a", LET_BLOCK(LET("$match0", REF("p")), LET_BLOCK(LET("$match1", REF("p")), CONST_LONG(1)))),
+ LET_BLOCK(LET("b", LET_BLOCK(LET("$match0", REF("p")), CONST_LONG(2))),
+ FUNCTION_CALL(FunctionHeader.Native(100), List(REF("a"), REF("b"))))
),
LONG))
@@ -303,7 +304,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
)
),
expectedResult = Left(
- "Compilation failed: Value 'p1' declared as non-existing type, while all possible types are List(Point, PointB, Boolean, Int, PointA, ByteStr, Unit, String) in -1--1")
+ "Compilation failed: Value 'p1' declared as non-existing type, while all possible types are List(Point, PointB, Boolean, Int, PointA, ByteVector, Unit, String) in -1--1")
)
treeTypeTest("Invalid LET")(
@@ -405,6 +406,33 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher
LONG))
)
+ treeTypeTest("union type inferrer with list")(
+ ctx = compilerContext,
+ expr = {
+ val script = """[1,""]"""
+ Parser.parseExpr(script).get.value
+ },
+ expectedResult = {
+ Right(
+ (FUNCTION_CALL(
+ FunctionHeader.Native(1100),
+ List(
+ CONST_LONG(1),
+ FUNCTION_CALL(
+ FunctionHeader.Native(1100),
+ List(
+ CONST_STRING(""),
+ REF("nil")
+ )
+ )
+ )
+ ),
+ LIST(UNION(List(LONG, STRING))))
+ )
+
+ }
+ )
+
private def treeTypeTest(propertyName: String)(expr: Expressions.EXPR, expectedResult: Either[String, (EXPR, TYPE)], ctx: CompilerContext): Unit =
property(propertyName) {
compiler.ExpressionCompiler(ctx, expr) shouldBe expectedResult
diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala
new file mode 100644
index 0000000..14788b8
--- /dev/null
+++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala
@@ -0,0 +1,204 @@
+package com.zbsnetwork.lang.compiler.names
+import cats.kernel.Monoid
+import com.zbsnetwork.lang.Common.{NoShrink, produce}
+import com.zbsnetwork.lang.compiler.compilerContext
+import com.zbsnetwork.lang.contract.Contract
+import com.zbsnetwork.lang.v1.compiler
+import com.zbsnetwork.lang.v1.compiler.CompilerContext
+import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
+import com.zbsnetwork.lang.v1.parser.Parser
+import com.zbsnetwork.lang.v1.testing.ScriptGen
+import com.zbsnetwork.lang.{Common, StdLibVersion}
+import org.scalatest.prop.PropertyChecks
+import org.scalatest.{FreeSpec, Matchers}
+
+class NameDuplicationTest extends FreeSpec with PropertyChecks with Matchers with ScriptGen with NoShrink {
+
+ val ctx: CompilerContext =
+ Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), isTokenContext = false).compilerContext)
+
+ "Contract compilation" - {
+
+ "should succeed when" - {
+ "these have the same name:" - {
+
+ "constant and user function argument" in {
+ compileOf("""
+ |let x = 42
+ |
+ |func some(y: Boolean, x: Boolean) = !x
+ |""") shouldBe 'right
+ }
+
+ "constant and callable function argument" in {
+ compileOf("""
+ |let x = 42
+ |
+ |@Callable(i)
+ |func some(a: Int, x: String) = WriteSet([DataEntry("a", x)])
+ |""") shouldBe 'right
+ }
+
+ "user function and its argument" in {
+ compileOf("""
+ |func sameName(sameName: Boolean) = !sameName
+ |""") shouldBe 'right
+ }
+
+ "user function and argument; callable annotation bindings and arguments" in {
+ compileOf("""
+ |func i(i: Int) = i + 1
+ |
+ |@Callable(x)
+ |func foo(i: Int) = WriteSet([DataEntry("a", i + 1)])
+ |
+ |@Callable(i)
+ |func bar(x: Int) = WriteSet([DataEntry("a", i.contractAddress.bytes)])
+ |""") shouldBe 'right
+ }
+
+ }
+ }
+
+ "should fail when" - {
+ "these have the same name:" - {
+
+ "two constants" in {
+ compileOf("""
+ |let x = 42
+ |let x = true
+ |""") should produce("already defined")
+ }
+
+ "constant and user function" in {
+ compileOf("""
+ |let x = 42
+ |
+ |func x() = true
+ |""") should produce("already defined")
+ }
+
+ "constant and callable function" in {
+ compileOf("""
+ |let x = 42
+ |
+ |@Callable(i)
+ |func x() = WriteSet([DataEntry("a", "a")])
+ |""") should produce("already defined")
+ }
+
+ "constant and verifier function" in {
+ compileOf("""
+ |let x = 42
+ |
+ |@Verifier(i)
+ |func x() = WriteSet([DataEntry("a", "a")])
+ |""") should produce("already defined")
+ }
+
+ "constant and callable annotation binding" in {
+ compileOf("""
+ |let x = 42
+ |
+ |@Callable(x)
+ |func some(i: Int) = WriteSet([DataEntry("a", "a")])
+ |""") should produce("Annotation bindings overrides already defined var")
+ }
+
+ "constant and verifier annotation binding" in {
+ compileOf("""
+ |let x = 42
+ |
+ |@Verifier(x)
+ |func some() = true
+ |""") should produce("Annotation bindings overrides already defined var")
+ }
+
+ "two user functions" in {
+ compileOf("""
+ |func sameName() = true
+ |
+ |func sameName() = 1
+ |""") should produce("already defined")
+ }
+
+ "two user function arguments" in {
+ compileOf("""
+ |func some(sameName: String, sameName: Int) = sameName
+ |""") should produce("declared with duplicating argument names")
+ }
+
+ "user and callable functions" in {
+ compileOf("""
+ |func sameName() = true
+ |
+ |@Callable(i)
+ |func sameName() = WriteSet([DataEntry("a", "a")])
+ |""") should produce("already defined")
+ }
+
+ "user and verifier functions" in {
+ compileOf("""
+ |func sameName() = true
+ |
+ |@Verifier(i)
+ |func sameName() = true
+ |""") should produce("already defined")
+ }
+
+ "two callable functions" in {
+ compileOf("""
+ |@Callable(i)
+ |func sameName() = WriteSet([DataEntry("a", "a")])
+ |
+ |@Callable(i)
+ |func sameName() = WriteSet([DataEntry("b", "b")])
+ |""") should produce("already defined")
+ }
+
+ "two callable function arguments" in {
+ compileOf("""
+ |@Callable(i)
+ |func some(sameName: String, sameName: Int) = WriteSet([DataEntry("b", sameName)])
+ |""") should produce("declared with duplicating argument names")
+ }
+
+ "callable and verifier functions" in {
+ compileOf("""
+ |@Callable(i)
+ |func sameName() = WriteSet([DataEntry("a", "a")])
+ |
+ |@Verifier(i)
+ |func sameName() = true
+ |""") should produce("already defined")
+ }
+
+ "callable function and its callable annotation binding" in {
+ compileOf("""
+ |@Callable(sameName)
+ |func sameName() = WriteSet([DataEntry("a", sameName.contractAddress.bytes)])
+ |""") shouldBe 'right
+ }
+
+ "callable annotation binding and its function argument" in {
+ compileOf("""
+ |@Callable(i)
+ |func some(s: String, i: Int) =
+ | if (i.contractAddress == "abc") then
+ | WriteSet([DataEntry("a", "a")])
+ | else
+ | WriteSet([DataEntry("a", "b")])
+ |""") should produce("override annotation bindings")
+ }
+
+ }
+ }
+
+ }
+
+ def compileOf(script: String): Either[String, Contract] = {
+ val expr = Parser.parseContract(script.stripMargin).get.value
+ compiler.ContractCompiler(ctx, expr)
+ }
+
+}
diff --git a/lang/shared/src/main/resources/fomo.ride b/lang/shared/src/main/resources/fomo.ride
index e3257ab..665ffdf 100644
--- a/lang/shared/src/main/resources/fomo.ride
+++ b/lang/shared/src/main/resources/fomo.ride
@@ -1,9 +1,13 @@
+{-# STDLIB_VERSION 3 #-}
+{-# CONTENT_TYPE CONTRACT -#}
+
+let lpKey = "lastPayment"
+let liKey = "bestFomoer"
+let lhKey = "height"
+let day = 1440
+
@Callable(i)
func fearmissing() = {
- let lpKey = "lastPayment"
- let liKey = "bestFomoer"
- let lhKey = "height"
-
let payment = match i.payment {
case p:AttachedPayment =>
match p.asset {
@@ -21,25 +25,20 @@ func fearmissing() = {
if(payment <= lastPayment)
then throw("min payment is " +toString(payment))
else # storing best payment, caller and height
- WriteSet(List(
+ WriteSet([
DataEntry(lpKey, payment),
DataEntry(liKey, i.caller.bytes),
DataEntry(lhKey, height)
- ))
+ ])
}
@Callable(i)
-func withdraw(amount: Int) = {
-
- let day = 1440
- let liKey = "bestFomoer"
- let lhKey = "height"
-
+func withdraw() = {
let callerCorrect = i.caller.bytes == extract(getBinary(i.contractAddress, liKey))
let heightCorrect = extract(getInteger(i.contractAddress, lhKey)) - height >= day
let canWithdraw = heightCorrect && callerCorrect
if (canWithdraw)
- then TransferSet(List(ContractTransfer(i.caller, zbsBalance(i.contractAddress), unit)))
+ then TransferSet([ContractTransfer(i.caller, zbsBalance(i.contractAddress), unit)])
else throw("behold")
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala
index 4e1c236..31bfc0e 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala
@@ -18,15 +18,15 @@ object StdLibVersion extends TaggedType[Int] {
}
}
-object ScriptType extends TaggedType[Int] {
- type ScriptType = ScriptType.Type
+object ContentType extends TaggedType[Int] {
+ type ContentType = ContentType.Type
- val Expression: ScriptType = 1 @@ ScriptType
- val Contract: ScriptType = 2 @@ ScriptType
+ val Expression: ContentType = 1 @@ ContentType
+ val Contract: ContentType = 2 @@ ContentType
- val SupportedVersions: Set[ScriptType] = Set(Expression, Contract)
+ val SupportedTypes: Set[ContentType] = Set(Expression, Contract)
- def parseVersion(i: Int) = i match {
+ def parseId(i: Int) = i match {
case 1 => Expression
case 2 => Contract
}
@@ -36,3 +36,23 @@ object ScriptType extends TaggedType[Int] {
case "CONTRACT" => Contract
}
}
+
+object ScriptType extends TaggedType[Int] {
+ type ScriptType = ScriptType.Type
+
+ val Account: ScriptType = 1 @@ ScriptType
+ val Asset: ScriptType = 2 @@ ScriptType
+
+ val SupportedTypes: Set[ScriptType] = Set(Account, Asset)
+
+ def parseId(i: Int) = i match {
+ case 1 => Account
+ case 2 => Asset
+ }
+
+ def parseString(s: String) = s match {
+ case "ACCOUNT" => Account
+ case "ASSET" => Asset
+ }
+}
+
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala
index 2259c85..c9d8298 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala
@@ -3,11 +3,13 @@ package com.zbsnetwork.lang.directives
sealed trait DirectiveKey
object DirectiveKey {
final case object STDLIB_VERSION extends DirectiveKey
+ final case object CONTENT_TYPE extends DirectiveKey
final case object SCRIPT_TYPE extends DirectiveKey
val dictionary =
Map(
"STDLIB_VERSION" -> DirectiveKey.STDLIB_VERSION,
+ "CONTENT_TYPE" -> DirectiveKey.CONTENT_TYPE,
"SCRIPT_TYPE" -> DirectiveKey.SCRIPT_TYPE
)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala
index e0382ab..bf2947c 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala
@@ -2,6 +2,7 @@ package com.zbsnetwork.lang
import cats.implicits._
import com.zbsnetwork.lang.ScriptType.ScriptType
+import com.zbsnetwork.lang.ContentType.ContentType
import com.zbsnetwork.lang.StdLibVersion.StdLibVersion
import com.zbsnetwork.lang.directives.{Directive, DirectiveKey}
@@ -28,23 +29,41 @@ package object utils {
.getOrElse(StdLibVersion.V2.asRight)
}
+ def extractContentType(directives: List[Directive]): Either[String, ContentType] = {
+ directives
+ .find(_.key == DirectiveKey.CONTENT_TYPE)
+ .map(d =>
+ Try(d.value) match {
+ case Success(v) =>
+ val cType = ContentType.parseString(v)
+ Either
+ .cond(
+ ContentType.SupportedTypes(cType),
+ cType,
+ "Unsupported content type"
+ )
+ case Failure(ex) =>
+ Left("Can't parse content type")
+ })
+ .getOrElse(ContentType.Expression.asRight)
+ }
+
def extractScriptType(directives: List[Directive]): Either[String, ScriptType] = {
directives
.find(_.key == DirectiveKey.SCRIPT_TYPE)
.map(d =>
Try(d.value) match {
case Success(v) =>
- val ver = ScriptType.parseString(v)
+ val sType = ScriptType.parseString(v)
Either
.cond(
- ScriptType.SupportedVersions(ver),
- ver,
+ ScriptType.SupportedTypes(sType),
+ sType,
"Unsupported script type"
)
case Failure(ex) =>
Left("Can't parse script type")
- })
- .getOrElse(ScriptType.Expression.asRight)
+ })
+ .getOrElse(ScriptType.Account.asRight)
}
-
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala
index 7ff40a2..a81a550 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala
@@ -29,7 +29,13 @@ case class CTX(@(JSExport @field) types: Seq[DefinedType],
functionDefs = functions.groupBy(_.name).map { case (k, v) => k -> v.map(_.signature).toList }
)
- val opsNames = BinaryOperation.opsByPriority.flatten.map(x => BinaryOperation.opsToFunctions(x)).toSet
+ val opsNames = BinaryOperation.opsByPriority
+ .flatMap({
+ case Left(l) => l
+ case Right(l) => l
+ })
+ .map(x => BinaryOperation.opsToFunctions(x))
+ .toSet
lazy val decompilerContext: DecompilerContext = DecompilerContext(
opCodes = compilerContext.functionDefs
@@ -39,12 +45,14 @@ case class CTX(@(JSExport @field) types: Seq[DefinedType],
.toMap,
binaryOps = compilerContext.functionDefs
.filterKeys(opsNames(_))
- .mapValues(_.map(_.header)
- .filter(_.isInstanceOf[Native])
- .map(_.asInstanceOf[Native].name))
+ .mapValues(
+ _.map(_.header)
+ .filter(_.isInstanceOf[Native])
+ .map(_.asInstanceOf[Native].name))
.toList
.flatMap { case (name, codes) => codes.map((_, name)) }
- .toMap
+ .toMap,
+ ident = 0
)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala
index b77e9e0..62926f5 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala
@@ -49,7 +49,7 @@ object CompilationError {
}
final case class BadFunctionSignatureSameArgNames(start: Int, end: Int, name: String) extends CompilationError {
- val message = s"Function'$name' declared with duplicating argument names"
+ val message = s"Function '$name' declared with duplicating argument names"
}
final case class FunctionNotFound(start: Int, end: Int, name: String, argTypes: List[String]) extends CompilationError {
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala
index d09004d..8f2f862 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala
@@ -3,7 +3,7 @@ import cats.Show
import cats.implicits._
import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.lang.contract.Contract._
-import com.zbsnetwork.lang.v1.compiler.CompilationError.Generic
+import com.zbsnetwork.lang.v1.compiler.CompilationError.{AlreadyDefined, Generic}
import com.zbsnetwork.lang.v1.compiler.CompilerContext.vars
import com.zbsnetwork.lang.v1.compiler.ExpressionCompiler._
import com.zbsnetwork.lang.v1.compiler.Terms.DECLARATION
@@ -11,10 +11,9 @@ import com.zbsnetwork.lang.v1.compiler.Types.{BOOLEAN, UNION}
import com.zbsnetwork.lang.v1.evaluator.ctx.FunctionTypeSignature
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.{FieldNames, ZbsContext}
import com.zbsnetwork.lang.v1.parser.Expressions.FUNC
-import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos
import com.zbsnetwork.lang.v1.parser.{Expressions, Parser}
import com.zbsnetwork.lang.v1.task.imports._
-import com.zbsnetwork.lang.v1.{FunctionHeader, compiler}
+import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader, compiler}
object ContractCompiler {
def compileAnnotatedFunc(af: Expressions.ANNOTATEDFUNC): CompileM[AnnotatedFunction] = {
@@ -31,7 +30,7 @@ object ContractCompiler {
compiledBody <- local {
for {
_ <- modify[CompilerContext, CompilationError](vars.modify(_)(_ ++ annotationBindings))
- r <- compiler.ExpressionCompiler.compileFunc(AnyPos, af.f)
+ r <- compiler.ExpressionCompiler.compileFunc(af.f.position, af.f, annotationBindings.map(_._1))
} yield r
}
} yield (annotations, compiledBody)
@@ -89,11 +88,22 @@ object ContractCompiler {
ds <- contract.decs.traverse[CompileM, DECLARATION](compileDeclaration)
_ <- validateDuplicateVarsInContract(contract)
l <- contract.fs.traverse[CompileM, AnnotatedFunction](af => local(compileAnnotatedFunc(af)))
+ duplicatedFuncNames = l.map(_.u.name).groupBy(identity).collect { case (x, List(_, _, _*)) => x }.toList
_ <- Either
.cond(
- l.map(_.u.name).toSet.size == l.size,
+ duplicatedFuncNames.isEmpty,
(),
- Generic(contract.position.start, contract.position.start, "Contract functions must have unique names")
+ AlreadyDefined(contract.position.start, contract.position.start, duplicatedFuncNames.mkString(", "), isFunction = true)
+ )
+ .toCompileM
+
+ _ <- Either
+ .cond(
+ l.forall(_.u.args.size <= ContractLimits.MaxContractInvocationArgs),
+ (),
+ Generic(contract.position.start,
+ contract.position.end,
+ s"Contract functions can have no more than ${ContractLimits.MaxContractInvocationArgs} arguments")
)
.toCompileM
verifierFunctions = l.filter(_.isInstanceOf[VerifierFunction]).map(_.asInstanceOf[VerifierFunction])
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala
new file mode 100644
index 0000000..4551774
--- /dev/null
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala
@@ -0,0 +1,19 @@
+package com.zbsnetwork.lang.v1
+
+object ContractLimits {
+ val MaxExprComplexity = 20 * 100
+ val MaxExprSizeInBytes = 8 * 1024
+
+ val MaxContractComplexity = 20 * 100
+ val MaxContractSizeInBytes = 32 * 1024
+
+ // As in Scala
+ val MaxContractInvocationArgs = 22
+
+ // Data 0.001 per kilobyte, rounded up, fee for CI is 0.005
+ val MaxContractInvocationSizeInBytes = 5 * 1024
+ val MaxWriteSetSizeInBytes = 5 * 1024
+
+ // Mass Transfer 0.001 + 0.0005*N, rounded up to 0.001, fee for CI is 0.005
+ val MaxPaymentAmount = 10
+}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala
index 593ef4c..9e65453 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala
@@ -1,150 +1,134 @@
package com.zbsnetwork.lang.v1.compiler
+import cats.implicits._
import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.lang.contract.Contract.{CallableFunction, VerifierFunction}
import com.zbsnetwork.lang.v1.FunctionHeader
import com.zbsnetwork.lang.v1.compiler.Terms._
+import monix.eval.Coeval
object Decompiler {
- case class Delay(seq: Seq[Either[String, Function[Unit, Delay]]])
-
- private def show(d: Delay): String = {
- var w = d.seq
- val s = new StringBuffer()
- while (w.nonEmpty) {
- w.head match {
- case Left(o) =>
- s.append(o)
- w = w.tail
- case Right(f) =>
- w = f(()).seq ++ w.tail
- }
- }
- s.toString
- }
+ sealed trait BlockBraces
+ case object NoBraces extends BlockBraces
+ case object BracesWhenNeccessary extends BlockBraces
+
+ sealed trait FirstLinePolicy
+ case object DontIndentFirstLine extends FirstLinePolicy
+ case object IdentFirstLine extends FirstLinePolicy
+
+ private[lang] def pure[A](a: A) = Coeval.evalOnce(a)
private def out(in: String, ident: Int): String =
Array.fill(4 * ident)(" ").mkString("") + in
- private def decl(e: DECLARATION, ident: Int, ctx: DecompilerContext): String =
- e match {
+ private def pureOut(in: String, ident: Int): Coeval[String] = pure(out(in, ident))
+
+ private val NEWLINE = scala.util.Properties.lineSeparator
+
+ private def decl(e: Coeval[DECLARATION], ctx: DecompilerContext): Coeval[String] =
+ e flatMap {
case Terms.FUNC(name, args, body) =>
- out("func " + name + " (" + args.map(_.toString).mkString(","), ident) + ") = {\n" +
- show(expr(body, 1 + ident, ctx)) + "\n" +
- out("}\n", ident)
+ expr(pure(body), ctx, BracesWhenNeccessary, DontIndentFirstLine).map(
+ fb =>
+ out("func " + name + " (" + args.mkString(",") + ") = ", ctx.ident) +
+ out(fb + NEWLINE, ctx.ident))
case Terms.LET(name, value) =>
- out("let " + name + " =\n", 0 + ident) +
- show(expr(value, 1 + ident, ctx))
- }
-
- private def argso(args: List[Terms.EXPR], ctx: DecompilerContext): Seq[Either[String, Function[Unit, Delay]]] = {
- args.foldLeft(Seq[Either[String, Function[Unit, Delay]]]()) { (alfa, beta) =>
- val gamma = Right((_: Unit) => expr(beta, 0, ctx))
- if (alfa.isEmpty) Seq(gamma)
- else alfa ++ Seq(Left(", "), gamma)
+ expr(pure(value), ctx, BracesWhenNeccessary, DontIndentFirstLine).map(e => out("let " + name + " = " + e, ctx.ident))
}
- }
- private def expr(e: EXPR, ident: Int, ctx: DecompilerContext): Delay =
- e match {
- case Terms.TRUE => Delay(Seq(Left(out("true", ident))))
- case Terms.FALSE => Delay(Seq(Left(out("false", ident))))
- case Terms.CONST_BOOLEAN(b) => Delay(Seq(Left(out(b.toString.toLowerCase(), ident))))
- case Terms.IF(cond, it, iff) =>
- Delay(
- Seq(
- Left(out("{\n", 0 + ident)),
- Left(out("if (\n", 1 + ident)),
- Right(_ => expr(cond, 2 + ident, ctx)),
- Left("\n"),
- Left(out(")\n", 1 + ident)),
- Left(out("then\n", 1 + ident)),
- Right(_ => expr(it, 2 + ident, ctx)),
- Left("\n"),
- Left(out("else\n", 1 + ident)),
- Right(_ => expr(iff, 2 + ident, ctx)),
- Left("\n"),
- Left(out("}", 0 + ident))
- ))
- case Terms.CONST_LONG(t) => Delay(Seq(Left(out(t.toLong.toString, ident))))
- case Terms.CONST_STRING(s) => Delay(Seq(Left(out('"' + s + '"', ident))))
- case Terms.LET_BLOCK(let, exprPar) =>
- Delay(
- Seq(
- Left(out("{ let " + let.name + " = ", ident)),
- Right(_ => expr(let.value, 0, ctx)),
- Left(out("; ", 0)),
- Right(_ => expr(exprPar, 0, ctx)),
- Left(out(" }", 0))
- ))
+ private[lang] def expr(e: Coeval[EXPR], ctx: DecompilerContext, braces: BlockBraces, firstLinePolicy: FirstLinePolicy): Coeval[String] = {
+ val i = if (braces == BracesWhenNeccessary) 0 else ctx.ident
+ e flatMap {
case Terms.BLOCK(declPar, body) =>
- Delay(
- Seq(
- Left(out("{\n", ident) + decl(declPar, 1 + ident, ctx) + ";\n"),
- Right(_ => expr(body, 1 + ident, ctx)),
- Left(out("\n", 0)),
- Left(out("}", ident))
- ))
- case Terms.CONST_BYTESTR(bs) => Delay(Seq(Left(out("base58'" + bs.base58 + "'", ident))))
+ val braceThis = braces match {
+ case NoBraces => false
+ case BracesWhenNeccessary => true
+ }
+ val modifiedCtx = if (braceThis) ctx.incrementIdent() else ctx
+ for {
+ d <- decl(pure(declPar), modifiedCtx)
+ b <- expr(pure(body), modifiedCtx, NoBraces, IdentFirstLine)
+ } yield {
+ if (braceThis)
+ out("{" + NEWLINE, ident = 0) +
+ out(d + NEWLINE, 0) +
+ out(b + NEWLINE, 0) +
+ out("}", ctx.ident + 1)
+ else
+ out(d + NEWLINE, 0) +
+ out(b, 0)
+ }
+ case Terms.LET_BLOCK(let, exprPar) => expr(pure(Terms.BLOCK(let, exprPar)), ctx, braces, firstLinePolicy)
+ case Terms.TRUE => pureOut("true", i)
+ case Terms.FALSE => pureOut("false", i)
+ case Terms.CONST_BOOLEAN(b) => pureOut(b.toString.toLowerCase(), i)
+ case Terms.CONST_LONG(t) => pureOut(t.toLong.toString, i)
+ case Terms.CONST_STRING(s) => pureOut('"' + s + '"', i)
+ case Terms.CONST_BYTESTR(bs) => pureOut("base58'" + bs.base58 + "'", i)
+ case Terms.REF(ref) => pureOut(ref, i)
+ case Terms.GETTER(getExpr, fld) => expr(pure(getExpr), ctx, BracesWhenNeccessary, firstLinePolicy).map(a => a + "." + fld)
+ case Terms.IF(cond, it, iff) =>
+ for {
+ c <- expr(pure(cond), ctx, BracesWhenNeccessary, DontIndentFirstLine)
+ it <- expr(pure(it), ctx.incrementIdent(), BracesWhenNeccessary, DontIndentFirstLine)
+ iff <- expr(pure(iff), ctx.incrementIdent(), BracesWhenNeccessary, DontIndentFirstLine)
+ } yield
+ out("if (" + c + ")" + NEWLINE, i) +
+ out("then " + it + NEWLINE, ctx.ident + 1) +
+ out("else " + iff, ctx.ident + 1)
case Terms.FUNCTION_CALL(func, args) =>
+ val argsCoeval = args
+ .map(a => expr(pure(a), ctx, BracesWhenNeccessary, DontIndentFirstLine))
+ .toVector
+ .sequence
func match {
- case FunctionHeader.User(name) => Delay(Seq(Left(out(name + "(", ident))) ++ argso(args, ctx) ++ Seq(Left(")")))
+ case FunctionHeader.User(name) => argsCoeval.map(as => out(name + "(" + as.mkString(", ") + ")", i))
case FunctionHeader.Native(name) =>
- val binOp: Option[String] = ctx.binaryOps.get(name)
- binOp match {
+ ctx.binaryOps.get(name) match {
case Some(binOp) =>
- Delay(Seq(
- Left(out("(", ident)),
- Right(_ => expr(args.head, 0, ctx)),
- Left(out(" " + binOp + " ", 0)),
- Right(_ => expr(args.tail.head, 0, ctx)),
- Left(out(")", 0))))
+ argsCoeval.map(as => out("(" + as.head + " " + binOp + " " + as.tail.head + ")", i))
case None =>
- val opCode = ctx.opCodes.get(name)
- opCode match {
- case None =>
- Delay(
- Seq(Left(out("Native<" + name + ">", ident))) ++
- Seq(Left(out("(", 0))) ++ argso(args, ctx) ++ Seq(Left(")")))
- case Some(opCode) =>
- Delay(Seq(Left(out(opCode.toString + "(", ident))) ++ argso(args, ctx) ++ Seq(Left(")")))
- }
+ argsCoeval.map(
+ as =>
+ out(ctx.opCodes.getOrElse(name, "Native<" + name + ">") + "(" + as.mkString(", ")
+ + ")",
+ i))
}
}
- case Terms.REF(ref) => Delay(Seq(Left(out(ref, ident))))
- case Terms.GETTER(get_expr, fld) =>
- Delay(
- Seq(
- Left(out("", 0)),
- Right(_ => expr(get_expr, ident, ctx)),
- Left(out("." + fld, 0))
- ))
- case Terms.ARR(_) => ??? // never happens
+ case _: Terms.ARR => ??? // never happens
case _: Terms.CaseObj => ??? // never happens
}
+ }
def apply(e: Contract, ctx: DecompilerContext): String = {
- e match {
- case Contract(dec, cfs, vf) =>
- dec.map(expr => decl(expr, 0, ctx)).mkString("\n\n") +
- cfs
- .map {
- case CallableFunction(annotation, u) =>
- out("\n@Callable(" + annotation.invocationArgName + ")\n", 0) +
- Decompiler.decl(u, 0, ctx)
- }
- .mkString("\n") +
- (vf match {
- case Some(VerifierFunction(annotation, u)) =>
- out("\n@Verifier(" + annotation.invocationArgName + ")\n", 0) +
- Decompiler.decl(u, 0, ctx)
- case None => ""
- })
- }
+
+ def intersperse(s: Seq[Coeval[String]]): Coeval[String] = s.toVector.sequence.map(v => v.mkString(NEWLINE + NEWLINE))
+
+ import e._
+
+ val decls: Seq[Coeval[String]] = dec.map(expr => decl(pure(expr), ctx))
+ val callables: Seq[Coeval[String]] = cfs
+ .map {
+ case CallableFunction(annotation, u) =>
+ Decompiler.decl(pure(u), ctx).map(out(NEWLINE + "@Callable(" + annotation.invocationArgName + ")" + NEWLINE, 0) + _)
+ }
+
+ val verifier: Seq[Coeval[String]] = vf.map {
+ case VerifierFunction(annotation, u) =>
+ Decompiler.decl(pure(u), ctx).map(out(NEWLINE + "@Verifier(" + annotation.invocationArgName + ")" + NEWLINE, 0) + _)
+ }.toSeq
+
+ val result = for {
+ d <- intersperse(decls)
+ c <- intersperse(callables)
+ v <- intersperse(verifier)
+ } yield d + NEWLINE + c + NEWLINE + v
+
+ result()
}
def apply(e0: EXPR, ctx: DecompilerContext): String =
- show(expr(e0, 0, ctx))
+ expr(pure(e0), ctx, NoBraces, IdentFirstLine).apply()
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala
index fd7bb2b..7a0076e 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala
@@ -1,3 +1,5 @@
package com.zbsnetwork.lang.v1.compiler
-case class DecompilerContext(opCodes: Map[Short, String], binaryOps: Map[Short, String])
+case class DecompilerContext(opCodes: Map[Short, String], binaryOps: Map[Short, String], ident: Int) {
+ def incrementIdent(): DecompilerContext = this.copy(ident = this.ident + 1)
+}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala
index 38568ac..4f4bda9 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala
@@ -10,7 +10,6 @@ import com.zbsnetwork.lang.v1.compiler.Types.{FINAL, _}
import com.zbsnetwork.lang.v1.evaluator.ctx._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext
import com.zbsnetwork.lang.v1.parser.BinaryOperation._
-import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos
import com.zbsnetwork.lang.v1.parser.Expressions.{BINARY_OP, MATCH_CASE, PART, Pos}
import com.zbsnetwork.lang.v1.parser.{BinaryOperation, Expressions, Parser}
import com.zbsnetwork.lang.v1.task.imports._
@@ -91,9 +90,9 @@ object ExpressionCompiler {
case _ => None
}
ifCases <- inspectFlat[CompilerContext, CompilationError, Expressions.EXPR](updatedCtx => {
- mkIfCases(updatedCtx, cases, Expressions.REF(p, PART.VALID(AnyPos, refTmpKey)), allowShadowVarName).toCompileM
+ mkIfCases(updatedCtx, cases, Expressions.REF(p, PART.VALID(p, refTmpKey)), allowShadowVarName).toCompileM
})
- compiledMatch <- compileLetBlock(p, Expressions.LET(AnyPos, PART.VALID(AnyPos, refTmpKey), expr, Seq.empty), ifCases)
+ compiledMatch <- compileLetBlock(p, Expressions.LET(p, PART.VALID(p, refTmpKey), expr, Seq.empty), ifCases)
_ <- cases
.flatMap(_.types)
.traverse[CompileM, String](handlePart)
@@ -121,11 +120,11 @@ object ExpressionCompiler {
private def handleTypeUnion(types: List[String], f: FINAL, ctx: CompilerContext) =
if (types.isEmpty) f else UNION.create(types.map(ctx.predefTypes).map(_.typeRef))
- private def validateShadowing(p: Pos, dec: Expressions.Declaration): CompileM[String] =
+ private def validateShadowing(p: Pos, dec: Expressions.Declaration, allowedExceptions: List[String] = List.empty): CompileM[String] =
for {
ctx <- get[CompilerContext, CompilationError]
letName <- handlePart(dec.name)
- .ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = false))(n => !ctx.varDefs.contains(n) || dec.allowShadowing)
+ .ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = false))(n => !ctx.varDefs.contains(n) || dec.allowShadowing || allowedExceptions.contains(n))
.ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = true))(n => !ctx.functionDefs.contains(n))
} yield letName
@@ -140,9 +139,9 @@ object ExpressionCompiler {
typeUnion = handleTypeUnion(letTypes, compiledLet._2, ctx)
} yield (letName, typeUnion, compiledLet._1)
- def compileFunc(p: Pos, func: Expressions.FUNC): CompileM[(FUNC, FINAL, List[(String, FINAL)])] = {
+ def compileFunc(p: Pos, func: Expressions.FUNC, annListVars: List[String] = List.empty): CompileM[(FUNC, FINAL, List[(String, FINAL)])] = {
for {
- funcName <- validateShadowing(p, func)
+ funcName <- validateShadowing(p, func, annListVars)
_ <- func.args.toList
.pure[CompileM]
.ensure(BadFunctionSignatureSameArgNames(p.start, p.end, funcName)) { l =>
@@ -185,7 +184,7 @@ object ExpressionCompiler {
updateCtx(letName, letType, p)
.flatMap(_ => compileExpr(body))
}
- } yield (BLOCK(LET(letName, letExpr), compiledBody._1), compiledBody._2)
+ } yield (LET_BLOCK(LET(letName, letExpr), compiledBody._1), compiledBody._2)
}
private def compileFuncBlock(p: Pos, func: Expressions.FUNC, body: Expressions.EXPR): CompileM[(Terms.EXPR, FINAL)] = {
@@ -305,7 +304,8 @@ object ExpressionCompiler {
}
}
- val default: Either[CompilationError, Expressions.EXPR] = Right(Expressions.FUNCTION_CALL(AnyPos, PART.VALID(AnyPos, "throw"), List.empty))
+ val default: Either[CompilationError, Expressions.EXPR] = Right(Expressions.FUNCTION_CALL(cases.head.position, PART.VALID(cases.head.position, "throw"), List.empty))
+
cases.foldRight(default) {
case (mc, furtherEi) =>
furtherEi match {
@@ -313,7 +313,6 @@ object ExpressionCompiler {
case Left(e) => Left(e)
}
}
-
}
private def mkGetter(p: Pos, ctx: CompilerContext, types: List[FINAL], fieldName: String, expr: EXPR): Either[CompilationError, (GETTER, FINAL)] = {
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala
index fc1ae5d..d3416e9 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala
@@ -44,7 +44,6 @@ object TypeInferrer {
lazy val err = s"Non-matching types: expected: $placeholder, actual: $argType"
(placeholder, argType) match {
- case (_, NOTHING) => Right(None)
case (tp @ TYPEPARAM(char), _) =>
Right(Some(MatchResult(argType, tp)))
case (tp @ PARAMETERIZEDLIST(innerTypeParam), LIST(t)) => matchTypes(t, innerTypeParam, knownTypes)
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala
index 8229e12..5b76fbc 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala
@@ -51,7 +51,7 @@ object Types {
case NOTHING => List.empty
case UNION(inner) => inner
case s: REAL => List(s)
- }.toList)
+ }.toList.distinct)
}
def apply(l: REAL*): UNION = create(l.toList)
@@ -79,4 +79,10 @@ object Types {
def <=(l2: FINAL): Boolean = l2 >= l1
}
+
+ val UNIT: CASETYPEREF = CASETYPEREF("Unit", List.empty)
+ val optionByteVector = UNION(BYTESTR, UNIT)
+ val optionLong = UNION(LONG, UNIT)
+ val listByteVector: LIST = LIST(BYTESTR)
+ val listString: LIST = LIST(STRING)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala
index 282aa1e..51e3adf 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala
@@ -11,15 +11,19 @@ import com.zbsnetwork.lang.v1.task.imports.{raiseError, _}
import com.zbsnetwork.lang.v1.traits.domain.Tx.{ContractTransfer, Pmt}
import com.zbsnetwork.lang.v1.traits.domain.{Ord, Recipient, Tx}
-import scala.collection.mutable.ListBuffer
-
object ContractEvaluator {
case class Invocation(fc: FUNCTION_CALL, invoker: ByteStr, payment: Option[(Long, Option[ByteStr])], contractAddress: ByteStr)
def eval(c: Contract, i: Invocation): EvalM[EVALUATED] = {
val functionName = i.fc.function.asInstanceOf[FunctionHeader.User].name
c.cfs.find(_.u.name == functionName) match {
- case None => raiseError[LoggedEvaluationContext, ExecutionError, EVALUATED](s"Callable function '$functionName doesn't exist in the contract")
+ case None =>
+ val otherFuncs = c.dec.filter(_.isInstanceOf[FUNC]).map(_.asInstanceOf[FUNC].name)
+ val message =
+ if (otherFuncs contains functionName)
+ s"function '$functionName exists in the contract but is not marked as @Callable, therefore cannot not be invoked"
+ else s"@Callable function '$functionName doesn't exist in the contract"
+ raiseError[LoggedEvaluationContext, ExecutionError, EVALUATED](message)
case Some(f) =>
val zeroExpr = Right(
BLOCK(
@@ -60,11 +64,6 @@ object ContractEvaluator {
EvaluatorV1.evalExpr(expr)
}
-
- def apply(ctx: EvaluationContext, c: Contract, i: Invocation): Either[ExecutionError, ContractResult] = {
- val log = ListBuffer[LogItem]()
- val llc = (str: String) => (v: LetExecResult) => log.append((str, v))
- val lec = LoggedEvaluationContext(llc, ctx)
- eval(c, i).run(lec).value._2.flatMap(ContractResult.fromObj)
- }
+ def apply(ctx: EvaluationContext, c: Contract, i: Invocation): Either[ExecutionError, ContractResult] =
+ EvaluatorV1.evalWithLogging(ctx, eval(c, i))._2.flatMap(ContractResult.fromObj)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala
index 67bedbd..7c1cd15 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala
@@ -33,6 +33,17 @@ object FunctionIds {
val LONG_TO_STRING: Short = 420
val BOOLEAN_TO_STRING: Short = 421
+ val CREATE_LIST: Short = 1100
+
+ val UTF8STRING: Short = 1200
+ val BININT: Short = 1201
+ val BININT_OFF: Short = 1202
+ val INDEXOF: Short = 1203
+ val INDEXOFN: Short = 1204
+ val SPLIT: Short = 1205
+ val PARSEINT: Short = 1206
+ val PARSEINTV: Short = 1207
+
// Crypto
val SIGVERIFY: Short = 500
val KECCAK256: Short = 501
@@ -62,10 +73,4 @@ object FunctionIds {
val ADDRESSFROMRECIPIENT: Short = 1060
- val CREATE_LIST0: Short = 1100
- val CREATE_LIST1: Short = 1101
- val CREATE_LIST2: Short = 1102
- val CREATE_LIST3: Short = 1103
-
-
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala
index 12f9f9a..0681242 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala
@@ -1,7 +1,8 @@
package com.zbsnetwork.lang.v1.evaluator.ctx
import cats.data.EitherT
-import com.zbsnetwork.lang.TrampolinedExecResult
+import com.zbsnetwork.lang.StdLibVersion.StdLibVersion
+import com.zbsnetwork.lang.{StdLibVersion, TrampolinedExecResult}
import com.zbsnetwork.lang.v1.FunctionHeader
import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, EXPR}
import com.zbsnetwork.lang.v1.compiler.Types._
@@ -17,6 +18,7 @@ sealed trait BaseFunction {
@JSExport def name: String
@JSExport def docString: String
@JSExport def argsDoc: Array[(String, String)]
+ @JSExport def deprecated: Boolean = false
}
object BaseFunction {
@@ -55,27 +57,63 @@ object NativeFunction {
@JSExportTopLevel("UserFunction")
case class UserFunction(@(JSExport @field) name: String,
@(JSExport @field) internalName: String,
- @(JSExport @field) cost: Long,
+ costByLibVersion: Map[StdLibVersion, Long],
@(JSExport @field) signature: FunctionTypeSignature,
ev: EXPR,
@(JSExport @field) docString: String,
- @(JSExport @field) argsDoc: Array[(String, String)])
- extends BaseFunction
+ @(JSExport @field) argsDoc: Array[(String, String)]
+ )
+ extends BaseFunction {
+
+ @(JSExport @field)
+ def cost: Long = costByLibVersion.get(StdLibVersion.SupportedVersions.last).get
+}
object UserFunction {
def apply(name: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)(ev: EXPR): UserFunction =
- UserFunction(name, name, cost, resultType, docString, args: _*)(ev)
+ UserFunction(name, name, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev)
+
+ def deprecated(name: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)(ev: EXPR): UserFunction =
+ UserFunction.deprecated(name, name, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev)
+
+ def apply(name: String, costByLibVersion: Map[StdLibVersion, Long], resultType: TYPE, docString: String, args: (String, TYPE, String)*)(
+ ev: EXPR): UserFunction =
+ UserFunction(name, name, costByLibVersion, resultType, docString, args: _*)(ev)
def apply(name: String, internalName: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)(
ev: EXPR): UserFunction =
+ UserFunction(name, internalName, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev)
+
+ def apply(name: String,
+ internalName: String,
+ costByLibVersion: Map[StdLibVersion, Long],
+ resultType: TYPE,
+ docString: String,
+ args: (String, TYPE, String)*)(ev: EXPR): UserFunction =
new UserFunction(
name = name,
internalName = internalName,
- cost = cost,
+ costByLibVersion = costByLibVersion,
signature = FunctionTypeSignature(result = resultType, args = args.map(a => (a._1, a._2)), header = FunctionHeader.User(internalName)),
ev = ev,
docString = docString,
argsDoc = args.map(a => (a._1 -> a._3)).toArray
)
+
+ def deprecated(name: String,
+ internalName: String,
+ costByLibVersion: Map[StdLibVersion, Long],
+ resultType: TYPE,
+ docString: String,
+ args: (String, TYPE, String)*)(ev: EXPR): UserFunction =
+ new UserFunction(
+ name = name,
+ internalName = internalName,
+ costByLibVersion = costByLibVersion,
+ signature = FunctionTypeSignature(result = resultType, args = args.map(a => (a._1, a._2)), header = FunctionHeader.User(internalName)),
+ ev = ev,
+ docString = docString,
+ argsDoc = args.map(a => (a._1 -> a._3)).toArray
+ ) { override def deprecated = true }
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala
index 9f061c2..965ed38 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala
@@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets
import cats.data.EitherT
import cats.kernel.Monoid
import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.lang.StdLibVersion._
+import com.zbsnetwork.lang.StdLibVersion
import com.zbsnetwork.lang.v1.CTX
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.Types._
@@ -13,11 +13,18 @@ import com.zbsnetwork.lang.v1.evaluator.FunctionIds._
import com.zbsnetwork.lang.v1.evaluator.ctx._
import com.zbsnetwork.lang.v1.parser.BinaryOperation
import com.zbsnetwork.lang.v1.parser.BinaryOperation._
+import scala.collection.mutable.ArrayBuffer
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.ByteBuffer
-import scala.util.Try
+import scala.util.{Try, Success}
object PureContext {
+ import StdLibVersion._
+
+ implicit def intToLong(num: Int): Long = num.toLong
+
private lazy val defaultThrowMessage = "Explicit script termination"
lazy val MaxStringResult = Short.MaxValue
lazy val MaxBytesResult = 65536
@@ -59,7 +66,12 @@ object PureContext {
}
lazy val ne: BaseFunction =
- UserFunction(NE_OP.func, 26, BOOLEAN, "Inequality", ("@a", TYPEPARAM('T'), "value"), ("@b", TYPEPARAM('T'), "value")) {
+ UserFunction(NE_OP.func,
+ Map[StdLibVersion, Long](V1 -> 26, V2 -> 26, V3 -> 1),
+ BOOLEAN,
+ "Inequality",
+ ("@a", TYPEPARAM('T'), "value"),
+ ("@b", TYPEPARAM('T'), "value")) {
FUNCTION_CALL(uNot, List(FUNCTION_CALL(eq, List(REF("@a"), REF("@b")))))
}
@@ -68,12 +80,12 @@ object PureContext {
case _ => Left(defaultThrowMessage)
}
- lazy val throwNoMessage: BaseFunction = UserFunction("throw", 2, NOTHING, "Fail script") {
+ lazy val throwNoMessage: BaseFunction = UserFunction("throw", Map[StdLibVersion, Long](V1 -> 2, V2 -> 2, V3 -> 1), NOTHING, "Fail script") {
FUNCTION_CALL(throwWithMessage, List(CONST_STRING(defaultThrowMessage)))
}
lazy val extract: BaseFunction =
- UserFunction("extract",
+ UserFunction.deprecated("extract",
13,
TYPEPARAM('T'),
"Extract value from option or fail",
@@ -86,7 +98,13 @@ object PureContext {
}
lazy val isDefined: BaseFunction =
- UserFunction("isDefined", 35, BOOLEAN, "Check the value is defined", ("@a", PARAMETERIZEDUNION(List(TYPEPARAM('T'), UNIT)), "Option value")) {
+ UserFunction(
+ "isDefined",
+ Map[StdLibVersion, Long](V1 -> 35, V2 -> 35, V3 -> 1),
+ BOOLEAN,
+ "Check the value is defined",
+ ("@a", PARAMETERIZEDUNION(List(TYPEPARAM('T'), UNIT)), "Option value")
+ ) {
FUNCTION_CALL(ne, List(REF("@a"), REF("unit")))
}
@@ -124,7 +142,7 @@ object PureContext {
case _ => Right(FALSE)
}
- lazy val sizeBytes: BaseFunction = NativeFunction("size", 1, SIZE_BYTES, LONG, "Size of bytes str", ("byteStr", BYTESTR, "vector")) {
+ lazy val sizeBytes: BaseFunction = NativeFunction("size", 1, SIZE_BYTES, LONG, "Size of bytes str", ("byteVector", BYTESTR, "vector")) {
case CONST_BYTESTR(bv) :: Nil => Right(CONST_LONG(bv.arr.length))
case xs => notImplemented("size(byte[])", xs)
}
@@ -218,28 +236,19 @@ object PureContext {
case xs => notImplemented("take(xs: String, number: Long)", xs)
}
- lazy val listConstructor1 =
- NativeFunction("List", 1, CREATE_LIST1, PARAMETERIZEDLIST(TYPEPARAM('T')), "Construct a new List[T]", ("arg1", TYPEPARAM('T'), "arg1"))(xs =>
- Right(ARR(xs.toIndexedSeq)))
-
- lazy val listConstructor2 = NativeFunction("List",
- 1,
- CREATE_LIST2,
- PARAMETERIZEDLIST(TYPEPARAM('T')),
- "Construct a new List[T]",
- ("arg1", TYPEPARAM('T'), "arg1"),
- ("arg2", TYPEPARAM('T'), "arg2"))(xs => Right(ARR(xs.toIndexedSeq)))
-
- lazy val listConstructor3 = NativeFunction(
- "List",
- 1,
- CREATE_LIST3,
- PARAMETERIZEDLIST(TYPEPARAM('T')),
- "Construct a new List[T]",
- ("arg1", TYPEPARAM('T'), "arg1"),
- ("arg2", TYPEPARAM('T'), "arg2"),
- ("arg3", TYPEPARAM('T'), "arg3")
- )(xs => Right(ARR(xs.toIndexedSeq)))
+ lazy val listConstructor: NativeFunction =
+ NativeFunction(
+ "cons",
+ 2,
+ CREATE_LIST,
+ PARAMETERIZEDLIST(PARAMETERIZEDUNION(List(TYPEPARAM('A'), TYPEPARAM('B')))),
+ "Construct a new List[T]",
+ ("head", TYPEPARAM('A'), "head"),
+ ("tail", PARAMETERIZEDLIST(TYPEPARAM('B')), "tail")
+ ) {
+ case h :: ARR(t) :: Nil => Right(ARR(h +: t))
+ case xs => notImplemented("cons(head: T, tail: LIST[T]", xs)
+ }
lazy val dropString: BaseFunction =
NativeFunction("drop", 1, DROP_STRING, STRING, "Remmove sring prefix", ("xs", STRING, "string"), ("number", LONG, "prefix size")) {
@@ -281,6 +290,87 @@ object PureContext {
)
}
+ val UTF8Decoder = UTF_8.newDecoder
+
+ lazy val toUtf8String: BaseFunction =
+ NativeFunction("toUtf8String", 20, UTF8STRING, STRING, "Convert UTF8 bytes to string", ("u", BYTESTR, "utf8")) {
+ case CONST_BYTESTR(u) :: Nil => Try(CONST_STRING(UTF8Decoder.decode(ByteBuffer.wrap(u.arr)).toString)).toEither.left.map(_.toString)
+ case xs => notImplemented("toUtf8String(u: byte[])", xs)
+ }
+
+ lazy val toLong: BaseFunction =
+ NativeFunction("toInt", 10, BININT, LONG, "Deserialize big endian 8-bytes value", ("bin", BYTESTR, "8-bytes BE binaries")) {
+ case CONST_BYTESTR(u) :: Nil => Try(CONST_LONG(ByteBuffer.wrap(u.arr).getLong())).toEither.left.map(_.toString)
+ case xs => notImplemented("toInt(u: byte[])", xs)
+ }
+
+ lazy val toLongOffset: BaseFunction =
+ NativeFunction("toInt", 10, BININT_OFF, LONG, "Deserialize big endian 8-bytes value", ("bin", BYTESTR, "8-bytes BE binaries"), ("offet", LONG, "bytes offset")) {
+ case CONST_BYTESTR(ByteStr(u)) :: CONST_LONG(o) :: Nil => if( o >= 0 && o <= u.size - 8) {
+ Try(CONST_LONG(ByteBuffer.wrap(u).getLong(o.toInt))).toEither.left.map(_.toString)
+ } else {
+ Left("IndexOutOfBounds")
+ }
+ case xs => notImplemented("toInt(u: byte[], off: int)", xs)
+ }
+
+ lazy val indexOf: BaseFunction =
+ NativeFunction("indexOf", 20, INDEXOF, optionLong, "index of substring", ("str", STRING, "String for analize"), ("substr", STRING, "String for searching")) {
+ case CONST_STRING(m) :: CONST_STRING(sub) :: Nil => Right({
+ val i = m.indexOf(sub)
+ if( i != -1 ) {
+ CONST_LONG(i.toLong)
+ } else {
+ unit
+ }
+ })
+ case xs => notImplemented("indexOf(STRING, STRING)", xs)
+ }
+
+ lazy val indexOfN: BaseFunction =
+ NativeFunction("indexOf", 20, INDEXOFN, optionLong, "index of substring after offset", ("str", STRING, "String for analize"), ("substr", STRING, "String for searching"), ("offset", LONG, "offset")) {
+ case CONST_STRING(m) :: CONST_STRING(sub) :: CONST_LONG(off) :: Nil => Right( if(off >= 0 && off <= m.length) {
+ val i = m.indexOf(sub, off.toInt)
+ if( i != -1 ) {
+ CONST_LONG(i.toLong)
+ } else {
+ unit
+ }
+ } else {
+ unit
+ } )
+ case xs => notImplemented("indexOf(STRING, STRING)", xs)
+ }
+
+ def split(m: String, sep: String, buffer: ArrayBuffer[CONST_STRING] = ArrayBuffer[CONST_STRING](), start: Int = 0): IndexedSeq[CONST_STRING] = {
+ m.indexOf(sep, start) match {
+ case -1 =>
+ buffer += CONST_STRING(m.substring(start))
+ buffer.result
+ case n =>
+ buffer += CONST_STRING(m.substring(0, n))
+ split(m, sep, buffer, n + sep.length)
+ }
+ }
+
+ lazy val splitStr: BaseFunction =
+ NativeFunction("split", 100, SPLIT, listString, "split string by separator", ("str", STRING, "String for splitting"), ("separator", STRING, "separator")) {
+ case CONST_STRING(m) :: CONST_STRING(sep) :: Nil => Right( ARR(split(m, sep)))
+ case xs => notImplemented("split(STRING, STRING)", xs)
+ }
+
+ lazy val parseInt: BaseFunction =
+ NativeFunction("parseInt", 20, PARSEINT, optionLong, "parse string to integer", ("str", STRING, "String for parsing")) {
+ case CONST_STRING(u) :: Nil => Try(CONST_LONG(u.toInt)).orElse(Success(unit)).toEither.left.map(_.toString)
+ case xs => notImplemented("parseInt(STRING)", xs)
+ }
+
+ lazy val parseIntVal: BaseFunction =
+ NativeFunction("parseIntValue", 20, PARSEINTV, LONG, "parse string to integer with fail on errors", ("str", STRING, "String for parsing")) {
+ case CONST_STRING(u) :: Nil => Try(CONST_LONG(u.toInt)).toEither.left.map(_.toString)
+ case xs => notImplemented("parseInt(STRING)", xs)
+ }
+
def createRawOp(op: BinaryOperation, t: TYPE, r: TYPE, func: Short, docString: String, arg1Doc: String, arg2Doc: String, complicity: Int = 1)(
body: (EVALUATED, EVALUATED) => Either[String, EVALUATED]): BaseFunction =
NativeFunction(opsToFunctions(op), complicity, func, r, docString, ("a", t, arg1Doc), ("b", t, arg2Doc)) {
@@ -327,13 +417,20 @@ object PureContext {
case _ => ???
}
- lazy val uMinus: BaseFunction = UserFunction("-", 9, LONG, "Change integer sign", ("@n", LONG, "value")) {
- FUNCTION_CALL(subLong, List(CONST_LONG(0), REF("@n")))
- }
+ lazy val uMinus: BaseFunction =
+ UserFunction("-", Map[StdLibVersion, Long](V1 -> 9, V2 -> 9, V3 -> 1), LONG, "Change integer sign", ("@n", LONG, "value")) {
+ FUNCTION_CALL(subLong, List(CONST_LONG(0), REF("@n")))
+ }
- lazy val uNot: BaseFunction = UserFunction("!", 11, BOOLEAN, "unary negation", ("@p", BOOLEAN, "boolean")) {
- IF(FUNCTION_CALL(eq, List(REF("@p"), FALSE)), TRUE, FALSE)
- }
+ lazy val uNot: BaseFunction =
+ UserFunction("!", Map[StdLibVersion, Long](V1 -> 11, V2 -> 11, V3 -> 1), BOOLEAN, "unary negation", ("@p", BOOLEAN, "boolean")) {
+ IF(REF("@p"), FALSE, TRUE)
+ }
+
+ lazy val ensure: BaseFunction =
+ UserFunction("ensure", 16, BOOLEAN, "Ensure parameter is true", ("@b", BOOLEAN, "condition"), ("@msg", STRING, "error message")) {
+ IF(REF("@b"), TRUE, FUNCTION_CALL(throwWithMessage, List(REF("@msg"))))
+ }
private lazy val operators: Array[BaseFunction] = Array(
mulLong,
@@ -353,7 +450,9 @@ object PureContext {
uNot
)
- private lazy val vars: Map[String, ((FINAL, String), LazyVal)] = Map(("unit", ((UNIT, "Single instance value"), LazyVal(EitherT.pure(unit)))))
+ private lazy val vars: Map[String, ((FINAL, String), LazyVal)] = Map(
+ ("unit", ((UNIT, "Single instance value"), LazyVal(EitherT.pure(unit))))
+ )
private lazy val functions = Array(
fraction,
sizeBytes,
@@ -380,11 +479,11 @@ object PureContext {
private lazy val ctx = CTX(
Seq(
- new DefinedType { lazy val name = "Unit"; lazy val typeRef = UNIT },
- new DefinedType { lazy val name = "Int"; lazy val typeRef = LONG },
- new DefinedType { lazy val name = "Boolean"; lazy val typeRef = BOOLEAN },
- new DefinedType { lazy val name = "ByteStr"; lazy val typeRef = BYTESTR },
- new DefinedType { lazy val name = "String"; lazy val typeRef = STRING }
+ new DefinedType { lazy val name = "Unit"; lazy val typeRef = UNIT },
+ new DefinedType { lazy val name = "Int"; lazy val typeRef = LONG },
+ new DefinedType { lazy val name = "Boolean"; lazy val typeRef = BOOLEAN },
+ new DefinedType { lazy val name = "ByteVector"; lazy val typeRef = BYTESTR },
+ new DefinedType { lazy val name = "String"; lazy val typeRef = STRING }
),
vars,
functions
@@ -393,7 +492,15 @@ object PureContext {
def build(version: StdLibVersion): CTX =
version match {
case V1 | V2 => ctx
- case V3 => Monoid.combine(ctx, CTX(Seq.empty, Map.empty, Array(listConstructor1, listConstructor2, listConstructor3)))
+ case V3 =>
+ Monoid.combine(
+ ctx,
+ CTX(
+ Seq.empty,
+ Map(("nil", ((LIST(NOTHING), "empty list of any type"), LazyVal(EitherT.pure(ARR(IndexedSeq.empty[EVALUATED])))))),
+ Array(listConstructor, ensure, toUtf8String, toLong, toLongOffset, indexOf, indexOfN, splitStr, parseInt, parseIntVal)
+ )
+ )
}
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala
index 591bb42..78158a1 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala
@@ -1,13 +1,12 @@
package com.zbsnetwork.lang.v1.evaluator.ctx
import com.zbsnetwork.lang.v1.compiler.Terms.CaseObj
-import com.zbsnetwork.lang.v1.compiler.Types.CASETYPEREF
+import com.zbsnetwork.lang.v1.compiler.Types.UNIT
package object impl {
def notImplemented(funcName: String, args: List[Any]): Nothing = throw new Exception(
s"Can't apply (${args.map(_.getClass.getSimpleName).mkString(", ")}) to '$funcName'"
)
- lazy val UNIT: CASETYPEREF = CASETYPEREF("Unit", List.empty)
lazy val unit: CaseObj = CaseObj(UNIT, Map.empty)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala
index 9de7e8a..bfd23d7 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala
@@ -76,18 +76,19 @@ object Bindings {
CaseObj(
buildOrderType(proofsEnabled).typeRef,
Map(
- "id" -> ord.id,
- "sender" -> senderObject(ord.sender),
- "senderPublicKey" -> ord.senderPublicKey,
- "matcherPublicKey" -> ord.matcherPublicKey,
- "assetPair" -> assetPair(ord.assetPair),
- "orderType" -> ordType(ord.orderType),
- "amount" -> ord.amount,
- "price" -> ord.price,
- "timestamp" -> ord.timestamp,
- "expiration" -> ord.expiration,
- "matcherFee" -> ord.matcherFee,
- "bodyBytes" -> ord.bodyBytes,
+ "id" -> ord.id,
+ "sender" -> senderObject(ord.sender),
+ "senderPublicKey" -> ord.senderPublicKey,
+ "matcherPublicKey" -> ord.matcherPublicKey,
+ "assetPair" -> assetPair(ord.assetPair),
+ "orderType" -> ordType(ord.orderType),
+ "amount" -> ord.amount,
+ "price" -> ord.price,
+ "timestamp" -> ord.timestamp,
+ "expiration" -> ord.expiration,
+ "matcherFee" -> ord.matcherFee,
+ "bodyBytes" -> ord.bodyBytes,
+ "matcherFeeAssetId" -> ord.matcherFeeAssetId,
proofsPart(ord.proofs)
)
)
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala
index e91065f..a9da516 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala
@@ -3,7 +3,6 @@ package com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs
import com.zbsnetwork.lang.StdLibVersion
import com.zbsnetwork.lang.StdLibVersion.StdLibVersion
import com.zbsnetwork.lang.v1.compiler.Types._
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl._
import com.zbsnetwork.lang.v1.evaluator.ctx.{CaseType, DefinedType, UnionType}
object Types {
@@ -13,17 +12,15 @@ object Types {
val addressOrAliasType = UNION(addressType.typeRef, aliasType.typeRef)
val transfer = CaseType("Transfer", List("recipient" -> addressOrAliasType, "amount" -> LONG))
- val optionByteVector = UNION(BYTESTR, UNIT)
val optionAddress = UNION(addressType.typeRef, UNIT)
- val optionLong = UNION(LONG, UNIT)
- val listByteVector: LIST = LIST(BYTESTR)
val listTransfers = LIST(transfer.typeRef)
val paymentType = CaseType("AttachedPayment", List("asset" -> optionByteVector, "amount" -> LONG))
val optionPayment = UNION(paymentType.typeRef, UNIT)
- val invocationType = CaseType("Invocation", List("caller" -> addressType.typeRef, "contractAddress" -> addressType.typeRef, "payment" -> optionPayment))
+ val invocationType =
+ CaseType("Invocation", List("caller" -> addressType.typeRef, "contractAddress" -> addressType.typeRef, "payment" -> optionPayment))
private val header = List(
"id" -> BYTESTR,
@@ -192,15 +189,16 @@ object Types {
"Order",
addProofsIfNeeded(
List(
- "id" -> BYTESTR,
- "matcherPublicKey" -> BYTESTR,
- "assetPair" -> assetPairType.typeRef,
- "orderType" -> ordTypeType,
- "price" -> LONG,
- "amount" -> LONG,
- "timestamp" -> LONG,
- "expiration" -> LONG,
- "matcherFee" -> LONG
+ "id" -> BYTESTR,
+ "matcherPublicKey" -> BYTESTR,
+ "assetPair" -> assetPairType.typeRef,
+ "orderType" -> ordTypeType,
+ "price" -> LONG,
+ "amount" -> LONG,
+ "timestamp" -> LONG,
+ "expiration" -> LONG,
+ "matcherFee" -> LONG,
+ "matcherFeeAssetId" -> optionByteVector
) ++ proven,
proofsEnabled
)
@@ -258,18 +256,17 @@ object Types {
List(genesisTransactionType, buildPaymentTransactionType(proofsEnabled))
}
- def buildAssetSupportedTransactions(proofsEnabled: Boolean) = List(
+ def buildAssetSupportedTransactions(proofsEnabled: Boolean, v: StdLibVersion) = List(
buildReissueTransactionType(proofsEnabled),
buildBurnTransactionType(proofsEnabled),
buildMassTransferTransactionType(proofsEnabled),
buildExchangeTransactionType(proofsEnabled),
buildTransferTransactionType(proofsEnabled),
- buildSetAssetScriptTransactionType(proofsEnabled),
- buildContractInvokationTransactionType(proofsEnabled),
- )
+ buildSetAssetScriptTransactionType(proofsEnabled)
+ ) ++ (if (v == StdLibVersion.V3) List(buildContractInvokationTransactionType(proofsEnabled)) else List.empty)
def buildActiveTransactionTypes(proofsEnabled: Boolean, v: StdLibVersion): List[CaseType] = {
- buildAssetSupportedTransactions(proofsEnabled) ++
+ buildAssetSupportedTransactions(proofsEnabled, v) ++
List(
buildIssueTransactionType(proofsEnabled),
buildLeaseTransactionType(proofsEnabled),
@@ -278,7 +275,7 @@ object Types {
buildSetScriptTransactionType(proofsEnabled),
buildSponsorFeeTransactionType(proofsEnabled),
buildDataTransactionType(proofsEnabled)
- ) ++ (if (v == StdLibVersion.V3) List(buildContractInvokationTransactionType(proofsEnabled)) else List.empty)
+ )
}
def buildZbsTypes(proofsEnabled: Boolean, v: StdLibVersion): Seq[DefinedType] = {
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala
index 9835fd2..0716e62 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala
@@ -107,6 +107,22 @@ object ZbsContext {
val getBinaryByIndexF: BaseFunction = getDataByIndexF("getBinary", DataType.ByteArray)
val getStringByIndexF: BaseFunction = getDataByIndexF("getString", DataType.String)
+ def withExtract(f: BaseFunction) = {
+ val args = f.signature.args.zip(f.argsDoc).map {
+ case ((name, ty), (_name, doc)) => ("@" ++ name, ty, doc)
+ }
+ UserFunction(
+ f.name ++ "Value",
+ "@extr" ++ f.header.toString,
+ f.cost,
+ f.signature.result.asInstanceOf[UNION].l.find(_ != UNIT).get,
+ f.docString ++ " (fail on error)",
+ args : _*
+ ) {
+ FUNCTION_CALL(PureContext.extract, List(FUNCTION_CALL(f.header, args.map(a => REF(a._1)).toList)))
+ }
+ }
+
def secureHashExpr(xs: EXPR): EXPR = FUNCTION_CALL(
FunctionHeader.Native(KECCAK256),
List(
@@ -332,7 +348,7 @@ object ZbsContext {
val scriptInputType =
if (isTokenContext)
- UNION(buildAssetSupportedTransactions(proofsEnabled).map(_.typeRef))
+ UNION(buildAssetSupportedTransactions(proofsEnabled, version).map(_.typeRef))
else
UNION((buildOrderType(proofsEnabled) :: buildActiveTransactionTypes(proofsEnabled, version)).map(_.typeRef))
@@ -348,8 +364,8 @@ object ZbsContext {
("tx", ((scriptInputType, "Processing transaction"), LazyVal(EitherT(inputEntityCoeval))))
),
3 -> Map(
- ("Sell", ((ordTypeType, "Sell OrderType"), LazyVal(EitherT(sellOrdTypeCoeval)))),
- ("Buy", ((ordTypeType, "Buy OrderType"), LazyVal(EitherT(buyOrdTypeCoeval))))
+ ("Sell", ((sellType.typeRef, "Sell OrderType"), LazyVal(EitherT(sellOrdTypeCoeval)))),
+ ("Buy", ((buyType.typeRef, "Buy OrderType"), LazyVal(EitherT(buyOrdTypeCoeval))))
)
)
@@ -378,11 +394,24 @@ object ZbsContext {
val types = buildZbsTypes(proofsEnabled, version)
CTX(
- types ++ (if (version == V3)
+ types ++ (if (version == V3) {
List(writeSetType, paymentType, contractTransfer, contractTransferSetType, contractResultType, invocationType)
- else List.empty),
+ } else List.empty),
commonVars ++ vars(version),
- functions
+ functions ++ List(getIntegerFromStateF,
+ getBooleanFromStateF,
+ getBinaryFromStateF,
+ getStringFromStateF,
+ getIntegerFromArrayF,
+ getBooleanFromArrayF,
+ getBinaryFromArrayF,
+ getStringFromArrayF,
+ getIntegerByIndexF,
+ getBooleanByIndexF,
+ getBinaryByIndexF,
+ getStringByIndexF,
+ addressFromStringF
+ ).map(withExtract)
)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala
index ee3be86..8544188 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala
@@ -13,12 +13,14 @@ sealed abstract class BinaryOperation {
object BinaryOperation {
- val opsByPriority: List[List[BinaryOperation]] = List(
- List(OR_OP, AND_OP),
- List(EQ_OP, NE_OP),
- List(GT_OP, GE_OP, LT_OP, LE_OP),
- List(SUM_OP, SUB_OP),
- List(MUL_OP, DIV_OP, MOD_OP)
+ // No monadic notion here, Left and Right mean `left-assosiative and `right-assosiative`
+ val opsByPriority: List[Either[List[BinaryOperation], List[BinaryOperation]]] = List(
+ Right(List(CONS_OP)),
+ Left(List(OR_OP, AND_OP)),
+ Left(List(EQ_OP, NE_OP)),
+ Left(List(GT_OP, GE_OP, LT_OP, LE_OP)),
+ Left(List(SUM_OP, SUB_OP)),
+ Left(List(MUL_OP, DIV_OP, MOD_OP))
)
def opsToFunctions(op: BinaryOperation): String = op.func
@@ -71,5 +73,12 @@ object BinaryOperation {
BINARY_OP(Pos(start, end), op2, LT_OP, op1)
}
}
+ case object CONS_OP extends BinaryOperation {
+ override val func: String = "::"
+ override def expr(start: Int, end: Int, op1: EXPR, op2: EXPR): EXPR = {
+ val pos = Pos(start, end)
+ FUNCTION_CALL(Pos(start, end), PART.VALID(pos, "cons"), List(op1, op2))
+ }
+ }
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala
index 1da10f2..1cdd4c8 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala
@@ -2,7 +2,6 @@ package com.zbsnetwork.lang.v1.parser
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.lang.v1.parser.BinaryOperation._
-import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos
import com.zbsnetwork.lang.v1.parser.Expressions._
import com.zbsnetwork.lang.v1.parser.UnaryOperation._
import fastparse.{WhitespaceApi, core}
@@ -29,6 +28,9 @@ object Parser {
val notEndOfString = CharPred(_ != '\"')
val specialSymbols = P("\\" ~~ AnyChar)
val comment: P[Unit] = P("#" ~~ CharPred(_ != '\n').repX).rep.map(_ => ())
+ val directive: P[Unit] = P("{-#" ~ CharPred(el => el != '\n' && el != '#').rep ~ "#-}").rep.map(_ => ())
+
+ val unusedText = comment ~ directive ~ comment
val escapedUnicodeSymbolP: P[(Int, String, Int)] = P(Index ~~ (NoCut(unicodeSymbolP) | specialSymbols).! ~~ Index)
val stringP: P[EXPR] = P(Index ~~ "\"" ~/ Pass ~~ (escapedUnicodeSymbolP | notEndOfString).!.repX ~~ "\"" ~~ Index)
@@ -160,9 +162,17 @@ object Parser {
case (_, id, None, _) => id
}
- val extractableAtom: P[EXPR] = P(curlyBracesP | bracesP | maybeFunctionCallP)
+ val list: P[EXPR] = (Index ~~ P("[") ~ functionCallArgs ~ P("]") ~~ Index).map { case (s,e,f) =>
+ val pos = Pos(s, f)
+ e.foldRight(REF(pos, PART.VALID(pos,"nil")):EXPR) { (v,l) => FUNCTION_CALL(pos, PART.VALID(pos, "cons"), List(v,l)) }
+ }
+
+ val extractableAtom: P[EXPR] = P(curlyBracesP | bracesP |
+ byteVectorP | stringP | numberP | trueP | falseP | list |
+ maybeFunctionCallP)
abstract class Accessor
+ case class Method(name: PART[String], args: Seq[EXPR]) extends Accessor
case class Getter(name: PART[String]) extends Accessor
case class ListIndex(index: EXPR) extends Accessor
val typesP: P[Seq[PART[String]]] = anyVarName.rep(min = 1, sep = comment ~ "|" ~ comment)
@@ -170,18 +180,18 @@ object Parser {
val funcname = anyVarName
val argWithType = anyVarName ~ ":" ~ typesP
val args = "(" ~ argWithType.rep(sep = ",") ~ ")"
- val funcHeader = "func" ~ funcname ~ args ~ "=" ~ P(singleBaseExpr | ("{" ~ baseExpr ~ "}"))
+ val funcHeader = Index ~~ "func" ~ funcname ~ args ~ "=" ~ P(singleBaseExpr | ("{" ~ baseExpr ~ "}")) ~~ Index
funcHeader.map {
- case (name, args, expr) => FUNC(AnyPos, name, args, expr)
+ case (start, name, args, expr, end) => FUNC(Pos(start, end), name, args, expr)
}
}
- val annotationP: P[ANNOTATION] = ("@" ~ anyVarName ~ "(" ~ anyVarName.rep(sep = ",") ~ ")").map {
- case (name: PART[String], args: Seq[PART[String]]) => ANNOTATION(AnyPos, name, args)
+ val annotationP: P[ANNOTATION] = (Index ~~ "@" ~ anyVarName ~ "(" ~ anyVarName.rep(sep = ",") ~ ")" ~~ Index).map {
+ case (start, name: PART[String], args: Seq[PART[String]], end) => ANNOTATION(Pos(start, end), name, args)
}
- val annotatedFunc: P[ANNOTATEDFUNC] = (annotationP.rep ~ funcP).map {
- case (as, f) => ANNOTATEDFUNC(AnyPos, as, f)
+ val annotatedFunc: P[ANNOTATEDFUNC] = (Index ~~ annotationP.rep ~ funcP ~~ Index).map {
+ case (start, as, f, end) => ANNOTATEDFUNC(Pos(start, end), as, f)
}
val matchCaseP: P[MATCH_CASE] = {
@@ -226,8 +236,9 @@ object Parser {
}
val accessP: P[(Int, Accessor, Int)] = P(
- ("" ~ comment ~ Index ~ "." ~/ comment ~ anyVarName.map(Getter) ~~ Index) |
- (Index ~~ "[" ~/ baseExpr.map(ListIndex) ~ "]" ~~ Index)
+ (("" ~ comment ~ Index ~ "." ~/ comment ~ (anyVarName.map(Getter) ~/ comment ~~ ("(" ~/ comment ~ functionCallArgs ~/ comment ~ ")").?).map {
+ case ((g@Getter(name)), args) => args.fold(g:Accessor)(a => Method(name, a))
+ }) ~~ Index) | (Index ~~ "[" ~/ baseExpr.map(ListIndex) ~ "]" ~~ Index)
)
val maybeAccessP: P[EXPR] =
@@ -238,6 +249,7 @@ object Parser {
case (e, (accessStart, a, accessEnd)) =>
a match {
case Getter(n) => GETTER(Pos(start, accessEnd), e, n)
+ case Method(n, args) => FUNCTION_CALL(Pos(start, accessEnd), n, (e :: args.toList))
case ListIndex(index) => FUNCTION_CALL(Pos(start, accessEnd), PART.VALID(Pos(accessStart, accessEnd), "getElement"), List(e, index))
}
}
@@ -306,30 +318,42 @@ object Parser {
}
val baseAtom = comment ~
- P(ifP | matchP | byteVectorP | stringP | numberP | trueP | falseP | block | maybeAccessP) ~
+ P(ifP | matchP | block | maybeAccessP) ~
comment
- lazy val baseExpr = P(binaryOp(baseAtom, opsByPriority) | baseAtom)
+ lazy val baseExpr = P(binaryOp(baseAtom, opsByPriority))
val singleBaseAtom = comment ~
- P(ifP | matchP | byteVectorP | stringP | numberP | trueP | falseP | maybeAccessP) ~
+ P(ifP | matchP | maybeAccessP) ~
comment
- lazy val singleBaseExpr = P(binaryOp(singleBaseAtom, opsByPriority) | singleBaseAtom)
+ val singleBaseExpr = P(binaryOp(singleBaseAtom, opsByPriority))
- lazy val declaration = P(letP | funcP)
+ val declaration = P(letP | funcP)
+
+ def revp[A,B](l:A, s:Seq[(B,A)], o:Seq[(A,B)]=Seq.empty) : (Seq[(A,B)], A) = {
+ s.foldLeft((o,l)) { (acc, op) => (acc, op) match { case ((o,l),(b,a)) => ((l,b) +: o) -> a } }
+ }
- def binaryOp(atom: P[EXPR], rest: List[List[BinaryOperation]]): P[EXPR] = rest match {
+ def binaryOp(atom: P[EXPR], rest: List[Either[List[BinaryOperation], List[BinaryOperation]]]): P[EXPR] = rest match {
case Nil => unaryOp(atom, unaryOps)
- case kinds :: restOps =>
+ case Left(kinds) :: restOps =>
val operand = binaryOp(atom, restOps)
val kind = kinds.map(_.parser).reduce((pl, pr) => P(pl | pr))
P(Index ~~ operand ~ P(kind ~ (NoCut(operand) | Index.map(i => INVALID(Pos(i, i), "expected a second operator")))).rep).map {
case (start, left: EXPR, r: Seq[(BinaryOperation, EXPR)]) =>
r.foldLeft(left) { case (acc, (currKind, currOperand)) => currKind.expr(start, currOperand.position.end, acc, currOperand) }
}
+ case Right(kinds) :: restOps =>
+ val operand = binaryOp(atom, restOps)
+ val kind = kinds.map(_.parser).reduce((pl, pr) => P(pl | pr))
+ P(Index ~~ operand ~ P(kind ~ (NoCut(operand) | Index.map(i => INVALID(Pos(i, i), "expected a second operator")))).rep).map {
+ case (start, left: EXPR, r: Seq[(BinaryOperation, EXPR)]) =>
+ val (ops,s) = revp(left, r)
+ ops.foldLeft(s) { case (acc, (currOperand, currKind)) => currKind.expr(start, currOperand.position.end, currOperand, acc) }
+ }
}
def unaryOp(atom: P[EXPR], ops: List[UnaryOperation]): P[EXPR] = ops.foldRight(atom) {
@@ -339,12 +363,12 @@ object Parser {
} | acc
}
- def parseExpr(str: String): core.Parsed[EXPR, Char, String] = P(Start ~ (baseExpr | invalid) ~ End).parse(str)
+ def parseExpr(str: String): core.Parsed[EXPR, Char, String] = P(Start ~ unusedText ~ (baseExpr | invalid) ~ End).parse(str)
def parseContract(str: String): core.Parsed[CONTRACT, Char, String] =
- P(Start ~ comment.? ~ (declaration.rep) ~ comment.? ~ (annotatedFunc.rep) ~ End)
+ P(Start ~ unusedText ~ (declaration.rep) ~ comment ~ (annotatedFunc.rep) ~ End ~~ Index)
.map {
- case (ds, fs) => CONTRACT(AnyPos, ds.toList, fs.toList)
+ case (ds, fs, end) => CONTRACT(Pos(0, end), ds.toList, fs.toList)
}
.parse(str)
}
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala
index 16d3f6f..211bd05 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala
@@ -3,6 +3,7 @@ package com.zbsnetwork.lang.v1.testing
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.lang.v1.parser.BinaryOperation
import com.zbsnetwork.lang.v1.parser.BinaryOperation._
+import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos
import com.zbsnetwork.lang.v1.parser.Expressions._
import com.zbsnetwork.lang.v1.parser.Parser.keywords
import org.scalacheck._
@@ -12,11 +13,11 @@ import scala.reflect.ClassTag
trait ScriptGen {
- def CONST_LONGgen: Gen[(EXPR, Long)] = Gen.choose(Long.MinValue, Long.MaxValue).map(v => (CONST_LONG(Pos(0, 0), v), v))
+ def CONST_LONGgen: Gen[(EXPR, Long)] = Gen.choose(Long.MinValue, Long.MaxValue).map(v => (CONST_LONG(AnyPos, v), v))
def BOOLgen(gas: Int): Gen[(EXPR, Boolean)] =
if (gas > 0) Gen.oneOf(GEgen(gas - 1), GTgen(gas - 1), EQ_INTgen(gas - 1), ANDgen(gas - 1), ORgen(gas - 1), IF_BOOLgen(gas - 1))
- else Gen.const((TRUE(Pos(0, 0)), true))
+ else Gen.const((TRUE(AnyPos), true))
def SUMgen(gas: Int): Gen[(EXPR, Long)] =
for {
@@ -24,9 +25,9 @@ trait ScriptGen {
(i2, v2) <- INTGen((gas - 2) / 2)
} yield
if ((BigInt(v1) + BigInt(v2)).isValidLong) {
- (BINARY_OP(Pos(0, 0), i1, SUM_OP, i2), v1 + v2)
+ (BINARY_OP(AnyPos, i1, SUM_OP, i2), v1 + v2)
} else {
- (BINARY_OP(Pos(0, 0), i1, SUB_OP, i2), v1 - v2)
+ (BINARY_OP(AnyPos, i1, SUB_OP, i2), v1 - v2)
}
def SUBgen(gas: Int): Gen[(EXPR, Long)] =
@@ -35,9 +36,9 @@ trait ScriptGen {
(i2, v2) <- INTGen((gas - 2) / 2)
} yield
if ((BigInt(v1) - BigInt(v2)).isValidLong) {
- (BINARY_OP(Pos(0, 0), i1, SUB_OP, i2), v1 - v2)
+ (BINARY_OP(AnyPos, i1, SUB_OP, i2), v1 - v2)
} else {
- (BINARY_OP(Pos(0, 0), i1, SUM_OP, i2), v1 + v2)
+ (BINARY_OP(AnyPos, i1, SUM_OP, i2), v1 + v2)
}
def INTGen(gas: Int): Gen[(EXPR, Long)] =
@@ -47,7 +48,7 @@ trait ScriptGen {
SUMgen(gas - 1),
SUBgen(gas - 1),
IF_INTgen(gas - 1),
- INTGen(gas - 1).filter(v => (-BigInt(v._2)).isValidLong).map(e => (FUNCTION_CALL(Pos(0, 0), PART.VALID(Pos(0, 0), "-"), List(e._1)), -e._2))
+ INTGen(gas - 1).filter(v => (-BigInt(v._2)).isValidLong).map(e => (FUNCTION_CALL(AnyPos, PART.VALID(AnyPos, "-"), List(e._1)), -e._2))
)
else CONST_LONGgen
@@ -55,63 +56,63 @@ trait ScriptGen {
for {
(i1, v1) <- INTGen((gas - 2) / 2)
(i2, v2) <- INTGen((gas - 2) / 2)
- } yield (BINARY_OP(Pos(0, 0), i1, GE_OP, i2), v1 >= v2)
+ } yield (BINARY_OP(AnyPos, i1, GE_OP, i2), v1 >= v2)
def GTgen(gas: Int): Gen[(EXPR, Boolean)] =
for {
(i1, v1) <- INTGen((gas - 2) / 2)
(i2, v2) <- INTGen((gas - 2) / 2)
- } yield (BINARY_OP(Pos(0, 0), i1, GT_OP, i2), v1 > v2)
+ } yield (BINARY_OP(AnyPos, i1, GT_OP, i2), v1 > v2)
def EQ_INTgen(gas: Int): Gen[(EXPR, Boolean)] =
for {
(i1, v1) <- INTGen((gas - 2) / 2)
(i2, v2) <- INTGen((gas - 2) / 2)
- } yield (BINARY_OP(Pos(0, 0), i1, EQ_OP, i2), v1 == v2)
+ } yield (BINARY_OP(AnyPos, i1, EQ_OP, i2), v1 == v2)
def ANDgen(gas: Int): Gen[(EXPR, Boolean)] =
for {
(i1, v1) <- BOOLgen((gas - 2) / 2)
(i2, v2) <- BOOLgen((gas - 2) / 2)
- } yield (BINARY_OP(Pos(0, 0), i1, AND_OP, i2), v1 && v2)
+ } yield (BINARY_OP(AnyPos, i1, AND_OP, i2), v1 && v2)
def ORgen(gas: Int): Gen[(EXPR, Boolean)] =
for {
(i1, v1) <- BOOLgen((gas - 2) / 2)
(i2, v2) <- BOOLgen((gas - 2) / 2)
- } yield (BINARY_OP(Pos(0, 0), i1, OR_OP, i2), v1 || v2)
+ } yield (BINARY_OP(AnyPos, i1, OR_OP, i2), v1 || v2)
def IF_BOOLgen(gas: Int): Gen[(EXPR, Boolean)] =
for {
(cnd, vcnd) <- BOOLgen((gas - 3) / 3)
(t, vt) <- BOOLgen((gas - 3) / 3)
(f, vf) <- BOOLgen((gas - 3) / 3)
- } yield (IF(Pos(0, 0), cnd, t, f), if (vcnd) { vt } else { vf })
+ } yield (IF(AnyPos, cnd, t, f), if (vcnd) { vt } else { vf })
def IF_INTgen(gas: Int): Gen[(EXPR, Long)] =
for {
(cnd, vcnd) <- BOOLgen((gas - 3) / 3)
(t, vt) <- INTGen((gas - 3) / 3)
(f, vf) <- INTGen((gas - 3) / 3)
- } yield (IF(Pos(0, 0), cnd, t, f), if (vcnd) { vt } else { vf })
+ } yield (IF(AnyPos, cnd, t, f), if (vcnd) { vt } else { vf })
def STRgen: Gen[EXPR] =
- Gen.identifier.map(PART.VALID[String](Pos(0, 0), _)).map(CONST_STRING(Pos(0, 0), _))
+ Gen.identifier.map(PART.VALID[String](AnyPos, _)).map(CONST_STRING(AnyPos, _))
def LETgen(gas: Int): Gen[LET] =
for {
name <- Gen.identifier
(value, _) <- BOOLgen((gas - 3) / 3)
- } yield LET(Pos(0, 0), PART.VALID(Pos(0, 0), name), value, Seq.empty)
+ } yield LET(AnyPos, PART.VALID(AnyPos, name), value, Seq.empty)
def REFgen: Gen[EXPR] =
- Gen.identifier.filter(!keywords(_)).map(PART.VALID[String](Pos(0, 0), _)).map(REF(Pos(0, 0), _))
+ Gen.identifier.filter(!keywords(_)).map(PART.VALID[String](AnyPos, _)).map(REF(AnyPos, _))
def BLOCKgen(gas: Int): Gen[EXPR] =
for {
let <- LETgen((gas - 3) / 3)
body <- Gen.oneOf(BOOLgen((gas - 3) / 3).map(_._1), BLOCKgen((gas - 3) / 3)) // BLOCKGen wasn't add to BOOLGen since issue: NODE-700
- } yield BLOCK(Pos(0, 0), let, body)
+ } yield BLOCK(AnyPos, let, body)
private val spaceChars: Seq[Char] = " \t\n\r"
@@ -171,7 +172,7 @@ trait ScriptGenParser extends ScriptGen {
override def BOOLgen(gas: Int): Gen[(EXPR, Boolean)] = {
if (gas > 0)
Gen.oneOf(GEgen(gas - 1), GTgen(gas - 1), EQ_INTgen(gas - 1), ANDgen(gas - 1), ORgen(gas - 1), IF_BOOLgen(gas - 1), REFgen.map(r => (r, false)))
- else Gen.const((TRUE(Pos(0, 0)), true))
+ else Gen.const((TRUE(AnyPos), true))
}
override def INTGen(gas: Int): Gen[(EXPR, Long)] =
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala
index 6fad21d..7e90ee1 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala
@@ -1,7 +1,9 @@
package com.zbsnetwork.lang.v1.testing
-import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.lang.contract.{Contract, ContractSerDe}
+import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction, VerifierAnnotation, VerifierFunction}
import com.zbsnetwork.lang.v1.FunctionHeader
+import com.zbsnetwork.lang.v1.compiler.Terms
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.compiler.Types._
import com.zbsnetwork.lang.v1.evaluator.FunctionIds._
@@ -9,50 +11,86 @@ import org.scalacheck._
trait TypedScriptGen {
+ def exprGen = BOOLEANgen(100)
+ private def letGen =
+ for {
+ name <- Gen.alphaStr
+ expr <- exprGen
+ } yield Terms.LET(name, expr)
+
+ private def funcGen =
+ for {
+ name <- Gen.alphaStr
+ arg0 <- Gen.alphaStr
+ args <- Gen.listOf(Gen.alphaStr)
+ allArgs = arg0 +: args
+ returned <- Gen.oneOf(allArgs)
+ } yield Terms.FUNC(name, allArgs, Terms.REF(returned))
+
+ private def callableGen =
+ for {
+ binding <- Gen.alphaStr
+ fnc <- funcGen
+ } yield CallableFunction(CallableAnnotation(binding), fnc)
+
+ private def verifierGen =
+ for {
+ binding <- Gen.alphaStr
+ name <- Gen.alphaStr
+ expr <- exprGen
+ } yield VerifierFunction(VerifierAnnotation(binding), Terms.FUNC(name, List.empty, expr))
+
+ def contractGen =
+ for {
+ nLets <- Gen.chooseNum(0, 10)
+ nFuncs <- Gen.chooseNum(0, 10)
+ nCallables <- Gen.chooseNum(0, 10)
+ lets <- Gen.listOfN(nLets, letGen)
+ funcs <- Gen.listOfN(nFuncs, funcGen)
+ callables <- Gen.listOfN(nCallables, callableGen)
+ verifier <- Gen.option(verifierGen)
+ c = Contract(lets ++ funcs, callables, verifier)
+ if ContractSerDe.serialize(c).size < Short.MaxValue - 3 - 4
+ } yield c
+
def BOOLEANgen(gas: Int): Gen[EXPR] =
if (gas > 0) Gen.oneOf(CONST_BOOLEANgen, BLOCK_BOOLEANgen(gas - 1), IF_BOOLEANgen(gas - 1), FUNCTION_CALLgen(BOOLEAN))
else Gen.const(TRUE)
- def CONST_BOOLEANgen: Gen[EXPR] = Gen.oneOf(FALSE, TRUE)
+ private def CONST_BOOLEANgen: Gen[EXPR] = Gen.oneOf(FALSE, TRUE)
- def BLOCK_BOOLEANgen(gas: Int): Gen[EXPR] =
+ private def BLOCK_BOOLEANgen(gas: Int): Gen[EXPR] =
for {
let <- LETgen((gas - 3) / 3)
body <- Gen.oneOf(BOOLEANgen((gas - 3) / 3), BLOCK_BOOLEANgen((gas - 3) / 3))
} yield BLOCK(let, body)
- def IF_BOOLEANgen(gas: Int): Gen[EXPR] =
+ private def IF_BOOLEANgen(gas: Int): Gen[EXPR] =
for {
cnd <- BOOLEANgen((gas - 3) / 3)
t <- BOOLEANgen((gas - 3) / 3)
f <- BOOLEANgen((gas - 3) / 3)
} yield IF(cnd, t, f)
- def LONGgen(gas: Int): Gen[EXPR] =
+ private def LONGgen(gas: Int): Gen[EXPR] =
if (gas > 0) Gen.oneOf(CONST_LONGgen, BLOCK_LONGgen(gas - 1), IF_LONGgen(gas - 1), FUNCTION_CALLgen(LONG)) else CONST_LONGgen
- def CONST_LONGgen: Gen[EXPR] = Gen.choose(Long.MinValue, Long.MaxValue).map(CONST_LONG)
+ private def CONST_LONGgen: Gen[EXPR] = Gen.choose(Long.MinValue, Long.MaxValue).map(CONST_LONG)
- def BLOCK_LONGgen(gas: Int): Gen[EXPR] =
+ private def BLOCK_LONGgen(gas: Int): Gen[EXPR] =
for {
let <- LETgen((gas - 3) / 3)
body <- Gen.oneOf(LONGgen((gas - 3) / 3), BLOCK_LONGgen((gas - 3) / 3))
} yield LET_BLOCK(let, body)
- def IF_LONGgen(gas: Int): Gen[EXPR] =
+ private def IF_LONGgen(gas: Int): Gen[EXPR] =
for {
cnd <- BOOLEANgen((gas - 3) / 3)
t <- LONGgen((gas - 3) / 3)
f <- LONGgen((gas - 3) / 3)
} yield IF(cnd, t, f)
- def STRINGgen: Gen[EXPR] = Gen.identifier.map(CONST_STRING)
-
- def BYTESTRgen: Gen[EXPR] = Gen.identifier.map(x => CONST_BYTESTR(ByteStr(x.getBytes)))
-
- def REFgen(tpe: TYPE): Gen[EXPR] = Gen.identifier.map(REF)
-
- def FUNCTION_CALLgen(resultType: TYPE): Gen[EXPR] =
+ private def FUNCTION_CALLgen(resultType: TYPE): Gen[EXPR] =
Gen.const(
FUNCTION_CALL(
function = FunctionHeader.Native(SUM_LONG),
@@ -60,7 +98,7 @@ trait TypedScriptGen {
)
)
- def LETgen(gas: Int): Gen[LET] =
+ private def LETgen(gas: Int): Gen[LET] =
for {
name <- Gen.identifier
value <- BOOLEANgen((gas - 3) / 3)
diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala
index 47c3dba..099e18b 100644
--- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala
+++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala
@@ -14,4 +14,5 @@ case class Ord(id: ByteStr,
expiration: Long,
matcherFee: Long,
bodyBytes: ByteStr,
- proofs: IndexedSeq[ByteStr])
+ proofs: IndexedSeq[ByteStr],
+ matcherFeeAssetId: Option[ByteStr] = None)
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 39e43bd..850eb1f 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -3,15 +3,19 @@ import sbt._
object Dependencies {
- def akkaModule(module: String) = "com.typesafe.akka" %% s"akka-$module" % "2.5.16"
+ def akkaModule(module: String) = "com.typesafe.akka" %% s"akka-$module" % "2.5.20"
def swaggerModule(module: String) = ("io.swagger.core.v3" % s"swagger-$module" % "2.0.5").exclude("com.google.guava", "guava")
- def akkaHttpModule(module: String) = "com.typesafe.akka" %% module % "10.1.4"
+ def akkaHttpModule(module: String = "") = "com.typesafe.akka" %% s"akka-http${if (module.isEmpty) "" else s"-$module"}" % "10.1.7"
def nettyModule(module: String) = "io.netty" % s"netty-$module" % "4.1.24.Final"
def kamonModule(module: String, v: String) = "io.kamon" %% s"kamon-$module" % v
+
+ val AkkaActor = akkaModule("actor")
+ val AkkaStream = akkaModule("stream")
+ val AkkaHTTP = akkaHttpModule()
val asyncHttpClient = "org.asynchttpclient" % "async-http-client" % "2.4.7"
@@ -27,7 +31,7 @@ object Dependencies {
"org.mockito" % "mockito-all" % "1.10.19",
"org.scalamock" %% "scalamock-scalatest-support" % "3.6.0",
("org.iq80.leveldb" % "leveldb" % "0.9").exclude("com.google.guava", "guava"),
- akkaHttpModule("akka-http-testkit")
+ akkaHttpModule("testkit")
)
lazy val itKit = scalatest ++ Seq(
@@ -43,7 +47,7 @@ object Dependencies {
"com.typesafe.play" %% "play-json" % "2.6.10"
)
- lazy val akka = Seq("actor", "slf4j").map(akkaModule)
+ lazy val akka = Seq(AkkaActor, akkaModule("slf4j"))
lazy val db = Seq(
"org.ethereum" % "leveldbjni-all" % "1.18.3"
@@ -61,21 +65,21 @@ object Dependencies {
"com.github.swagger-akka-http" %% "swagger-akka-http" % "1.0.0",
"com.fasterxml.jackson.core" % "jackson-databind" % "2.9.6",
"com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.6",
- akkaHttpModule("akka-http")
+ AkkaHTTP
)
lazy val matcher = Seq(
akkaModule("persistence"),
akkaModule("persistence-tck") % "test",
"com.github.dnvriend" %% "akka-persistence-inmemory" % "2.5.15.1" % "test",
- "com.typesafe.akka" %% "akka-stream-kafka" % "1.0-RC1",
+ "com.typesafe.akka" %% "akka-stream-kafka" % "1.0-RC2",
"org.ethereum" % "leveldbjni-all" % "1.18.3"
)
lazy val metrics = Seq(
kamonModule("core", "1.1.3"),
kamonModule("system-metrics", "1.0.0").exclude("io.kamon", "kamon-core_2.12"),
- kamonModule("akka-2.5", "1.1.1").exclude("io.kamon", "kamon-core_2.12"),
+ kamonModule("akka-2.5", "1.1.3").exclude("io.kamon", "kamon-core_2.12"),
kamonModule("influxdb", "1.0.2"),
"org.influxdb" % "influxdb-java" % "2.11"
).map(_.exclude("org.asynchttpclient", "async-http-client"))
@@ -110,4 +114,19 @@ object Dependencies {
)
lazy val kindProjector = "org.spire-math" %% "kind-projector" % "0.9.6"
lazy val betterFor = "com.olegpy" %% "better-monadic-for" % "0.3.0-M4"
+
+ lazy val protobuf = Def.setting {
+ val version = scalapb.compiler.Version.scalapbVersion
+ Seq(
+ // "com.google.protobuf" % "protobuf-java" % "3.4.0",
+ "com.thesamet.scalapb" %%% "scalapb-runtime" % version,
+ "com.thesamet.scalapb" %%% "scalapb-runtime" % version % "protobuf",
+ "com.thesamet.scalapb" %% "scalapb-json4s" % "0.7.0"
+ )
+ }
+
+ lazy val grpc = Seq(
+ "io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion,
+ "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion
+ )
}
diff --git a/project/build.properties b/project/build.properties
index 7c58a83..4f2bf11 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1,2 @@
sbt.version=1.2.6
+project.version=0.16.2
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 9e182c4..dc2499b 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -5,6 +5,13 @@ resolvers ++= Seq(
Resolver.sbtPluginRepo("releases")
)
+// Should go before Scala.js
+addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.19")
+addSbtPlugin("org.ensime" % "sbt-ensime" % "2.5.1")
+
+
+libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.8.4"
+
Seq(
"com.eed3si9n" % "sbt-assembly" % "0.14.5",
"com.typesafe.sbt" % "sbt-native-packager" % "1.3.2",
diff --git a/release-notes.md b/release-notes.md
deleted file mode 100644
index 5fbf1d7..0000000
--- a/release-notes.md
+++ /dev/null
@@ -1,67 +0,0 @@
-**0.6.0**
-
-* The DEX's Order Match transaction has been changed. This is the main reason for restarting Testnet. Now, a second asset of transaction's pair is used to set an amount of the transaction.
-* LPOS was implemented. New Leasing and Leasing Cancel transactions were added.
-* New, HOCON based, configuration file. Old configuration file (JSON based) is supported in this release for backward compatibility. Automatic configuration file conversion added to DEB packages.
-
-**0.3.2**
-
-* By default walletDir and dataDir located in $HOME/zbs
-
-**0.3.1**
-
-* HTTP API /scorex removed. Use /node instead.
-
-**0.2.2**
-
-* Switch network by "testnet" in settings. Default value is true."
-* /scorex/* HTTP API deprecated. Use /node/* instead.
-* All logs goes to stdout and stderr. Use "loggingLevel" in config.
-
-**0.2.1**
-
-* peers.dat format changed. Delete old version.
-* Different HTTP status codes in replies in HTTP API were implemented
-* Zbs' Scorex v1.3.2
-
-**0.2.0**
-
-* Peers blacklist ttl configuration via "p2p"/"blacklistResidenceTimeMilliseconds"
-* Upgrade to Zbs' Scorex v1.3.1
-
-**0.2.0-RC7**
-
-* New API /zbs/payment returns senderPublicKey
-* New API /zbs/create-signed-payment
-* /zbs/external-payment deprecated.
- Use new /zbs/broadcast-signed-payment.
-* New API /zbs/payment/signature
-* minimumTxFee verification for API
-
-**0.2.0-RC5**
-
-* /zbs/external-payment returns error for incorrect recipient
-
-**0.2.0-RC4**
-
-* Fixed issue with incorrect Handshake
-* Balance with confirmations is the minimum balance
-* /zbs/external-payment returns error if account balance invalid
-* New API method /consensus/generatingbalance/{address}
-
-**0.2.0-RC3**
-
-* Incompatible with 0.1.3
-* Upgrade to Scorex 1.2.8
-* New Address format
-* New hash chain for Address - Blake2b256, Keccak32
-* New Testnet Genesis
-
-**0.1.3**
-
-* Upgrade to Scorex 1.2.6.
-* New http api method /external-payment for lite client
-
-**0.1.2**
-
-* Upgrade to Scorex 1.2.4. Clean /scorex/zbs/data/ before run.
diff --git a/src/main/protobuf/address.proto b/src/main/protobuf/address.proto
new file mode 100644
index 0000000..98d4d5a
--- /dev/null
+++ b/src/main/protobuf/address.proto
@@ -0,0 +1,15 @@
+// Transactions
+syntax = "proto3";
+option java_package = "com.zbsnetwork.protobuf.account";
+
+message Alias {
+ bytes chainId = 1;
+ string name = 2;
+};
+
+message Recipient {
+ oneof recipient {
+ bytes address = 1;
+ Alias alias = 2;
+ };
+};
\ No newline at end of file
diff --git a/src/main/protobuf/block.proto b/src/main/protobuf/block.proto
new file mode 100644
index 0000000..dabfa16
--- /dev/null
+++ b/src/main/protobuf/block.proto
@@ -0,0 +1,25 @@
+// Transactions
+syntax = "proto3";
+option java_package = "com.zbsnetwork.protobuf.block";
+import "transactions.proto";
+
+message Block {
+ message SignedHeader {
+ Header header = 1;
+ bytes signature = 8;
+ }
+
+ message Header {
+ bytes reference = 1;
+ int64 baseTarget = 2;
+ bytes generationSignature = 3;
+ repeated uint32 featureVotes = 4;
+ int64 timestamp = 5;
+ int32 version = 6;
+ bytes generator = 7;
+ }
+
+ bytes chainId = 1;
+ SignedHeader header = 2;
+ repeated SignedTransaction transactions = 3;
+}
\ No newline at end of file
diff --git a/src/main/protobuf/scripts.proto b/src/main/protobuf/scripts.proto
new file mode 100644
index 0000000..f9b9d30
--- /dev/null
+++ b/src/main/protobuf/scripts.proto
@@ -0,0 +1,7 @@
+// Transactions
+syntax = "proto3";
+option java_package = "com.zbsnetwork.protobuf.transaction.smart.script";
+
+message Script {
+ bytes bytes = 1;
+};
\ No newline at end of file
diff --git a/src/main/protobuf/transactions.proto b/src/main/protobuf/transactions.proto
new file mode 100644
index 0000000..6bd2a31
--- /dev/null
+++ b/src/main/protobuf/transactions.proto
@@ -0,0 +1,180 @@
+// Transactions
+syntax = "proto3";
+option java_package = "com.zbsnetwork.protobuf.transaction";
+import "scripts.proto";
+import "address.proto";
+
+message AssetAmount {
+ bytes assetId = 1;
+ int64 amount = 2;
+}
+
+message Amount {
+ oneof amount {
+ int64 zbsAmount = 1;
+ AssetAmount assetAmount = 2;
+ }
+}
+
+message SignedTransaction {
+ Transaction transaction = 1;
+ repeated bytes proofs = 2;
+}
+
+message Transaction {
+ bytes chainId = 1;
+ bytes senderPublicKey = 2;
+ Amount fee = 3;
+ int64 timestamp = 4;
+ int32 version = 5;
+
+ oneof data {
+ GenesisTransactionData genesis = 101;
+ PaymentTransactionData payment = 102;
+ IssueTransactionData issue = 103;
+ TransferTransactionData transfer = 104;
+ ReissueTransactionData reissue = 105;
+ BurnTransactionData burn = 106;
+ ExchangeTransactionData exchange = 107;
+ LeaseTransactionData lease = 108;
+ LeaseCancelTransactionData leaseCancel = 109;
+ CreateAliasTransactionData createAlias = 110;
+ MassTransferTransactionData massTransfer = 111;
+ DataTransactionData dataTransaction = 112;
+ SetScriptTransactionData setScript = 113;
+ SponsorFeeTransactionData sponsorFee = 114;
+ SetAssetScriptTransactionData setAssetScript = 115;
+ // TODO: 116 = contract invocation
+ };
+};
+
+message GenesisTransactionData {
+ bytes recipientAddress = 1;
+ int64 amount = 2;
+};
+
+message PaymentTransactionData {
+ bytes address = 1;
+ int64 amount = 2;
+};
+
+message TransferTransactionData {
+ Recipient recipient = 1;
+ Amount amount = 2;
+ bytes attachment = 3;
+};
+
+message CreateAliasTransactionData {
+ string alias = 1;
+};
+
+message DataTransactionData {
+ message DataEntry {
+ string key = 1;
+ oneof value {
+ int64 intValue = 10;
+ bool boolValue = 11;
+ bytes binaryValue = 12;
+ string stringValue = 13;
+ };
+ };
+
+ repeated DataEntry data = 1;
+};
+
+message MassTransferTransactionData {
+ message Transfer {
+ Recipient address = 1;
+ int64 amount = 2;
+ };
+
+ bytes assetId = 1;
+ repeated Transfer transfers = 2;
+ bytes attachment = 3;
+};
+
+message LeaseTransactionData {
+ Recipient recipient = 1;
+ int64 amount = 2;
+};
+
+message LeaseCancelTransactionData {
+ bytes leaseId = 1;
+};
+
+message BurnTransactionData {
+ AssetAmount assetAmount = 1;;
+};
+
+message IssueTransactionData {
+ bytes name = 1;
+ bytes description = 2;
+ int64 amount = 3;
+ int32 decimals = 4;
+ bool reissuable = 5;
+ Script script = 6;
+};
+
+
+message ReissueTransactionData {
+ AssetAmount assetAmount = 1;
+ bool reissuable = 2;
+};
+
+message SetAssetScriptTransactionData {
+ bytes assetId = 1;
+ Script script = 2;
+};
+
+message SetScriptTransactionData {
+ Script script = 2;
+};
+
+message ExchangeTransactionData {
+ message BuySellOrders {
+ Order buyOrder = 1;
+ Order sellOrder = 2;
+ }
+
+ message MakerTakerOrders {
+ Order makerOrder = 1;
+ Order takerOrder = 2;
+ }
+
+ message Order {
+ enum Side {
+ BUY = 0;
+ SELL = 1;
+ };
+
+ message AssetPair {
+ bytes amountAssetId = 1;
+ bytes priceAssetId = 2;
+ };
+
+ bytes senderPublicKey = 1;
+ bytes matcherPublicKey = 2;
+ AssetPair assetPair = 3;
+ Side orderSide = 4;
+ int64 amount = 5;
+ int64 price = 6;
+ int64 timestamp = 7;
+ int64 expiration = 8;
+ Amount matcherFee = 9;
+ int32 version = 10;
+ repeated bytes proofs = 11;
+ };
+
+ oneof orders {
+ BuySellOrders buySellOrders = 1;
+ MakerTakerOrders makerTakerOrders = 2;
+ }
+ int64 amount = 3;
+ int64 price = 4;
+ int64 buyMatcherFee = 5;
+ int64 sellMatcherFee = 6;
+};
+
+message SponsorFeeTransactionData {
+ AssetAmount minFee = 1;
+};
\ No newline at end of file
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
index 7364fde..4463027 100644
--- a/src/main/resources/application.conf
+++ b/src/main/resources/application.conf
@@ -234,15 +234,9 @@ zbs {
# Maximum allowed amount of orders retrieved via REST
rest-order-limit = 100
- # A new order should have timestamp more than (previous_order_timestamp - order-timestamp-drift)
- order-timestamp-drift = 1m
-
# Base assets used as price assets
price-assets: []
- # Maximum difference with Matcher server time
- max-timestamp-diff = 30d
-
# Blacklisted assets id
blacklisted-assets: []
@@ -410,6 +404,15 @@ zbs {
# Max time for buffer. When time is out, the node processes all transactions in batch
max-buffer-time = 100ms
+
+ # Max scheduler parallelism
+ parallelism = 4
+
+ # Max scheduler threads
+ max-threads = 8
+
+ # Max pending queue size
+ max-queue-size = 5000
}
# MicroBlock synchronizer settings
@@ -431,8 +434,6 @@ zbs {
max-size = 100000
# Pool size in bytes
max-bytes-size = 52428800 // 50 MB
- # Utx cleanup task interval
- cleanup-interval = 5m
# Blacklist transactions from these addresses (Base58 strings)
blacklist-sender-addresses = []
# Allow transfer transactions from the blacklisted addresses to these recipients (Base58 strings)
@@ -604,7 +605,9 @@ akka {
group.id = "0"
auto.offset.reset = "earliest"
enable.auto.commit = false
- # max.poll.records = 10 # Should be <= ${zbs.matcher.events-queue.kafka.consumer.buffer-size}
+ session.timeout.ms = 10000
+ max.poll.interval.ms = 11000
+ max.poll.records = 100 # Should be <= ${zbs.matcher.events-queue.kafka.consumer.buffer-size}
}
# Time to wait for pending requests when a partition is closed
@@ -644,6 +647,8 @@ akka {
kafka-clients {
bootstrap.servers = ${akka.kafka.consumer.kafka-clients.bootstrap.servers}
+ acks = all
+
# Buffer messages into a batch for this duration
linger.ms = 5
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 010af84..1f81dd9 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -41,6 +41,7 @@
+
diff --git a/src/main/scala/com/zbsnetwork/Application.scala b/src/main/scala/com/zbsnetwork/Application.scala
index f0cd1e8..c7e9b53 100644
--- a/src/main/scala/com/zbsnetwork/Application.scala
+++ b/src/main/scala/com/zbsnetwork/Application.scala
@@ -29,6 +29,7 @@ import com.zbsnetwork.network.RxExtensionLoader.RxExtensionLoaderShutdownHook
import com.zbsnetwork.network._
import com.zbsnetwork.settings._
import com.zbsnetwork.state.appender.{BlockAppender, ExtensionAppender, MicroblockAppender}
+import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.utils.{NTP, ScorexLogging, SystemInformationReporter, forceStopApplication}
import com.zbsnetwork.utx.{UtxPool, UtxPoolImpl}
import com.zbsnetwork.wallet.Wallet
@@ -58,9 +59,9 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi
private val LocalScoreBroadcastDebounce = 1.second
- private val portfolioChanged = ConcurrentSubject.publish[Address]
+ private val spendableBalanceChanged = ConcurrentSubject.publish[(Address, Option[AssetId])]
- private val blockchainUpdater = StorageFactory(settings, db, time, portfolioChanged)
+ private val blockchainUpdater = StorageFactory(settings, db, time, spendableBalanceChanged)
private lazy val upnp = new UPnP(settings.networkSettings.uPnPSettings) // don't initialize unless enabled
@@ -101,11 +102,11 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi
val establishedConnections = new ConcurrentHashMap[Channel, PeerInfo]
val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
val utxStorage =
- new UtxPoolImpl(time, blockchainUpdater, portfolioChanged, settings.blockchainSettings.functionalitySettings, settings.utxSettings)
+ new UtxPoolImpl(time, blockchainUpdater, spendableBalanceChanged, settings.blockchainSettings.functionalitySettings, settings.utxSettings)
maybeUtx = Some(utxStorage)
matcher = if (settings.matcherSettings.enable) {
- Matcher(actorSystem, time, wallet, utxStorage, allChannels, blockchainUpdater, portfolioChanged, settings)
+ Matcher(actorSystem, time, wallet, utxStorage, allChannels, blockchainUpdater, spendableBalanceChanged, settings)
} else None
val knownInvalidBlocks = new InvalidBlockStorageImpl(settings.synchronizationSettings.invalidBlocksStorage)
@@ -179,11 +180,16 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi
rxExtensionLoaderShutdown = Some(sh)
UtxPoolSynchronizer.start(utxStorage, settings.synchronizationSettings.utxSynchronizerSettings, allChannels, transactions)
- val microBlockSink = microblockDatas.mapTask(scala.Function.tupled(processMicroBlock))
- val blockSink = newBlocks.mapTask(scala.Function.tupled(processBlock))
+
+ val microBlockSink = microblockDatas
+ .mapTask(scala.Function.tupled(processMicroBlock))
+
+ val blockSink = newBlocks
+ .mapTask(scala.Function.tupled(processBlock))
Observable.merge(microBlockSink, blockSink).subscribe()
miner.scheduleMining()
+ utxStorage.cleanup.runCleanupOn(blockSink)
for (addr <- settings.networkSettings.declaredAddress if settings.networkSettings.uPnPSettings.enable) {
upnp.addPort(addr.getPort)
@@ -279,7 +285,7 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi
if (!shutdownInProgress) {
shutdownInProgress = true
- portfolioChanged.onComplete()
+ spendableBalanceChanged.onComplete()
utx.close()
shutdownAndWait(historyRepliesScheduler, "HistoryReplier", 5.minutes)
diff --git a/src/main/scala/com/zbsnetwork/Importer.scala b/src/main/scala/com/zbsnetwork/Importer.scala
index b942748..6cdeaf0 100644
--- a/src/main/scala/com/zbsnetwork/Importer.scala
+++ b/src/main/scala/com/zbsnetwork/Importer.scala
@@ -12,8 +12,9 @@ import com.zbsnetwork.db.openDB
import com.zbsnetwork.history.StorageFactory
import com.zbsnetwork.mining.MultiDimensionalMiningConstraint
import com.zbsnetwork.settings.{ZbsSettings, loadConfig}
+import com.zbsnetwork.state.Portfolio
import com.zbsnetwork.state.appender.BlockAppender
-import com.zbsnetwork.transaction.Transaction
+import com.zbsnetwork.transaction.{AssetId, Transaction}
import com.zbsnetwork.utils._
import com.zbsnetwork.utx.UtxPool
import monix.execution.{Scheduler, UncaughtExceptionReporter}
@@ -49,15 +50,15 @@ object Importer extends ScorexLogging {
implicit val scheduler: Scheduler = Scheduler.singleThread("appender")
val utxPoolStub = new UtxPool {
- override def putIfNew(tx: Transaction) = ???
- override def removeAll(txs: Traversable[Transaction]): Unit = {}
- override def accountPortfolio(addr: Address) = ???
- override def portfolio(addr: Address) = ???
- override def all = ???
- override def size = ???
- override def transactionById(transactionId: ByteStr) = ???
- override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ???
- override def close(): Unit = {}
+ override def putIfNew(tx: Transaction) = ???
+ override def removeAll(txs: Traversable[Transaction]): Unit = {}
+ override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long = ???
+ override def pessimisticPortfolio(addr: Address): Portfolio = ???
+ override def all = ???
+ override def size = ???
+ override def transactionById(transactionId: ByteStr) = ???
+ override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ???
+ override def close(): Unit = {}
}
val time = new NTP(settings.ntpServer)
diff --git a/src/main/scala/com/zbsnetwork/account/Address.scala b/src/main/scala/com/zbsnetwork/account/Address.scala
index 97bb9ae..f5125d9 100644
--- a/src/main/scala/com/zbsnetwork/account/Address.scala
+++ b/src/main/scala/com/zbsnetwork/account/Address.scala
@@ -30,11 +30,13 @@ object Address extends ScorexLogging {
private class AddressImpl(val bytes: ByteStr) extends Address
+ private[this] def createUnsafe(address: ByteStr): Address = new AddressImpl(address)
+
def fromPublicKey(publicKey: Array[Byte], chainId: Byte = scheme.chainId): Address = {
val publicKeyHash = crypto.secureHash(publicKey)
val withoutChecksum = ByteBuffer.allocate(1 + 1 + HashLength).put(AddressVersion).put(chainId).put(publicKeyHash, 0, HashLength).array()
val bytes = ByteBuffer.allocate(AddressLength).put(withoutChecksum).put(crypto.secureHash(withoutChecksum), 0, ChecksumLength).array()
- new AddressImpl(ByteStr(bytes))
+ createUnsafe(bytes)
}
def fromBytes(addressBytes: Array[Byte], chainId: Byte = scheme.chainId): Either[InvalidAddress, Address] = {
@@ -49,7 +51,7 @@ object Address extends ScorexLogging {
checkSum = addressBytes.takeRight(ChecksumLength)
checkSumGenerated = calcCheckSum(addressBytes.dropRight(ChecksumLength))
_ <- Either.cond(checkSum.sameElements(checkSumGenerated), (), s"Bad address checksum")
- } yield new AddressImpl(ByteStr(addressBytes))).left.map(InvalidAddress)
+ } yield createUnsafe(addressBytes)).left.map(InvalidAddress)
}
def fromString(addressStr: String): Either[ValidationError, Address] = {
diff --git a/src/main/scala/com/zbsnetwork/account/Alias.scala b/src/main/scala/com/zbsnetwork/account/Alias.scala
index 2ec2ba1..808f9fb 100644
--- a/src/main/scala/com/zbsnetwork/account/Alias.scala
+++ b/src/main/scala/com/zbsnetwork/account/Alias.scala
@@ -24,13 +24,12 @@ object Alias {
private val AliasPatternInfo = "Alias string pattern is 'alias::"
- private def currentChainId: Byte = AddressScheme.current.chainId
+ private[this] def currentChainId: Byte = AddressScheme.current.chainId
- private def validAliasChar(c: Char): Boolean =
+ private[this] def validAliasChar(c: Char): Boolean =
('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || c == '_' || c == '@' || c == '-' || c == '.'
- private def buildAlias(chainId: Byte, name: String): Either[ValidationError, Alias] = {
-
+ private[zbsnetwork] def buildAlias(chainId: Byte, name: String): Either[ValidationError, Alias] = {
case class AliasImpl(chainId: Byte, name: String) extends Alias
if (name.length < MinLength || MaxLength < name.length)
diff --git a/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala b/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala
index 5bad24c..ef105b7 100644
--- a/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala
+++ b/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala
@@ -1,9 +1,9 @@
package com.zbsnetwork.account
import com.zbsnetwork.common.utils.Base58
-import com.zbsnetwork.utils.base58Length
-import com.zbsnetwork.transaction.ValidationError.InvalidAddress
import com.zbsnetwork.crypto._
+import com.zbsnetwork.transaction.ValidationError.InvalidAddress
+import com.zbsnetwork.utils.base58Length
trait PublicKeyAccount {
def publicKey: Array[Byte]
@@ -19,6 +19,7 @@ trait PublicKeyAccount {
}
object PublicKeyAccount {
+ val empty = apply(Array.emptyByteArray)
val KeyStringLength: Int = base58Length(KeyLength)
diff --git a/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala
index dc0af68..417579d 100644
--- a/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala
@@ -20,6 +20,7 @@ import com.zbsnetwork.wallet.Wallet
import io.netty.channel.group.ChannelGroup
import io.swagger.annotations._
import javax.ws.rs.Path
+
import play.api.libs.json._
import scala.util.{Failure, Success, Try}
@@ -349,13 +350,13 @@ case class AddressApiRoute(settings: RestAPISettings,
Balance(
acc.address,
0,
- blockchain.portfolio(acc).balance
+ blockchain.balance(acc)
)))
.getOrElse(InvalidAddress)
}
private def balancesDetailsJson(account: Address): BalanceDetails = {
- val portfolio = blockchain.portfolio(account)
+ val portfolio = blockchain.zbsPortfolio(account)
BalanceDetails(
account.address,
portfolio.balance,
diff --git a/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala b/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala
index fef18a7..54dd7db 100644
--- a/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala
@@ -7,7 +7,7 @@ import akka.http.scaladsl.model.{HttpRequest, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.RouteResult.Complete
import akka.http.scaladsl.server.directives.{DebuggingDirectives, LoggingMagnet}
-import akka.http.scaladsl.server.{Directive0, Route, RouteResult}
+import akka.http.scaladsl.server.{Route, RouteResult}
import akka.stream.ActorMaterializer
import com.zbsnetwork.api.http.swagger.SwaggerDocService
import com.zbsnetwork.settings.RestAPISettings
@@ -16,45 +16,39 @@ import com.zbsnetwork.utils.ScorexLogging
case class CompositeHttpService(apiTypes: Set[Class[_]], routes: Seq[ApiRoute], settings: RestAPISettings)(implicit system: ActorSystem)
extends ScorexLogging {
- val swaggerService = new SwaggerDocService(system, ActorMaterializer()(system), apiTypes, settings)
-
- def withCors: Directive0 =
- if (settings.cors)
- respondWithHeader(`Access-Control-Allow-Origin`.*)
- else pass
-
- private val headers: scala.collection.immutable.Seq[String] = scala.collection.immutable.Seq("Authorization",
- "Content-Type",
- "X-Requested-With",
- "Timestamp",
- "x-api-key",
- "Signature") ++
- (if (settings.apiKeyDifferentHost) Seq("api_key", "X-API-Key") else Seq.empty[String])
-
- val compositeRoute: Route =
- withCors(routes.map(_.route).reduce(_ ~ _)) ~
- swaggerService.routes ~
- (pathEndOrSingleSlash | path("swagger")) {
- redirect("/api-docs/index.html", StatusCodes.PermanentRedirect)
- } ~
- pathPrefix("api-docs") {
- pathEndOrSingleSlash {
- redirect("/api-docs/index.html", StatusCodes.PermanentRedirect)
- } ~
- getFromResourceDirectory("swagger-ui")
- } ~ options {
- respondWithDefaultHeaders(`Access-Control-Allow-Credentials`(true),
- `Access-Control-Allow-Headers`(headers),
- `Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE))(withCors(complete(StatusCodes.OK)))
- } ~ complete(StatusCodes.NotFound)
-
- def logRequestResponse(req: HttpRequest)(res: RouteResult): Unit = res match {
+ private val swaggerService = new SwaggerDocService(system, ActorMaterializer()(system), apiTypes, settings)
+ private val redirectToSwagger = redirect("/api-docs/index.html", StatusCodes.PermanentRedirect)
+ private val swaggerRoute: Route = swaggerService.routes ~
+ (pathEndOrSingleSlash | path("swagger"))(redirectToSwagger) ~
+ pathPrefix("api-docs") {
+ pathEndOrSingleSlash(redirectToSwagger) ~ getFromResourceDirectory("swagger-ui")
+ }
+
+ val compositeRoute: Route = extendRoute(routes.map(_.route).reduce(_ ~ _)) ~ swaggerRoute ~ complete(StatusCodes.NotFound)
+ val loggingCompositeRoute: Route = DebuggingDirectives.logRequestResult(LoggingMagnet(_ => logRequestResponse))(compositeRoute)
+
+ private def logRequestResponse(req: HttpRequest)(res: RouteResult): Unit = res match {
case Complete(resp) =>
val msg = s"HTTP ${resp.status.value} from ${req.method.value} ${req.uri}"
- if (resp.status == StatusCodes.OK) log.debug(msg) else log.warn(msg)
+ if (resp.status == StatusCodes.OK) log.info(msg) else log.warn(msg)
case _ =>
}
- val loggingCompositeRoute: Route =
- DebuggingDirectives.logRequestResult(LoggingMagnet(_ => logRequestResponse))(compositeRoute)
+ private val corsAllowedHeaders = (if (settings.apiKeyDifferentHost) List("api_key", "X-API-Key") else List.empty[String]) ++
+ Seq("Authorization", "Content-Type", "X-Requested-With", "Timestamp", "Signature")
+
+ private def corsAllowAll = if (settings.cors) respondWithHeader(`Access-Control-Allow-Origin`.*) else pass
+
+ private def extendRoute(base: Route): Route =
+ if (settings.cors) { ctx =>
+ val extendedRoute = corsAllowAll(base) ~ options {
+ respondWithDefaultHeaders(
+ `Access-Control-Allow-Credentials`(true),
+ `Access-Control-Allow-Headers`(corsAllowedHeaders),
+ `Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE)
+ )(corsAllowAll(complete(StatusCodes.OK)))
+ }
+
+ extendedRoute(ctx)
+ } else base
}
diff --git a/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala b/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala
index 6e7dae4..7513ada 100644
--- a/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala
@@ -1,35 +1,56 @@
package com.zbsnetwork.api.http
import cats.implicits._
-import com.zbsnetwork.account.{Address, PublicKeyAccount}
import com.zbsnetwork.lang.v1.FunctionHeader
import com.zbsnetwork.lang.v1.compiler.Terms._
-import com.zbsnetwork.state.{BinaryDataEntry, BooleanDataEntry, DataEntry, IntegerDataEntry, StringDataEntry}
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.account.{Address, PublicKeyAccount}
import com.zbsnetwork.transaction.smart.ContractInvocationTransaction
import com.zbsnetwork.transaction.{Proofs, ValidationError}
import io.swagger.annotations.{ApiModel, ApiModelProperty}
-import play.api.libs.json.Json
+import play.api.libs.json._
import scala.annotation.meta.field
object ContractInvocationRequest {
- case class FunctionCallPart(function: String, args: List[DataEntry[_]])
- implicit val functionCallReads = Json.reads[FunctionCallPart]
+ case class FunctionCallPart(function: String, args: List[EVALUATED])
+ implicit val EvaluatedReads = new Reads[EVALUATED] {
+ def reads(jv: JsValue): JsResult[EVALUATED] = {
+ jv \ "type" match {
+ case JsDefined(JsString("integer")) =>
+ jv \ "value" match {
+ case JsDefined(JsNumber(n)) => JsSuccess(CONST_LONG(n.toLong))
+ case _ => JsError("value is missing or not an integer")
+ }
+ case JsDefined(JsString("boolean")) =>
+ jv \ "value" match {
+ case JsDefined(JsBoolean(n)) => JsSuccess(CONST_BOOLEAN(n))
+ case _ => JsError("value is missing or not an boolean")
+ }
+ case JsDefined(JsString("string")) =>
+ jv \ "value" match {
+ case JsDefined(JsString(n)) => JsSuccess(CONST_STRING(n))
+ case _ => JsError("value is missing or not an string")
+ }
+ case JsDefined(JsString("binary")) =>
+ jv \ "value" match {
+ case JsDefined(JsString(n)) =>
+ ByteStr.decodeBase64(n).fold(ex => JsError(ex.getMessage), bstr => JsSuccess(CONST_BYTESTR(bstr)))
+ case _ => JsError("value is missing or not an base64 encoded string")
+ }
+ case _ => JsError("type is missing")
+ }
+ }
+ }
+
+ implicit val functionCallReads = Json.reads[FunctionCallPart]
implicit val unsignedContractInvocationRequestReads = Json.reads[ContractInvocationRequest]
implicit val signedContractInvocationRequestReads = Json.reads[SignedContractInvocationRequest]
def buildFunctionCall(fc: FunctionCallPart): FUNCTION_CALL =
- FUNCTION_CALL(
- FunctionHeader.User(fc.function),
- fc.args.map {
- case BooleanDataEntry(_, b) => CONST_BOOLEAN(b)
- case StringDataEntry(_, b) => CONST_STRING(b)
- case IntegerDataEntry(_, b) => CONST_LONG(b)
- case BinaryDataEntry(_, b) => CONST_BYTESTR(b)
- }
- )
+ FUNCTION_CALL(FunctionHeader.User(fc.function), fc.args)
}
case class ContractInvocationRequest(
diff --git a/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala
index 7cc14ef..5d5a6cc 100644
--- a/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala
@@ -3,7 +3,7 @@ package com.zbsnetwork.api.http
import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Route
-import com.zbsnetwork.account.{Address, AddressOrAlias}
+import com.zbsnetwork.account.Address
import com.zbsnetwork.api.http.DataRequest._
import com.zbsnetwork.api.http.alias.{CreateAliasV1Request, CreateAliasV2Request}
import com.zbsnetwork.api.http.assets.SponsorFeeRequest._
@@ -28,8 +28,6 @@ import io.swagger.annotations._
import javax.ws.rs.Path
import play.api.libs.json._
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
import scala.util.Success
@Path("/transactions")
@@ -278,60 +276,43 @@ case class TransactionsApiRoute(settings: RestAPISettings,
}
}
- /**
- * Produces compact representation for large transactions by stripping unnecessary data.
- * Currently implemented for MassTransfer transaction only.
- */
- private def txToCompactJson(address: Address, addresses: Set[AddressOrAlias], tx: Transaction): JsObject = {
- import com.zbsnetwork.transaction.transfer._
- tx match {
- case mtt: MassTransferTransaction if mtt.sender.toAddress != address =>
- mtt.compactJson(addresses)
- case _ => txToExtendedJson(tx)
- }
- }
+ def transactionsByAddress(addressParam: String, limitParam: Int, maybeAfterParam: Option[String]): Either[ApiError, JsArray] = {
+ def createTransactionsJsonArray(address: Address, limit: Int, fromId: Option[ByteStr]): Either[String, JsArray] = {
+ lazy val addressesCached = concurrent.blocking((blockchain.aliasesOfAddress(address) :+ address).toSet)
- def getResponse(address: Address, limit: Int, fromId: Option[ByteStr]): Either[String, JsArray] = {
- lazy val aoa = blockchain.aliasesOfAddress(address) :+ address
-
- val txs =
- blockchain
- .addressTransactions(address, Set.empty, limit, fromId)
-
- val json =
- txs.map { txSeq =>
- txSeq.map { htx =>
- txToCompactJson(address, aoa.toSet, htx._2) + ("height" -> JsNumber(htx._1))
+ /**
+ * Produces compact representation for large transactions by stripping unnecessary data.
+ * Currently implemented for MassTransfer transaction only.
+ */
+ def txToCompactJson(address: Address, tx: Transaction): JsObject = {
+ import com.zbsnetwork.transaction.transfer._
+ tx match {
+ case mtt: MassTransferTransaction if mtt.sender.toAddress != address => mtt.compactJson(addressesCached)
+ case _ => txToExtendedJson(tx)
}
}
- json.map(txs => Json.arr(JsArray(txs)))
- }
-
- def transactionsByAddress(addressParam: String, limitParam: Int, maybeAfterParam: Option[String]): Future[ToResponseMarshallable] =
- Future {
- val result = for {
- address <- Address.fromString(addressParam).left.map(ApiError.fromValidationError)
- limit <- Either.cond(limitParam < settings.transactionByAddressLimit, limitParam, TooBigArrayAllocation)
- maybeAfter <- maybeAfterParam match {
- case Some(v) =>
- ByteStr
- .decodeBase58(v)
- .fold(
- _ => Left(CustomValidationError(s"Unable to decode transaction id $v")),
- id => Right(Some(id))
- )
- case None => Right(None)
- }
- result <- getResponse(address, limit, maybeAfter).fold(
- err => Left(CustomValidationError(err)),
- arr => Right(arr)
- )
- } yield result
+ val txs = concurrent.blocking(blockchain.addressTransactions(address, Set.empty, limit, fromId))
+ txs.map(txs => Json.arr(JsArray(txs.map { case (height, tx) => txToCompactJson(address, tx) + ("height" -> JsNumber(height)) })))
+ }
- result match {
- case Right(arr) => arr: ToResponseMarshallable
- case Left(err) => err: ToResponseMarshallable
+ for {
+ address <- Address.fromString(addressParam).left.map(ApiError.fromValidationError)
+ limit <- Either.cond(limitParam <= settings.transactionByAddressLimit, limitParam, TooBigArrayAllocation)
+ maybeAfter <- maybeAfterParam match {
+ case Some(v) =>
+ ByteStr
+ .decodeBase58(v)
+ .fold(
+ _ => Left(CustomValidationError(s"Unable to decode transaction id $v")),
+ id => Right(Some(id))
+ )
+ case None => Right(None)
}
- }
+ result <- createTransactionsJsonArray(address, limit, maybeAfter).fold(
+ err => Left(CustomValidationError(err)),
+ arr => Right(arr)
+ )
+ } yield result
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala
index ea49cf0..0cf096b 100644
--- a/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala
@@ -26,7 +26,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A
}
override val route: Route = pathPrefix("utils") {
- decompile ~ compile ~ compileContract ~ estimate ~ time ~ seedRoute ~ length ~ hashFast ~ hashSecure ~ sign ~ transactionSerialize
+ decompile ~ compile ~ compileCode ~ estimate ~ time ~ seedRoute ~ length ~ hashFast ~ hashSecure ~ sign ~ transactionSerialize
}
@Path("/script/decompile")
@@ -61,6 +61,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A
}
}
+ @Deprecated
@Path("/script/compile")
@ApiOperation(value = "Compile", notes = "Compiles string code to base64 script representation", httpMethod = "POST")
@ApiImplicitParams(
@@ -97,8 +98,8 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A
}
}
- @Path("/script/compileContract")
- @ApiOperation(value = "Compile Contract", notes = "Compiles string code to base64 contract representation", httpMethod = "POST")
+ @Path("/script/compileCode")
+ @ApiOperation(value = "Compile script", notes = "Compiles string code to base64 script representation", httpMethod = "POST")
@ApiImplicitParams(
Array(
new ApiImplicitParam(
@@ -106,7 +107,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A
required = true,
dataType = "string",
paramType = "body",
- value = "Contract code",
+ value = "Script code",
example = "true"
)
))
@@ -114,17 +115,17 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A
Array(
new ApiResponse(code = 200, message = "base64 or error")
))
- def compileContract: Route = path("script" / "compileContract") {
+ def compileCode: Route = path("script" / "compileCode") {
(post & entity(as[String])) { code =>
complete(
ScriptCompiler
- .contract(code)
+ .compile(code)
.fold(
e => ScriptCompilerError(e), {
- case (contract) =>
+ case (script, complexity) =>
Json.obj(
- "script" -> contract.bytes().base64,
- "complexity" -> 0,
+ "script" -> script.bytes().base64,
+ "complexity" -> complexity,
"extraFee" -> CommonValidation.ScriptExtraFee
)
}
diff --git a/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala
index 9941501..b8c3c84 100644
--- a/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala
@@ -206,9 +206,8 @@ case class AssetsApiRoute(settings: RestAPISettings, wallet: Wallet, utx: UtxPoo
(for {
acc <- Address.fromString(address)
} yield
- Json.obj("address" -> acc.address,
- "assetId" -> assetIdStr,
- "balance" -> JsNumber(BigDecimal(blockchain.portfolio(acc).assets.getOrElse(assetId, 0L))))).left.map(ApiError.fromValidationError)
+ Json.obj("address" -> acc.address, "assetId" -> assetIdStr, "balance" -> JsNumber(BigDecimal(blockchain.balance(acc, Some(assetId)))))).left
+ .map(ApiError.fromValidationError)
case _ => Left(InvalidAddress)
}
}
@@ -226,7 +225,7 @@ case class AssetsApiRoute(settings: RestAPISettings, wallet: Wallet, utx: UtxPoo
assetInfo <- blockchain.assetDescription(assetId)
(_, (issueTransaction: IssueTransaction)) <- blockchain.transactionInfo(assetId)
sponsorBalance = if (assetInfo.sponsorship != 0) {
- Some(blockchain.portfolio(issueTransaction.sender).spendableBalance)
+ Some(blockchain.zbsPortfolio(issueTransaction.sender).spendableBalance)
} else {
None
}
diff --git a/src/main/scala/com/zbsnetwork/block/Block.scala b/src/main/scala/com/zbsnetwork/block/Block.scala
index 72650f8..0ba77d6 100644
--- a/src/main/scala/com/zbsnetwork/block/Block.scala
+++ b/src/main/scala/com/zbsnetwork/block/Block.scala
@@ -113,13 +113,13 @@ object BlockHeader extends ScorexLogging {
}
-case class Block private (override val timestamp: Long,
- override val version: Byte,
- override val reference: ByteStr,
- override val signerData: SignerData,
- override val consensusData: NxtLikeConsensusBlockData,
- transactionData: Seq[Transaction],
- override val featureVotes: Set[Short])
+case class Block private[block] (override val timestamp: Long,
+ override val version: Byte,
+ override val reference: ByteStr,
+ override val signerData: SignerData,
+ override val consensusData: NxtLikeConsensusBlockData,
+ transactionData: Seq[Transaction],
+ override val featureVotes: Set[Short])
extends BlockHeader(timestamp, version, reference, signerData, consensusData, transactionData.length, featureVotes)
with Signed {
@@ -173,7 +173,7 @@ case class Block private (override val timestamp: Long,
val prevBlockFeePart: Coeval[Portfolio] =
Coeval.evalOnce(Monoid[Portfolio].combineAll(transactionData.map(tx => tx.feeDiff().minus(tx.feeDiff().multiply(CurrentBlockFeePart)))))
- protected val signatureValid: Coeval[Boolean] = Coeval.evalOnce {
+ override val signatureValid: Coeval[Boolean] = Coeval.evalOnce {
import signerData.generator.publicKey
!crypto.isWeakPublicKey(publicKey) && crypto.verify(signerData.signature.arr, bytesWithoutSignature(), publicKey)
}
diff --git a/src/main/scala/com/zbsnetwork/database/Caches.scala b/src/main/scala/com/zbsnetwork/database/Caches.scala
index f18be64..9f85eb7 100644
--- a/src/main/scala/com/zbsnetwork/database/Caches.scala
+++ b/src/main/scala/com/zbsnetwork/database/Caches.scala
@@ -18,7 +18,7 @@ import scala.collection.JavaConverters._
import scala.concurrent.duration._
import scala.reflect.ClassTag
-abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain with ScorexLogging {
+abstract class Caches(spendableBalanceChanged: Observer[(Address, Option[AssetId])]) extends Blockchain with ScorexLogging {
import Caches._
protected def maxCacheSize: Int
@@ -130,16 +130,17 @@ abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain wi
protected def discardLeaseBalance(address: Address): Unit = leaseBalanceCache.invalidate(address)
override def leaseBalance(address: Address): LeaseBalance = leaseBalanceCache.get(address)
- private val portfolioCache: LoadingCache[Address, Portfolio] = observedCache(maxCacheSize / 4, portfolioChanged, loadPortfolio)
+ private val portfolioCache: LoadingCache[Address, Portfolio] = cache(maxCacheSize / 4, loadPortfolio)
protected def loadPortfolio(address: Address): Portfolio
protected def discardPortfolio(address: Address): Unit = portfolioCache.invalidate(address)
override def portfolio(a: Address): Portfolio = {
portfolioCache.get(a)
}
- private val balancesCache: LoadingCache[(Address, Option[AssetId]), java.lang.Long] = cache(maxCacheSize * 16, loadBalance)
- protected def discardBalance(key: (Address, Option[AssetId])) = balancesCache.invalidate(key)
- override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = balancesCache.get(address -> mayBeAssetId)
+ private val balancesCache: LoadingCache[(Address, Option[AssetId]), java.lang.Long] =
+ observedCache(maxCacheSize * 16, spendableBalanceChanged, loadBalance)
+ protected def discardBalance(key: (Address, Option[AssetId])) = balancesCache.invalidate(key)
+ override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = balancesCache.get(address -> mayBeAssetId)
protected def loadBalance(req: (Address, Option[AssetId])): Long
private val assetDescriptionCache: LoadingCache[AssetId, Option[AssetDescription]] = cache(maxCacheSize, loadAssetDescription)
@@ -266,21 +267,28 @@ abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain wi
(orderId, fillInfo) <- diff.orderFills
} yield orderId -> volumeAndFeeCache.get(orderId).combine(fillInfo)
+ val transactionList = diff.transactions.toList
+
+ transactionList.foreach {
+ case (_, (_, tx, _)) =>
+ transactionIds.put(tx.id(), newHeight)
+ }
+
val addressTransactions: Map[AddressId, List[TransactionId]] =
- diff.transactions.toList
+ transactionList
.flatMap {
case (_, (h, tx, addrs)) =>
+ transactionIds.put(tx.id(), newHeight) // be careful here!
+
addrs.map { addr =>
val addrId = AddressId(addressId(addr))
- val htx = (h, tx)
- addrId -> htx
+ addrId -> TransactionId(tx.id())
}
}
.groupBy(_._1)
- .mapValues { txs =>
- val sorted = txs.sortBy { case (_, (h, tx)) => (-h, -tx.timestamp) }
- sorted.map { case (_, (_, tx)) => TransactionId(tx.id()) }
- }
+ .mapValues(_.map {
+ case (_, txId) => txId
+ })
current = (newHeight, current._2 + block.blockScore(), Some(block))
diff --git a/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala b/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala
index ed90217..c419804 100644
--- a/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala
+++ b/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala
@@ -70,12 +70,12 @@ object LevelDBWriter {
}
class LevelDBWriter(writableDB: DB,
- portfolioChanged: Observer[Address],
+ spendableBalanceChanged: Observer[(Address, Option[AssetId])],
fs: FunctionalitySettings,
val maxCacheSize: Int,
val maxRollbackDepth: Int,
val rememberBlocksInterval: Long)
- extends Caches(portfolioChanged)
+ extends Caches(spendableBalanceChanged)
with ScorexLogging {
private val balanceSnapshotMaxRollbackDepth: Int = maxRollbackDepth + 1000
@@ -364,7 +364,7 @@ class LevelDBWriter(writableDB: DB,
(tx.builder.typeId, num)
}
- rw.put(Keys.addressTransactionHN(addressId, nextSeqNr), Some((Height(height), txTypeNumSeq)))
+ rw.put(Keys.addressTransactionHN(addressId, nextSeqNr), Some((Height(height), txTypeNumSeq.sortBy(-_._2))))
rw.put(kk, nextSeqNr)
}
@@ -598,61 +598,47 @@ class LevelDBWriter(writableDB: DB,
override def addressTransactions(address: Address, types: Set[Type], count: Int, fromId: Option[ByteStr]): Either[String, Seq[(Int, Transaction)]] =
readOnly { db =>
- def takeTypes(s: Stream[(Height, Type, TxNum)], maybeTypes: Set[Type]) = {
- if (maybeTypes.nonEmpty) {
- s.filter { case (_, tp, _) => maybeTypes.contains(tp) }
- } else s
+ def takeTypes(txNums: Stream[(Height, Type, TxNum)], maybeTypes: Set[Type]) =
+ if (maybeTypes.nonEmpty) txNums.filter { case (_, tp, _) => maybeTypes.contains(tp) } else txNums
+
+ def takeAfter(txNums: Stream[(Height, Type, TxNum)], maybeAfter: Option[(Height, TxNum)]): Stream[(Height, Type, TxNum)] = maybeAfter match {
+ case None => txNums
+ case Some((afterHeight, filterHeight)) =>
+ txNums
+ .dropWhile { case (streamHeight, _, _) => streamHeight > afterHeight }
+ .dropWhile { case (streamHeight, _, streamNum) => streamNum >= filterHeight && streamHeight >= afterHeight }
}
- def takeAfter(s: Stream[(Height, Type, TxNum)], maybeAfter: Option[(Height, TxNum)]): Stream[(Height, Type, TxNum)] = {
- maybeAfter match {
- case None => s
- case Some((h, num)) =>
- s.dropWhile {
- case (s_h, _, s_n) => s_h != h ^ s_n != num
- }
- .drop(1)
-
- }
- }
-
- val maybeAfter = fromId.flatMap(id => db.get(Keys.transactionHNById(TransactionId(id))))
+ def readTransactions(): Seq[(Int, Transaction)] = {
+ val maybeAfter = fromId.flatMap(id => db.get(Keys.transactionHNById(TransactionId(id))))
- lazy val transactions: Seq[(Int, Transaction)] =
db.get(Keys.addressId(address)).fold(Seq.empty[(Int, Transaction)]) { id =>
val addressId = AddressId(id)
- val hnSeq =
- (db.get(Keys.addressTransactionSeqNr(addressId)) to 1 by -1).toStream
- .flatMap { seqNr =>
- val maybeHNSeq = db.get(Keys.addressTransactionHN(addressId, seqNr))
+ val heightNumStream = (db.get(Keys.addressTransactionSeqNr(addressId)) to 1 by -1).toStream
+ .flatMap(seqNr =>
+ db.get(Keys.addressTransactionHN(addressId, seqNr)) match {
+ case Some((height, txNums)) => txNums.map { case (txType, txNum) => (height, txType, txNum) }
+ case None => Nil
+ })
- maybeHNSeq match {
- case Some((h, seq)) =>
- seq.map { case (tp, num) => (h, tp, num) }.toStream
- case None => Stream.empty
- }
- }
-
- takeAfter(takeTypes(hnSeq, types), maybeAfter)
- .flatMap {
- case (h, _, num) =>
- db.get(Keys.transactionAt(h, num))
- .map((h, _))
- }
+ takeAfter(takeTypes(heightNumStream, types), maybeAfter)
+ .flatMap { case (height, _, txNum) => db.get(Keys.transactionAt(height, txNum)).map((height, _)) }
.take(count)
- .toList
+ .toVector
}
+ }
fromId match {
- case None => Right(transactions)
+ case None =>
+ Right(readTransactions())
+
case Some(id) =>
db.get(Keys.transactionHNById(TransactionId(id))) match {
case None => Left(s"Transaction $id does not exist")
- case _ => Right(transactions)
+ case _ => Right(readTransactions())
}
}
-
}
override def resolveAlias(alias: Alias): Either[ValidationError, Address] = readOnly { db =>
@@ -968,15 +954,24 @@ class LevelDBWriter(writableDB: DB,
)
}
- override def zbsDistribution(height: Int): Map[Address, Long] = readOnly { db =>
- (for {
- seqNr <- (1 to db.get(Keys.addressesForZbsSeqNr)).par
- addressId <- db.get(Keys.addressesForZbs(seqNr)).par
- history = db.get(Keys.zbsBalanceHistory(addressId))
- actualHeight <- history.partition(_ > height)._2.headOption
- balance = db.get(Keys.zbsBalance(addressId)(actualHeight))
- if balance > 0
- } yield db.get(Keys.idToAddress(addressId)) -> balance).toMap.seq
+ override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = readOnly { db =>
+ val canGetAfterHeight = db.get(Keys.safeRollbackHeight)
+
+ def createMap() =
+ (for {
+ seqNr <- (1 to db.get(Keys.addressesForZbsSeqNr)).par
+ addressId <- db.get(Keys.addressesForZbs(seqNr)).par
+ history = db.get(Keys.zbsBalanceHistory(addressId))
+ actualHeight <- history.partition(_ > height)._2.headOption
+ balance = db.get(Keys.zbsBalance(addressId)(actualHeight))
+ if balance > 0
+ } yield db.get(Keys.idToAddress(addressId)) -> balance).toMap.seq
+
+ Either.cond(
+ height > canGetAfterHeight,
+ createMap(),
+ GenericError(s"Cannot get zbs distribution at height less than ${canGetAfterHeight + 1}")
+ )
}
private[database] def loadBlock(height: Height): Option[Block] = readOnly { db =>
diff --git a/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala b/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala
index 5e9e7cd..e14377c 100644
--- a/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala
+++ b/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala
@@ -15,6 +15,10 @@ object BlockchainFeatures {
val SmartAssets = BlockchainFeature(9, "Smart Assets")
val SmartAccountTrading = BlockchainFeature(10, "Smart Account Trading")
val Ride4DApps = BlockchainFeature(11, "RIDE 4 DAPPS")
+ val OrderV3 = BlockchainFeature(12, "Order Version 3")
+
+ // When next fork-parameter is created, you must replace all uses of the DummyFeature with the new one.
+ val DummyFeature = BlockchainFeature(-1, "Non Votable!")
private val dict = Seq(
SmallerMinimalGeneratingBalance,
@@ -27,7 +31,8 @@ object BlockchainFeatures {
FairPoS,
SmartAccountTrading,
SmartAssets,
- Ride4DApps
+ Ride4DApps,
+ OrderV3
).map(f => f.id -> f).toMap
val implemented: Set[Short] = dict.keySet
diff --git a/src/main/scala/com/zbsnetwork/history/StorageFactory.scala b/src/main/scala/com/zbsnetwork/history/StorageFactory.scala
index a7191da..0149b33 100644
--- a/src/main/scala/com/zbsnetwork/history/StorageFactory.scala
+++ b/src/main/scala/com/zbsnetwork/history/StorageFactory.scala
@@ -4,25 +4,25 @@ import com.zbsnetwork.account.Address
import com.zbsnetwork.database.{DBExt, Keys, LevelDBWriter}
import com.zbsnetwork.settings.ZbsSettings
import com.zbsnetwork.state.{BlockchainUpdaterImpl, NG}
-import com.zbsnetwork.transaction.BlockchainUpdater
+import com.zbsnetwork.transaction.{AssetId, BlockchainUpdater}
import com.zbsnetwork.utils.{ScorexLogging, Time, UnsupportedFeature, forceStopApplication}
import monix.reactive.Observer
import org.iq80.leveldb.DB
object StorageFactory extends ScorexLogging {
- private val StorageVersion = 3
+ private val StorageVersion = 4
- def apply(settings: ZbsSettings, db: DB, time: Time, portfolioChanged: Observer[Address]): BlockchainUpdater with NG = {
+ def apply(settings: ZbsSettings, db: DB, time: Time, spendableBalanceChanged: Observer[(Address, Option[AssetId])]): BlockchainUpdater with NG = {
checkVersion(db)
val levelDBWriter = new LevelDBWriter(
db,
- portfolioChanged,
+ spendableBalanceChanged,
settings.blockchainSettings.functionalitySettings,
settings.maxCacheSize,
settings.maxRollbackDepth,
settings.rememberBlocks.toMillis
)
- new BlockchainUpdaterImpl(levelDBWriter, portfolioChanged, settings, time)
+ new BlockchainUpdaterImpl(levelDBWriter, spendableBalanceChanged, settings, time)
}
private def checkVersion(db: DB): Unit = db.readWrite { rw =>
diff --git a/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala b/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala
index 8336403..136e82b 100644
--- a/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala
@@ -7,6 +7,7 @@ import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Route
import cats.implicits._
+import cats.kernel.Monoid
import com.typesafe.config.{ConfigObject, ConfigRenderOptions}
import com.zbsnetwork.account.Address
import com.zbsnetwork.api.http._
@@ -26,13 +27,13 @@ import com.zbsnetwork.transaction.smart.Verifier
import com.zbsnetwork.utils.{ScorexLogging, Time}
import com.zbsnetwork.utx.UtxPool
import com.zbsnetwork.wallet.Wallet
+import com.zbsnetwork.utils.byteStrWrites
import io.netty.channel.Channel
import io.netty.channel.group.ChannelGroup
import io.swagger.annotations._
import javax.ws.rs.Path
import monix.eval.{Coeval, Task}
import play.api.libs.json._
-import com.zbsnetwork.utils.byteStrWrites
import scala.concurrent.Future
import scala.concurrent.duration._
@@ -140,7 +141,8 @@ case class DebugApiRoute(ws: ZbsSettings,
Address.fromString(rawAddress) match {
case Left(_) => complete(InvalidAddress)
case Right(address) =>
- val portfolio = if (considerUnspent.getOrElse(true)) utxStorage.portfolio(address) else ng.portfolio(address)
+ val base = ng.portfolio(address)
+ val portfolio = if (considerUnspent.getOrElse(true)) Monoid.combine(base, utxStorage.pessimisticPortfolio(address)) else base
complete(Json.toJson(portfolio))
}
}
@@ -150,7 +152,7 @@ case class DebugApiRoute(ws: ZbsSettings,
@ApiOperation(value = "State", notes = "Get current state", httpMethod = "GET")
@ApiResponses(Array(new ApiResponse(code = 200, message = "Json state")))
def state: Route = (path("state") & get & withAuth) {
- complete(ng.zbsDistribution(ng.height).map { case (a, b) => a.stringRepr -> b })
+ complete(ng.zbsDistribution(ng.height).map(_.map { case (a, b) => a.stringRepr -> b }))
}
@Path("/stateZbs/{height}")
@@ -160,7 +162,7 @@ case class DebugApiRoute(ws: ZbsSettings,
new ApiImplicitParam(name = "height", value = "height", required = true, dataType = "integer", paramType = "path")
))
def stateZbs: Route = (path("stateZbs" / IntNumber) & get & withAuth) { height =>
- complete(ng.zbsDistribution(height).map { case (a, b) => a.stringRepr -> b })
+ complete(ng.zbsDistribution(height).map(_.map { case (a, b) => a.stringRepr -> b }))
}
private def rollbackToBlock(blockId: ByteStr, returnTransactionsToUtx: Boolean): Future[ToResponseMarshallable] = {
diff --git a/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala b/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala
index adaed13..cd22abf 100644
--- a/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala
@@ -11,7 +11,6 @@ import com.zbsnetwork.matcher.OrderDB.orderInfoOrdering
import com.zbsnetwork.matcher.model.Events.{OrderAdded, OrderCanceled, OrderExecuted}
import com.zbsnetwork.matcher.model.{LimitOrder, OrderInfo, OrderStatus, OrderValidator}
import com.zbsnetwork.matcher.queue.QueueEvent
-import com.zbsnetwork.state.Portfolio
import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.transaction.assets.exchange.AssetPair.assetIdStr
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order}
@@ -26,11 +25,11 @@ import scala.util.{Failure, Success}
class AddressActor(
owner: Address,
- portfolio: => Portfolio,
- maxTimestampDrift: FiniteDuration,
+ spendableBalance: Option[AssetId] => Long,
cancelTimeout: FiniteDuration,
time: Time,
orderDB: OrderDB,
+ hasOrder: Order.Id => Boolean,
storeEvent: StoreEvent,
) extends Actor
with ScorexLogging {
@@ -70,22 +69,17 @@ class AddressActor(
latestOrderTs = newTimestamp
}
- private def tradableBalance(assetId: Option[AssetId]): Long = {
- val p = portfolio
- assetId.fold(p.spendableBalance)(p.assets.getOrElse(_, 0L)) - openVolume(assetId)
- }
+ private def tradableBalance(assetId: Option[AssetId]): Long = spendableBalance(assetId) - openVolume(assetId)
private val validator =
OrderValidator.accountStateAware(owner,
tradableBalance,
activeOrders.size,
- latestOrderTs - maxTimestampDrift.toMillis,
- id => activeOrders.contains(id) || orderDB.contains(id)) _
+ id => activeOrders.contains(id) || orderDB.containsInfo(id) || hasOrder(id)) _
private def handleCommands: Receive = {
- case BalanceUpdated =>
- val newPortfolio = portfolio
- val toCancel = ordersToDelete(toSpendable(newPortfolio))
+ case evt: BalanceUpdated =>
+ val toCancel = ordersToDelete(toSpendable(evt))
if (toCancel.nonEmpty) {
log.debug(s"Canceling: $toCancel")
toCancel.foreach { x =>
@@ -122,17 +116,14 @@ class AddressActor(
Future.successful(api.OrderCancelRejected(reason))
})(_.future) pipeTo sender()
- case CancelAllOrders(maybePair, timestamp) =>
- if ((timestamp - latestOrderTs).abs <= maxTimestampDrift.toMillis) {
- val batchCancelFutures = for {
- lo <- activeOrders.values
- if maybePair.forall(_ == lo.order.assetPair)
- } yield storeCanceled(lo.order.assetPair, lo.order.id()).map(lo.order.id() -> _)
+ case CancelAllOrders(maybePair, _) =>
+ val batchCancelFutures = for {
+ lo <- activeOrders.values
+ if maybePair.forall(_ == lo.order.assetPair)
+ } yield storeCanceled(lo.order.assetPair, lo.order.id()).map(lo.order.id() -> _)
+
+ Future.sequence(batchCancelFutures).map(_.toMap).map(api.BatchCancelCompleted).pipeTo(sender())
- Future.sequence(batchCancelFutures).map(_.toMap).map(api.BatchCancelCompleted).pipeTo(sender())
- } else {
- sender() ! api.OrderCancelRejected("Invalid timestamp")
- }
case CancelExpiredOrder(id) =>
expiration.remove(id)
for (lo <- activeOrders.get(id)) {
@@ -252,14 +243,20 @@ class AddressActor(
private type SpendableBalance = Map[Option[AssetId], Long]
+ /**
+ * @param initBalance Contains only changed assets
+ */
private def ordersToDelete(initBalance: SpendableBalance): Queue[QueueEvent.Canceled] = {
- // Probably, we need to check orders with changed assets only.
+ def keepChanged(requiredBalance: Map[Option[AssetId], Long]) = requiredBalance.filter {
+ case (requiredAssetId, _) => initBalance.contains(requiredAssetId)
+ }
+
// Now a user can have 100 active transaction maximum - easy to traverse.
val (_, r) = activeOrders.values.toSeq
.sortBy(_.order.timestamp)(Ordering[Long]) // Will cancel newest orders first
.view
.map { lo =>
- (lo.order.id(), lo.order.assetPair, lo.requiredBalance)
+ (lo.order.id(), lo.order.assetPair, keepChanged(lo.requiredBalance))
}
.foldLeft((initBalance, Queue.empty[QueueEvent.Canceled])) {
case ((restBalance, toDelete), (id, assetPair, requiredBalance)) =>
@@ -273,11 +270,10 @@ class AddressActor(
r
}
- private def toSpendable(p: Portfolio): SpendableBalance =
- p.assets
- .map { case (k, v) => (Some(k): Option[AssetId]) -> v }
- .updated(None, p.spendableBalance)
- .withDefaultValue(0)
+ private def toSpendable(event: BalanceUpdated): SpendableBalance = {
+ val r: SpendableBalance = event.changedAssets.map(x => x -> spendableBalance(x))(collection.breakOut)
+ r.withDefaultValue(0)
+ }
private def remove(from: SpendableBalance, xs: SpendableBalance): Option[SpendableBalance] =
xs.foldLeft[Option[SpendableBalance]](Some(from)) {
@@ -309,7 +305,7 @@ object AddressActor {
}
case class CancelOrder(orderId: ByteStr) extends Command
case class CancelAllOrders(pair: Option[AssetPair], timestamp: Long) extends Command
- case object BalanceUpdated extends Command
+ case class BalanceUpdated(changedAssets: Set[Option[AssetId]]) extends Command
private case class CancelExpiredOrder(orderId: ByteStr)
}
diff --git a/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala b/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala
index 0a58b75..a68bc5d 100644
--- a/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala
@@ -3,22 +3,17 @@ package com.zbsnetwork.matcher
import akka.actor.{Actor, ActorRef, Props, SupervisorStrategy, Terminated}
import com.zbsnetwork.account.Address
import com.zbsnetwork.common.utils.EitherExt2
-import com.zbsnetwork.matcher.Matcher.StoreEvent
import com.zbsnetwork.matcher.model.Events
-import com.zbsnetwork.state.Portfolio
-import com.zbsnetwork.utils.{ScorexLogging, Time}
+import com.zbsnetwork.transaction.AssetId
+import com.zbsnetwork.utils.ScorexLogging
import monix.execution.Scheduler
import monix.reactive.Observable
import scala.collection.mutable
-import scala.concurrent.duration._
-class AddressDirectory(portfolioChanged: Observable[Address],
- portfolio: Address => Portfolio,
- storeEvent: StoreEvent,
+class AddressDirectory(spendableBalanceChanged: Observable[(Address, Option[AssetId])],
settings: MatcherSettings,
- time: Time,
- orderDB: OrderDB)
+ addressActorProps: Address => Props)
extends Actor
with ScorexLogging {
import AddressDirectory._
@@ -26,21 +21,22 @@ class AddressDirectory(portfolioChanged: Observable[Address],
private[this] val children = mutable.AnyRefMap.empty[Address, ActorRef]
- portfolioChanged
- .filter(children.contains)
+ spendableBalanceChanged
+ .filter(x => children.contains(x._1))
.bufferTimed(settings.balanceWatchingBufferInterval)
.filter(_.nonEmpty)
- .foreach(_.toSet.foreach((address: Address) => children.get(address).foreach(_ ! AddressActor.BalanceUpdated)))(Scheduler(context.dispatcher))
+ .foreach { changes =>
+ val acc = mutable.Map.empty[Address, Set[Option[AssetId]]]
+ changes.foreach { case (addr, changed) => acc.update(addr, acc.getOrElse(addr, Set.empty) + changed) }
+
+ acc.foreach { case (addr, changedAssets) => children.get(addr).foreach(_ ! AddressActor.BalanceUpdated(changedAssets)) }
+ }(Scheduler(context.dispatcher))
override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy
private def createAddressActor(address: Address): ActorRef = {
log.debug(s"Creating address actor for $address")
- watch(
- actorOf(
- Props(new AddressActor(address, portfolio(address), settings.maxTimestampDiff, 5.seconds, time, orderDB, storeEvent)),
- address.toString
- ))
+ watch(actorOf(addressActorProps(address), address.toString))
}
private def forward(address: Address, msg: Any): Unit = {
diff --git a/src/main/scala/com/zbsnetwork/matcher/Matcher.scala b/src/main/scala/com/zbsnetwork/matcher/Matcher.scala
index 766cc95..ba59ec6 100644
--- a/src/main/scala/com/zbsnetwork/matcher/Matcher.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/Matcher.scala
@@ -21,7 +21,8 @@ import com.zbsnetwork.matcher.model.{ExchangeTransactionCreator, OrderBook, Orde
import com.zbsnetwork.matcher.queue._
import com.zbsnetwork.network._
import com.zbsnetwork.settings.ZbsSettings
-import com.zbsnetwork.state.Blockchain
+import com.zbsnetwork.state.{Blockchain, VolumeAndFee}
+import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order}
import com.zbsnetwork.utils.{ErrorStartingMatcher, ScorexLogging, Time, forceStopApplication}
import com.zbsnetwork.utx.UtxPool
@@ -39,7 +40,7 @@ class Matcher(actorSystem: ActorSystem,
utx: UtxPool,
allChannels: ChannelGroup,
blockchain: Blockchain,
- portfoliosChanged: Observable[Address],
+ spendableBalanceChanged: Observable[(Address, Option[AssetId])],
settings: ZbsSettings,
matcherPrivateKey: PrivateKeyAccount)
extends ScorexLogging {
@@ -163,10 +164,28 @@ class Matcher(actorSystem: ActorSystem,
MatcherActor.name
)
+ private lazy val orderDb = OrderDB(matcherSettings, db)
+
private lazy val addressActors =
actorSystem.actorOf(
- Props(new AddressDirectory(portfoliosChanged, utx.portfolio, matcherQueue.storeEvent, matcherSettings, time, OrderDB(matcherSettings, db))),
- "addresses")
+ Props(
+ new AddressDirectory(
+ spendableBalanceChanged,
+ matcherSettings,
+ address =>
+ Props(
+ new AddressActor(
+ address,
+ utx.spendableBalance(address, _),
+ 5.seconds,
+ time,
+ orderDb,
+ id => blockchain.filledVolumeAndFee(id) != VolumeAndFee.empty,
+ matcherQueue.storeEvent
+ ))
+ )),
+ "addresses"
+ )
private lazy val blacklistedAddresses = settings.matcherSettings.blacklistedAddresses.map(Address.fromString(_).explicitGet())
private lazy val matcherPublicKey = PublicKeyAccount(matcherPrivateKey.publicKey)
@@ -275,7 +294,7 @@ object Matcher extends ScorexLogging {
utx: UtxPool,
allChannels: ChannelGroup,
blockchain: Blockchain,
- portfoliosChanged: Observable[Address],
+ spendableBalanceChanged: Observable[(Address, Option[AssetId])],
settings: ZbsSettings): Option[Matcher] =
try {
val privateKey = (for {
@@ -283,7 +302,7 @@ object Matcher extends ScorexLogging {
pk <- wallet.privateKeyAccount(address)
} yield pk).explicitGet()
- val matcher = new Matcher(actorSystem, time, utx, allChannels, blockchain, portfoliosChanged, settings, privateKey)
+ val matcher = new Matcher(actorSystem, time, utx, allChannels, blockchain, spendableBalanceChanged, settings, privateKey)
matcher.runMatcher()
Some(matcher)
} catch {
diff --git a/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala b/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala
index 95582bc..bae2c83 100644
--- a/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala
@@ -30,11 +30,9 @@ case class MatcherSettings(enable: Boolean,
startEventsProcessingTimeout: FiniteDuration,
makeSnapshotsAtStart: Boolean,
priceAssets: Seq[String],
- maxTimestampDiff: FiniteDuration,
blacklistedAssets: Set[String],
blacklistedNames: Seq[Regex],
maxOrdersPerRequest: Int,
- orderTimestampDrift: Long,
// this is not a Set[Address] because to parse an address, global AddressScheme must be initialized
blacklistedAddresses: Set[String],
orderBookSnapshotHttpCache: OrderBookSnapshotHttpCache.Settings,
@@ -72,9 +70,7 @@ object MatcherSettings {
val startEventsProcessingTimeout = config.as[FiniteDuration](s"$configPath.start-events-processing-timeout")
val makeSnapshotsAtStart = config.as[Boolean](s"$configPath.make-snapshots-at-start")
val maxOrdersPerRequest = config.as[Int](s"$configPath.rest-order-limit")
- val orderTimestampDrift = config.as[FiniteDuration](s"$configPath.order-timestamp-drift")
val baseAssets = config.as[List[String]](s"$configPath.price-assets")
- val maxTimestampDiff = config.as[FiniteDuration](s"$configPath.max-timestamp-diff")
val blacklistedAssets = config.as[List[String]](s"$configPath.blacklisted-assets")
val blacklistedNames = config.as[List[String]](s"$configPath.blacklisted-names").map(_.r)
@@ -103,11 +99,9 @@ object MatcherSettings {
startEventsProcessingTimeout,
makeSnapshotsAtStart,
baseAssets,
- maxTimestampDiff,
blacklistedAssets.toSet,
blacklistedNames,
maxOrdersPerRequest,
- orderTimestampDrift.toMillis,
blacklistedAddresses,
orderBookSnapshotHttpCache,
balanceWatchingBufferInterval,
diff --git a/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala b/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala
index 7b18091..c41c8fc 100644
--- a/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala
@@ -10,7 +10,7 @@ import com.zbsnetwork.utils.ScorexLogging
import org.iq80.leveldb.DB
trait OrderDB {
- def contains(id: ByteStr): Boolean
+ def containsInfo(id: ByteStr): Boolean
def status(id: ByteStr): OrderStatus.Final
def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit
def saveOrder(o: Order): Unit
@@ -20,8 +20,10 @@ trait OrderDB {
}
object OrderDB {
+ private val OldestOrderIndexOffset = 100
+
def apply(settings: MatcherSettings, db: DB): OrderDB = new OrderDB with ScorexLogging {
- override def contains(id: ByteStr): Boolean = db.readOnly(_.has(MatcherKeys.order(id)))
+ override def containsInfo(id: ByteStr): Boolean = db.readOnly(_.has(MatcherKeys.orderInfo(id)))
override def status(id: ByteStr): OrderStatus.Final = db.readOnly { ro =>
ro.get(MatcherKeys.orderInfo(id)).fold[OrderStatus.Final](OrderStatus.NotFound)(_.status)
@@ -42,8 +44,14 @@ object OrderDB {
db.readWrite { rw =>
val newCommonSeqNr = rw.inc(MatcherKeys.finalizedCommonSeqNr(sender))
rw.put(MatcherKeys.finalizedCommon(sender, newCommonSeqNr), Some(id))
+
val newPairSeqNr = rw.inc(MatcherKeys.finalizedPairSeqNr(sender, oi.assetPair))
rw.put(MatcherKeys.finalizedPair(sender, oi.assetPair, newPairSeqNr), Some(id))
+ if (newPairSeqNr > OldestOrderIndexOffset) // Indexes start with 1, so if newPairSeqNr=101, we delete 1 (the first)
+ rw.get(MatcherKeys.finalizedPair(sender, oi.assetPair, newPairSeqNr - OldestOrderIndexOffset))
+ .map(MatcherKeys.order)
+ .foreach(x => rw.delete(x))
+
rw.put(orderInfoKey, Some(oi))
}
}
diff --git a/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala b/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala
index daa80d7..a046e5f 100644
--- a/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala
@@ -62,15 +62,9 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
override val settings: RestAPISettings = restAPISettings
- private val timer = Kamon.timer("matcher.api-requests")
- private val placeTimer = timer.refine("action" -> "place")
-
override lazy val route: Route = pathPrefix("matcher") {
matcherStatusBarrier {
- getMatcherPublicKey ~ getOrderBook ~ marketStatus ~ place ~ getAssetPairAndPublicKeyOrderHistory ~ getPublicKeyOrderHistory ~
- getAllOrderHistory ~ tradableBalance ~ reservedBalance ~ orderStatus ~
- historyDelete ~ cancel ~ cancelAll ~ orderbooks ~ orderBookDelete ~ getTransactionsByOrder ~ forceCancelOrder ~
- getSettings ~ getCurrentOffset ~ getOldestSnapshotOffset ~ getAllSnapshotOffsets
+ getMatcherPublicKey
}
}
@@ -79,476 +73,12 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
case Matcher.Status.Starting => complete(DuringStart)
case Matcher.Status.Stopping => complete(DuringShutdown)
}
-
- private def unavailableOrderBookBarrier(p: AssetPair): Directive0 = orderBook(p) match {
- case Some(Left(_)) => complete(OrderBookUnavailable)
- case _ => pass
- }
-
- private def withAssetPair(p: AssetPair, redirectToInverse: Boolean = false, suffix: String = ""): Directive1[AssetPair] =
- assetPairBuilder.validateAssetPair(p) match {
- case Right(_) => provide(p)
- case Left(e) if redirectToInverse =>
- assetPairBuilder
- .validateAssetPair(p.reverse)
- .fold(
- _ => complete(StatusCodes.NotFound -> Json.obj("message" -> e)),
- _ => redirect(s"/matcher/orderbook/${p.priceAssetStr}/${p.amountAssetStr}$suffix", StatusCodes.MovedPermanently)
- )
- case Left(e) => complete(StatusCodes.NotFound -> Json.obj("message" -> e))
- }
-
- private def withCancelRequest(f: CancelOrderRequest => Route): Route =
- post {
- entity(as[CancelOrderRequest]) { req =>
- if (req.isSignatureValid()) f(req) else complete(InvalidSignature)
- } ~ complete(StatusCodes.BadRequest)
- } ~ complete(StatusCodes.MethodNotAllowed)
-
- private def signedGet(publicKey: PublicKeyAccount): Directive0 =
- (headerValueByName("Timestamp") & headerValueByName("Signature")).tflatMap {
- case (ts, sig) =>
- val timestamp = ts.toLong
- require(math.abs(timestamp - time.correctedTime()).millis < matcherSettings.maxTimestampDiff, "Incorrect timestamp")
- require(crypto.verify(Base58.decode(sig).get, publicKey.publicKey ++ Longs.toByteArray(timestamp), publicKey.publicKey),
- "Incorrect signature")
- pass
- }
-
- @inline
- private def askAddressActor[A: ClassTag](sender: Address, msg: AddressActor.Command): Future[A] = {
- (addressActor ? Env(sender, msg))
- .mapTo[A]
- .andThen {
- case Failure(e) => log.warn(s"Error processing $msg", e)
- }
- }
-
@Path("/")
@ApiOperation(value = "Matcher Public Key", notes = "Get matcher public key", httpMethod = "GET")
def getMatcherPublicKey: Route = (pathEndOrSingleSlash & get) {
complete(JsString(Base58.encode(matcherPublicKey.publicKey)))
}
- @Path("/settings")
- @ApiOperation(value = "Matcher Settings", notes = "Get matcher settings", httpMethod = "GET")
- def getSettings: Route = (path("settings") & get) {
- complete(StatusCodes.OK -> Json.obj("priceAssets" -> matcherSettings.priceAssets))
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}")
- @ApiOperation(value = "Get Order Book for a given Asset Pair", notes = "Get Order Book for a given Asset Pair", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "depth",
- value = "Limit the number of bid/ask records returned",
- required = false,
- dataType = "integer",
- paramType = "query")
- ))
- def getOrderBook: Route = (path("orderbook" / AssetPairPM) & get) { p =>
- parameters('depth.as[Int].?) { depth =>
- withAssetPair(p, redirectToInverse = true) { pair =>
- complete(orderBookSnapshot.get(pair, depth))
- }
- }
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/status")
- @ApiOperation(value = "Get Market Status", notes = "Get current market data such as last trade, best bid and ask", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path")
- ))
- def marketStatus: Route = (path("orderbook" / AssetPairPM / "status") & get) { p =>
- withAssetPair(p, redirectToInverse = true) { pair =>
- getMarketStatus(pair).fold(complete(StatusCodes.NotFound -> Json.obj("message" -> "There is no information about this asset pair"))) { ms =>
- complete(StatusCodes.OK -> ms)
- }
- }
- }
-
- @Path("/orderbook")
- @ApiOperation(value = "Place order",
- notes = "Place a new limit order (buy or sell)",
- httpMethod = "POST",
- produces = "application/json",
- consumes = "application/json")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(
- name = "body",
- value = "Json with data",
- required = true,
- paramType = "body",
- dataType = "com.zbsnetwork.transaction.assets.exchange.Order"
- )
- ))
- def place: Route = path("orderbook") {
- (pathEndOrSingleSlash & post) {
- _json[Order] { order =>
- unavailableOrderBookBarrier(order.assetPair) {
- complete {
- placeTimer.measureFuture {
- orderValidator(order) match {
- case Right(_) =>
- placeTimer.measureFuture(askAddressActor[MatcherResponse](order.sender, AddressActor.PlaceOrder(order)))
- case Left(error) => Future.successful[MatcherResponse](OrderRejected(error))
- }
- }
- }
- }
- }
- }
- }
-
- @Path("/orderbook")
- @ApiOperation(value = "Get the open trading markets", notes = "Get the open trading markets along with trading pairs meta data", httpMethod = "GET")
- def orderbooks: Route = (path("orderbook") & pathEndOrSingleSlash & get) {
- complete((matcher ? GetMarkets).mapTo[Seq[MarketData]].map { markets =>
- StatusCodes.OK -> Json.obj(
- "matcherPublicKey" -> Base58.encode(matcherPublicKey.publicKey),
- "markets" -> JsArray(markets.map(m =>
- Json.obj(
- "amountAsset" -> m.pair.amountAssetStr,
- "amountAssetName" -> m.amountAssetName,
- "amountAssetInfo" -> m.amountAssetInfo,
- "priceAsset" -> m.pair.priceAssetStr,
- "priceAssetName" -> m.priceAssetName,
- "priceAssetInfo" -> m.priceAssetinfo,
- "created" -> m.created
- )))
- )
- })
- }
-
- private def handleCancelRequest(assetPair: Option[AssetPair], sender: Address, orderId: Option[ByteStr], timestamp: Option[Long]): Route =
- complete((timestamp, orderId) match {
- case (Some(ts), None) => askAddressActor[MatcherResponse](sender, AddressActor.CancelAllOrders(assetPair, ts))
- case (None, Some(oid)) => askAddressActor[MatcherResponse](sender, AddressActor.CancelOrder(oid))
- case _ => OrderCancelRejected("Either timestamp or orderId must be specified")
- })
-
- private def handleCancelRequest(assetPair: Option[AssetPair]): Route =
- withCancelRequest { req =>
- handleCancelRequest(assetPair, req.sender, req.orderId, req.timestamp)
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/cancel")
- @ApiOperation(
- value = "Cancel order",
- notes = "Cancel previously submitted order if it's not already filled completely",
- httpMethod = "POST",
- produces = "application/json",
- consumes = "application/json"
- )
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(
- name = "body",
- value = "Json with data",
- required = true,
- paramType = "body",
- dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest"
- )
- ))
- def cancel: Route = path("orderbook" / AssetPairPM / "cancel") { p =>
- withAssetPair(p) { pair =>
- handleCancelRequest(Some(pair))
- }
- }
-
- @Path("/orderbook/cancel")
- @ApiOperation(
- value = "Cancel all active orders",
- httpMethod = "POST",
- produces = "application/json",
- consumes = "application/json"
- )
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(
- name = "body",
- value = "Json with data",
- required = true,
- paramType = "body",
- dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest"
- )
- ))
- def cancelAll: Route = path("orderbook" / "cancel") {
- handleCancelRequest(None)
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/delete")
- @Deprecated
- @ApiOperation(
- value = "Delete Order from History by Id",
- notes = "This method is deprecated and doesn't work anymore. Please don't use it.",
- httpMethod = "POST",
- produces = "application/json",
- consumes = "application/json"
- )
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(
- name = "body",
- value = "Json with data",
- required = true,
- paramType = "body",
- dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest"
- )
- ))
- def historyDelete: Route = (path("orderbook" / AssetPairPM / "delete") & post) { _ =>
- json[CancelOrderRequest] { req =>
- req.orderId.fold[MatcherResponse](NotImplemented("Batch order deletion is not supported yet"))(OrderDeleted)
- }
- }
-
- private def loadOrders(address: Address, pair: Option[AssetPair], activeOnly: Boolean): Route = complete {
- askAddressActor[Seq[(ByteStr, OrderInfo[OrderStatus])]](address, AddressActor.GetOrders(pair, activeOnly))
- .map(orders =>
- StatusCodes.OK -> orders.map {
- case (id, oi) =>
- Json.obj(
- "id" -> id.base58,
- "type" -> oi.side.toString,
- "amount" -> oi.amount,
- "price" -> oi.price,
- "timestamp" -> oi.timestamp,
- "filled" -> (oi.status match {
- case OrderStatus.Filled(f) => f
- case OrderStatus.PartiallyFilled(f) => f
- case OrderStatus.Cancelled(f) => f
- case _ => 0L
- }),
- "status" -> oi.status.name,
- "assetPair" -> oi.assetPair.json
- )
- })
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/publicKey/{publicKey}")
- @ApiOperation(value = "Order History by Asset Pair and Public Key",
- notes = "Get Order History for a given Asset Pair and Public Key",
- httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"),
- new ApiImplicitParam(
- name = "activeOnly",
- value = "Return active only orders (Accepted and PartiallyFilled)",
- required = false,
- dataType = "boolean",
- paramType = "query",
- defaultValue = "false"
- ),
- new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"),
- new ApiImplicitParam(name = "Signature",
- value = "Signature of [Public Key ++ Timestamp] bytes",
- required = true,
- dataType = "string",
- paramType = "header")
- ))
- def getAssetPairAndPublicKeyOrderHistory: Route = (path("orderbook" / AssetPairPM / "publicKey" / PublicKeyPM) & get) { (p, publicKey) =>
- withAssetPair(p, redirectToInverse = true, s"/publicKey/$publicKey") { pair =>
- parameters('activeOnly.as[Boolean].?) { activeOnly =>
- signedGet(publicKey) {
- loadOrders(publicKey, Some(pair), activeOnly.getOrElse(false))
- }
- }
- }
- }
-
- @Path("/orderbook/{publicKey}")
- @ApiOperation(value = "Order History by Public Key", notes = "Get Order History for a given Public Key", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"),
- new ApiImplicitParam(
- name = "activeOnly",
- value = "Return active only orders (Accepted and PartiallyFilled)",
- required = false,
- dataType = "boolean",
- paramType = "query",
- defaultValue = "false"
- ),
- new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"),
- new ApiImplicitParam(name = "Signature",
- value = "Signature of [Public Key ++ Timestamp] bytes",
- required = true,
- dataType = "string",
- paramType = "header")
- ))
- def getPublicKeyOrderHistory: Route = (path("orderbook" / PublicKeyPM) & get) { publicKey =>
- parameters('activeOnly.as[Boolean].?) { activeOnly =>
- signedGet(publicKey) {
- loadOrders(publicKey, None, activeOnly.getOrElse(false))
- }
- }
- }
-
- @Path("/orders/cancel/{orderId}")
- @ApiOperation(value = "Cancel Order by ID without signature", notes = "Cancel Order by ID without signature", httpMethod = "POST")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "orderId", value = "Order Id", required = true, dataType = "string", paramType = "path")
- ))
- def forceCancelOrder: Route = (path("orders" / "cancel" / ByteStrPM) & post & withAuth) { orderId =>
- DBUtils.order(db, orderId) match {
- case Some(order) => handleCancelRequest(None, order.sender, Some(orderId), None)
- case None => complete(OrderCancelRejected("Order not found"))
- }
- }
-
- @Path("/orders/{address}")
- @ApiOperation(value = "All Order History by address", notes = "Get All Order History for a given address", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "address", value = "Address", dataType = "string", paramType = "path"),
- new ApiImplicitParam(
- name = "activeOnly",
- value = "Return active only orders (Accepted and PartiallyFilled)",
- required = false,
- dataType = "boolean",
- paramType = "query",
- defaultValue = "false"
- ),
- ))
- def getAllOrderHistory: Route = (path("orders" / AddressPM) & get & withAuth) { address =>
- parameters('activeOnly.as[Boolean].?) { activeOnly =>
- loadOrders(address, None, activeOnly.getOrElse(true))
- }
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/tradableBalance/{address}")
- @ApiOperation(value = "Tradable balance for Asset Pair", notes = "Get Tradable balance for the given Asset Pair", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "address", value = "Account Address", required = true, dataType = "string", paramType = "path")
- ))
- def tradableBalance: Route = (path("orderbook" / AssetPairPM / "tradableBalance" / AddressPM) & get) { (pair, address) =>
- withAssetPair(pair, redirectToInverse = true, s"/tradableBalance/$address") { pair =>
- complete {
- askAddressActor[Map[Option[AssetId], Long]](address, AddressActor.GetTradableBalance(pair))
- .map(stringifyAssetIds)
- }
- }
- }
-
- @Path("/balance/reserved/{publicKey}")
- @ApiOperation(value = "Reserved Balance", notes = "Get non-zero balance of open orders", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"),
- new ApiImplicitParam(name = "Signature",
- value = "Signature of [Public Key ++ Timestamp] bytes",
- required = true,
- dataType = "string",
- paramType = "header")
- ))
- def reservedBalance: Route = (path("balance" / "reserved" / PublicKeyPM) & get) { publicKey =>
- signedGet(publicKey) {
- complete {
- askAddressActor[Map[Option[AssetId], Long]](publicKey, AddressActor.GetReservedBalance)
- .map(stringifyAssetIds)
- }
- }
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}/{orderId}")
- @ApiOperation(value = "Order Status", notes = "Get Order status for a given Asset Pair during the last 30 days", httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "orderId", value = "Order Id", required = true, dataType = "string", paramType = "path")
- ))
- def orderStatus: Route = (path("orderbook" / AssetPairPM / ByteStrPM) & get) { (p, orderId) =>
- withAssetPair(p, redirectToInverse = true, s"/$orderId") { _ =>
- complete(
- DBUtils
- .order(db, orderId)
- .fold[Future[OrderStatus]](Future.successful(OrderStatus.NotFound)) { order =>
- askAddressActor[OrderStatus](order.sender, GetOrderStatus(orderId))
- }
- .map(_.json))
- }
- }
-
- @Path("/orderbook/{amountAsset}/{priceAsset}")
- @ApiOperation(value = "Remove Order Book for a given Asset Pair", notes = "Remove Order Book for a given Asset Pair", httpMethod = "DELETE")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"),
- new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path")
- ))
- def orderBookDelete: Route = (path("orderbook" / AssetPairPM) & delete & withAuth) { p =>
- withAssetPair(p) { pair =>
- complete(storeEvent(QueueEvent.OrderBookDeleted(pair)).map(_ => SimpleResponse(StatusCodes.Accepted, "Deleting order book")))
- }
- }
-
- @Path("/transactions/{orderId}")
- @ApiOperation(value = "Get Exchange Transactions for order",
- notes = "Get all exchange transactions created by DEX on execution of the given order",
- httpMethod = "GET")
- @ApiImplicitParams(
- Array(
- new ApiImplicitParam(name = "orderId", value = "Order Id", dataType = "string", paramType = "path")
- ))
- def getTransactionsByOrder: Route = (path("transactions" / ByteStrPM) & get) { orderId =>
- complete(StatusCodes.OK -> Json.toJson(DBUtils.transactionsForOrder(db, orderId)))
- }
-
- @Path("/debug/currentOffset")
- @ApiOperation(value = "Get a current offset in the queue", notes = "", httpMethod = "GET")
- def getCurrentOffset: Route = (path("debug" / "currentOffset") & get & withAuth) {
- complete(StatusCodes.OK -> currentOffset())
- }
-
- @Path("/debug/oldestSnapshotOffset")
- @ApiOperation(value = "Get the oldest snapshot's offset in the queue", notes = "", httpMethod = "GET")
- def getOldestSnapshotOffset: Route = (path("debug" / "oldestSnapshotOffset") & get & withAuth) {
- complete {
- (matcher ? GetSnapshotOffsets).mapTo[SnapshotOffsetsResponse].map { x =>
- StatusCodes.OK -> x.offsets.valuesIterator.min
- }
- }
- }
-
- @Path("/debug/allSnapshotOffsets")
- @ApiOperation(value = "Get all snapshots' offsets in the queue", notes = "", httpMethod = "GET")
- def getAllSnapshotOffsets: Route = (path("debug" / "allSnapshotOffsets") & get & withAuth) {
- complete {
- (matcher ? GetSnapshotOffsets).mapTo[SnapshotOffsetsResponse].map { x =>
- val js = Json.obj(
- x.offsets.map {
- case (assetPair, offset) =>
- assetPair.key -> Json.toJsFieldJsValueWrapper(offset)
- }.toSeq: _*
- )
-
- StatusCodes.OK -> js
- }
- }
- }
}
-object MatcherApiRoute {
- private implicit val timeout: Timeout = 5.seconds
-
- private def stringifyAssetIds(balances: Map[Option[AssetId], Long]): Map[String, Long] =
- balances.map { case (aid, v) => AssetPair.assetIdStr(aid) -> v }
-}
+object MatcherApiRoute {}
diff --git a/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala b/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala
index abceb8e..d57265a 100644
--- a/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala
@@ -159,14 +159,12 @@ object OrderValidator {
sender: Address,
tradableBalance: Option[AssetId] => Long,
activeOrderCount: => Int,
- lowestOrderTimestamp: => Long,
orderExists: ByteStr => Boolean,
)(order: Order): ValidationResult =
for {
_ <- (Right(order): ValidationResult)
.ensure(s"Order sender ${order.sender.toAddress} does not match expected $sender")(_.sender.toAddress == sender)
.ensure(s"Limit of $MaxActiveOrders active orders has been reached")(_ => activeOrderCount < MaxActiveOrders)
- .ensure(s"Order should have a timestamp after $lowestOrderTimestamp, but it is ${order.timestamp}")(_.timestamp > lowestOrderTimestamp)
.ensure("Order has already been placed")(o => !orderExists(o.id()))
_ <- validateBalance(order, tradableBalance)
} yield order
diff --git a/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala b/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala
index 7098bb0..14d2fc2 100644
--- a/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala
@@ -76,10 +76,13 @@ class KafkaMatcherQueue(settings: Settings)(implicit mat: ActorMaterializer) ext
private val consumerControl = new AtomicReference[Consumer.Control](Consumer.NoopControl)
private val consumerSettings = {
val config = mat.system.settings.config.getConfig("akka.kafka.consumer")
- ConsumerSettings(config, new ByteArrayDeserializer, deserializer)
+ ConsumerSettings(config, new ByteArrayDeserializer, deserializer).withClientId("consumer")
}
- private val metadataConsumer = mat.system.actorOf(KafkaConsumerActor.props(consumerSettings))
+ private val metadataConsumer = mat.system.actorOf(
+ KafkaConsumerActor.props(consumerSettings.withClientId("meta-consumer")),
+ "meta-consumer"
+ )
override def startConsume(fromOffset: QueueEventWithMeta.Offset, process: QueueEventWithMeta => Unit): Unit = {
log.info(s"Start consuming from $fromOffset")
diff --git a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala
index 031333d..4471bab 100644
--- a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala
@@ -6,12 +6,12 @@ import cats.kernel.Monoid
import com.zbsnetwork.lang.Global
import com.zbsnetwork.lang.StdLibVersion._
import com.zbsnetwork.lang.v1.compiler.Terms.{CONST_LONG, CaseObj}
-import com.zbsnetwork.lang.v1.compiler.Types.FINAL
+import com.zbsnetwork.lang.v1.compiler.Types.{FINAL, UNIT}
import com.zbsnetwork.lang.v1.evaluator.FunctionIds._
import com.zbsnetwork.lang.v1.evaluator.ctx._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.Bindings.{ordType, orderObject}
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.Types._
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext, _}
+import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
import com.zbsnetwork.lang.v1.traits.domain.OrdType
import com.zbsnetwork.lang.v1.{CTX, FunctionHeader}
import com.zbsnetwork.transaction.assets.exchange.Order
diff --git a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala
index c648b07..0982296 100644
--- a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala
+++ b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala
@@ -5,17 +5,17 @@ import com.zbsnetwork.account.AddressScheme
import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FALSE, TRUE}
import com.zbsnetwork.lang.v1.evaluator.{ContractEvaluator, EvaluatorV1, Log}
-import com.zbsnetwork.transaction.{Authorized, Proven}
import com.zbsnetwork.transaction.assets.exchange.Order
-import com.zbsnetwork.transaction.smart.{RealTransactionWrapper, Verifier}
+import com.zbsnetwork.transaction.smart.script.v1.ExprScript
import com.zbsnetwork.transaction.smart.script.{ContractScript, Script}
-import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl
+import com.zbsnetwork.transaction.smart.{RealTransactionWrapper, Verifier}
+import com.zbsnetwork.transaction.{Authorized, Proven}
import monix.eval.Coeval
object MatcherScriptRunner {
def apply(script: Script, order: Order, isTokenScript: Boolean): (Log, Either[String, EVALUATED]) = script match {
- case s: ExprScriprImpl =>
+ case s: ExprScript =>
val ctx = MatcherContext.build(script.stdLibVersion, AddressScheme.current.chainId, Coeval.evalOnce(order), !isTokenScript)
EvaluatorV1.applywithLogging(ctx, s.expr)
diff --git a/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala b/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala
index 734c226..6066770 100644
--- a/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala
+++ b/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala
@@ -9,10 +9,11 @@ import com.zbsnetwork.transaction.Transaction
import com.zbsnetwork.utils.ScorexLogging
import com.zbsnetwork.utx.UtxPool
import io.netty.channel.Channel
-import io.netty.channel.group.{ChannelGroup, ChannelMatcher}
+import io.netty.channel.group.ChannelGroup
+import monix.eval.Task
import monix.execution.{CancelableFuture, Scheduler}
+import monix.reactive.OverflowStrategy
-import scala.util.control.NonFatal
import scala.util.{Failure, Success}
object UtxPoolSynchronizer extends ScorexLogging {
@@ -21,7 +22,7 @@ object UtxPoolSynchronizer extends ScorexLogging {
settings: UtxSynchronizerSettings,
allChannels: ChannelGroup,
txSource: ChannelObservable[Transaction]): CancelableFuture[Unit] = {
- implicit val scheduler: Scheduler = Scheduler.singleThread("utx-pool-sync")
+ implicit val scheduler: Scheduler = Scheduler.forkJoin(settings.parallelism, settings.maxThreads, "utx-pool-sync")
val dummy = new Object()
val knownTransactions = CacheBuilder
@@ -30,39 +31,46 @@ object UtxPoolSynchronizer extends ScorexLogging {
.expireAfterWrite(settings.networkTxCacheTime.toMillis, TimeUnit.MILLISECONDS)
.build[ByteStr, Object]
- val synchronizerFuture = txSource
+ val newTxSource = txSource
.observeOn(scheduler)
- .bufferTimedAndCounted(settings.maxBufferTime, settings.maxBufferSize)
- .foreach { txBuffer =>
- val toAdd = txBuffer.filter {
- case (_, tx) =>
- val isNew = Option(knownTransactions.getIfPresent(tx.id())).isEmpty
- if (isNew) knownTransactions.put(tx.id(), dummy)
- isNew
- }
+ .filter {
+ case (_, tx) =>
+ var isNew = false
+ knownTransactions.get(tx.id(), { () =>
+ isNew = true; dummy
+ })
+ isNew
+ }
+
+ val synchronizerFuture = newTxSource
+ .whileBusyBuffer(OverflowStrategy.DropOldAndSignal(settings.maxQueueSize, { dropped =>
+ log.warn(s"UTX queue overflow: $dropped transactions dropped")
+ None
+ }))
+ .mapParallelUnordered(settings.parallelism) {
+ case (sender, transaction) =>
+ Task {
+ concurrent.blocking(utx.putIfNew(transaction)) match {
+ case Right((isNew, _)) =>
+ if (isNew) Some(allChannels.write(RawBytes.from(transaction), (_: Channel) != sender))
+ else None
- if (toAdd.nonEmpty) {
- toAdd
- .groupBy { case (channel, _) => channel }
- .foreach {
- case (sender, xs) =>
- val channelMatcher: ChannelMatcher = { (_: Channel) != sender }
- xs.foreach {
- case (_, tx) =>
- utx.putIfNew(tx) match {
- case Right((true, _)) => allChannels.write(RawBytes.from(tx), channelMatcher)
- case _ =>
- }
- }
+ case Left(error) =>
+ log.debug(s"Error adding transaction to UTX pool: $error")
+ None
}
- allChannels.flush()
- }
+ }
}
+ .bufferTimedAndCounted(settings.maxBufferTime, settings.maxBufferSize)
+ .filter(_.flatten.nonEmpty)
+ .foreachL(_ => allChannels.flush())
+ .runAsyncLogErr
synchronizerFuture.onComplete {
- case Success(_) => log.error("UtxPoolSynschronizer stops")
- case Failure(NonFatal(th)) => log.error("Error in utx pool synchronizer", th)
+ case Success(_) => log.info("UtxPoolSynschronizer stops")
+ case Failure(error) => log.error("Error in utx pool synchronizer", error)
}
+
synchronizerFuture
}
}
diff --git a/src/main/scala/com/zbsnetwork/network/messages.scala b/src/main/scala/com/zbsnetwork/network/messages.scala
index 99519d6..2a5f053 100644
--- a/src/main/scala/com/zbsnetwork/network/messages.scala
+++ b/src/main/scala/com/zbsnetwork/network/messages.scala
@@ -2,12 +2,12 @@ package com.zbsnetwork.network
import java.net.InetSocketAddress
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.block.{Block, MicroBlock}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.crypto
import com.zbsnetwork.transaction.{Signed, Transaction}
+import monix.eval.Coeval
sealed trait Message
@@ -41,7 +41,7 @@ case class MicroBlockRequest(totalBlockSig: ByteStr) extends Message
case class MicroBlockResponse(microblock: MicroBlock) extends Message
case class MicroBlockInv(sender: PublicKeyAccount, totalBlockSig: ByteStr, prevBlockSig: ByteStr, signature: ByteStr) extends Message with Signed {
- override protected val signatureValid: Coeval[Boolean] =
+ override val signatureValid: Coeval[Boolean] =
Coeval.evalOnce(crypto.verify(signature.arr, sender.toAddress.bytes.arr ++ totalBlockSig.arr ++ prevBlockSig.arr, sender.publicKey))
override def toString: String = s"MicroBlockInv(${totalBlockSig.trim} ~> ${prevBlockSig.trim})"
diff --git a/src/main/scala/com/zbsnetwork/network/package.scala b/src/main/scala/com/zbsnetwork/network/package.scala
index 3f38abf..339d51a 100644
--- a/src/main/scala/com/zbsnetwork/network/package.scala
+++ b/src/main/scala/com/zbsnetwork/network/package.scala
@@ -17,10 +17,13 @@ import monix.reactive.Observable
import com.zbsnetwork.block.Block
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.transaction.Transaction
+import kamon.Kamon
import scala.concurrent.duration._
package object network extends ScorexLogging {
+ private val broadcastTimeStats = Kamon.timer("network-broadcast-time")
+
def inetSocketAddress(addr: String, defaultPort: Int): InetSocketAddress = {
val uri = new URI(s"node://$addr")
if (uri.getPort < 0) new InetSocketAddress(addr, defaultPort)
@@ -69,9 +72,14 @@ package object network extends ScorexLogging {
def broadcast(message: AnyRef, except: Set[Channel]): ChannelGroupFuture = {
logBroadcast(message, except)
- allChannels.writeAndFlush(message, { (channel: Channel) =>
- !except.contains(channel)
- })
+ val st = broadcastTimeStats.refine("object", message.getClass.getSimpleName).start()
+ allChannels
+ .writeAndFlush(message, { (channel: Channel) =>
+ !except.contains(channel)
+ })
+ .addListener { _: ChannelGroupFuture =>
+ st.stop()
+ }
}
def broadcastMany(messages: Seq[AnyRef], except: Set[Channel] = Set.empty): Unit = {
diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala
new file mode 100644
index 0000000..209127f
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala
@@ -0,0 +1,13 @@
+package com.zbsnetwork.protobuf.block
+import com.google.protobuf.ByteString
+import com.zbsnetwork.protobuf.utils.PBUtils
+
+private[block] object PBBlockSerialization {
+ def signedBytes(block: PBBlock): Array[Byte] = {
+ PBUtils.encodeDeterministic(block)
+ }
+
+ def unsignedBytes(block: PBBlock): Array[Byte] = {
+ PBUtils.encodeDeterministic(block.withHeader(block.getHeader.withSignature(ByteString.EMPTY)))
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala
new file mode 100644
index 0000000..2e36ca5
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala
@@ -0,0 +1,79 @@
+package com.zbsnetwork.protobuf.block
+import com.google.protobuf.ByteString
+import com.zbsnetwork.account.PublicKeyAccount
+import com.zbsnetwork.block.SignerData
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.consensus.nxt.NxtLikeConsensusBlockData
+import com.zbsnetwork.protobuf.transaction.{PBTransactions, VanillaTransaction}
+import com.zbsnetwork.transaction.ValidationError
+import com.zbsnetwork.transaction.ValidationError.GenericError
+
+object PBBlocks {
+ def vanilla(block: PBBlock): Either[ValidationError, VanillaBlock] = {
+ def create(version: Int,
+ timestamp: Long,
+ reference: ByteStr,
+ consensusData: NxtLikeConsensusBlockData,
+ transactionData: Seq[VanillaTransaction],
+ featureVotes: Set[Short],
+ generator: PublicKeyAccount,
+ signature: ByteStr): VanillaBlock = {
+ VanillaBlock(timestamp, version.toByte, reference, SignerData(generator, signature), consensusData, transactionData, featureVotes)
+ }
+
+ for {
+ signedHeader <- block.header.toRight(GenericError("No block header"))
+ header <- signedHeader.header.toRight(GenericError("No block header"))
+ transactions <- {
+ val eithers = block.transactions.map(PBTransactions.vanilla(_))
+ (eithers.find(_.isLeft): @unchecked) match {
+ case None => Right(eithers.map(_.right.get))
+ case Some(Left(error)) => Left(error)
+ }
+ }
+ result = create(
+ header.version,
+ header.timestamp,
+ ByteStr(header.reference.toByteArray),
+ NxtLikeConsensusBlockData(header.baseTarget, ByteStr(header.generationSignature.toByteArray)),
+ transactions,
+ header.featureVotes.map(intToShort).toSet,
+ PublicKeyAccount(header.generator.toByteArray),
+ ByteStr(signedHeader.signature.toByteArray)
+ )
+ } yield result
+ }
+
+ def protobuf(block: VanillaBlock): PBBlock = {
+ import block._
+ import consensusData._
+ import signerData._
+
+ new PBBlock(
+ ByteString.EMPTY,
+ Some(
+ PBBlock.SignedHeader(
+ Some(PBBlock.Header(
+ ByteString.copyFrom(reference),
+ baseTarget,
+ ByteString.copyFrom(generationSignature),
+ featureVotes.map(shortToInt).toSeq,
+ timestamp,
+ version,
+ ByteString.copyFrom(generator.publicKey)
+ )),
+ ByteString.copyFrom(signature)
+ )),
+ transactionData.map(PBTransactions.protobuf)
+ )
+ }
+
+ private[this] def shortToInt(s: Short): Int = {
+ java.lang.Short.toUnsignedInt(s)
+ }
+
+ private[this] def intToShort(int: Int): Short = {
+ require(int >= 0 && int <= 65535, s"Short overflow: $int")
+ int.toShort
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/block.scala b/src/main/scala/com/zbsnetwork/protobuf/block/block.scala
new file mode 100644
index 0000000..5acff06
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/block/block.scala
@@ -0,0 +1,9 @@
+package com.zbsnetwork.protobuf
+
+package object block {
+ type PBBlock = com.zbsnetwork.protobuf.block.Block
+ val PBBlock = com.zbsnetwork.protobuf.block.Block
+
+ type VanillaBlock = com.zbsnetwork.block.Block
+ val VanillaBlock = com.zbsnetwork.block.Block
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala
new file mode 100644
index 0000000..b78ae93
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala
@@ -0,0 +1,59 @@
+package com.zbsnetwork.protobuf.transaction
+import com.google.protobuf.ByteString
+import com.zbsnetwork.account.PublicKeyAccount
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.transaction.assets.exchange.{OrderV1, OrderV2}
+import com.zbsnetwork.{transaction => vt}
+
+object PBOrders {
+ import com.zbsnetwork.protobuf.utils.PBInternalImplicits._
+
+ def vanilla(order: PBOrder, version: Int = 0): VanillaOrder = {
+ VanillaOrder(
+ PublicKeyAccount(order.senderPublicKey.toByteArray),
+ PublicKeyAccount(order.matcherPublicKey.toByteArray),
+ vt.assets.exchange.AssetPair(Some(order.getAssetPair.amountAssetId.toByteArray), Some(order.getAssetPair.priceAssetId.toByteArray)),
+ order.orderSide match {
+ case PBOrder.Side.BUY => vt.assets.exchange.OrderType.BUY
+ case PBOrder.Side.SELL => vt.assets.exchange.OrderType.SELL
+ case PBOrder.Side.Unrecognized(v) => throw new IllegalArgumentException(s"Unknown order type: $v")
+ },
+ order.amount,
+ order.price,
+ order.timestamp,
+ order.expiration,
+ order.getMatcherFee.longAmount,
+ order.proofs.map(_.toByteArray: ByteStr),
+ if (version == 0) order.version.toByte else version.toByte
+ )
+ }
+
+ def vanillaV1(order: PBOrder): OrderV1 = vanilla(order, 1) match {
+ case v1: OrderV1 => v1
+ case _ => ???
+ }
+
+ def vanillaV2(order: PBOrder): OrderV2 = vanilla(order, 2) match {
+ case v1: OrderV2 => v1
+ case _ => ???
+ }
+
+ def protobuf(order: VanillaOrder): PBOrder = {
+ PBOrder(
+ ByteString.copyFrom(order.senderPublicKey.publicKey),
+ ByteString.copyFrom(order.matcherPublicKey.publicKey),
+ Some(PBOrder.AssetPair(order.assetPair.amountAsset.get, order.assetPair.priceAsset.get)),
+ order.orderType match {
+ case vt.assets.exchange.OrderType.BUY => PBOrder.Side.BUY
+ case vt.assets.exchange.OrderType.SELL => PBOrder.Side.SELL
+ },
+ order.amount,
+ order.price,
+ order.timestamp,
+ order.expiration,
+ Some((order.matcherFeeAssetId, order.matcherFee)),
+ order.version,
+ order.proofs.map(bs => bs: ByteString)
+ )
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala
new file mode 100644
index 0000000..f7d8eb8
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala
@@ -0,0 +1,24 @@
+package com.zbsnetwork.protobuf.transaction
+import com.google.protobuf.CodedOutputStream
+import com.zbsnetwork.protobuf.utils.PBUtils
+
+private[transaction] object PBTransactionSerialization {
+ def signedBytes(tx: PBSignedTransaction): Array[Byte] = {
+ val outArray = new Array[Byte](tx.serializedSize + 2)
+ val outputStream = CodedOutputStream.newInstance(outArray)
+ outputStream.useDeterministicSerialization()
+
+ outputStream.write(0xff.toByte)
+ outputStream.write(0x01.toByte)
+ tx.writeTo(outputStream)
+
+ outputStream.flush()
+ outputStream.checkNoSpaceLeft()
+
+ outArray
+ }
+
+ def unsignedBytes(unsignedTx: PBTransaction): Array[Byte] = {
+ PBUtils.encodeDeterministic(unsignedTx)
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala
new file mode 100644
index 0000000..d056a41
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala
@@ -0,0 +1,426 @@
+package com.zbsnetwork.protobuf.transaction
+import com.google.protobuf.ByteString
+import com.zbsnetwork.account.{Address, PublicKeyAccount}
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.{BuySellOrders, Orders}
+import com.zbsnetwork.protobuf.transaction.Transaction.Data
+import com.zbsnetwork.protobuf.transaction.smart.script.{Script => PBScript}
+import com.zbsnetwork.state.{BinaryDataEntry, BooleanDataEntry, IntegerDataEntry, StringDataEntry}
+import com.zbsnetwork.transaction.ValidationError.GenericError
+import com.zbsnetwork.transaction.smart.script.ScriptReader
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer
+import com.zbsnetwork.transaction.{Proofs, ValidationError}
+import com.zbsnetwork.{transaction => vt}
+
+object PBTransactions {
+ import com.zbsnetwork.protobuf.utils.PBInternalImplicits._
+
+ private[this] val NoChainId: Byte = 0: Byte
+ private[this] val NoAssetId = ByteStr.empty
+
+ def create(
+ sender: com.zbsnetwork.account.PublicKeyAccount = PublicKeyAccount.empty,
+ chainId: Byte = 0,
+ fee: Long = 0L,
+ feeAssetId: VanillaAssetId = ByteStr.empty,
+ timestamp: Long = 0L,
+ version: Int = 0,
+ proofsArray: Seq[com.zbsnetwork.common.state.ByteStr] = Nil,
+ data: com.zbsnetwork.protobuf.transaction.Transaction.Data = com.zbsnetwork.protobuf.transaction.Transaction.Data.Empty): SignedTransaction = {
+ new SignedTransaction(
+ Some(Transaction(chainId, sender.publicKey: ByteStr, Some((feeAssetId, fee): Amount), timestamp, version, data)),
+ proofsArray.map(bs => ByteString.copyFrom(bs.arr))
+ )
+ }
+
+ def vanilla(signedTx: PBSignedTransaction): Either[ValidationError, VanillaTransaction] = {
+ def toAmountAndAssetId(amount: Amount): Either[ValidationError, (Long, VanillaAssetId)] = amount.amount match {
+ case Amount.Amount.ZbsAmount(value) => Right((value, ByteStr.empty))
+ case Amount.Amount.AssetAmount(AssetAmount(assetId, amount)) => Right((amount, ByteStr(assetId.toByteArray)))
+ case Amount.Amount.Empty => Left(GenericError("Empty amount"))
+ }
+
+ for {
+ parsedTx <- signedTx.transaction.toRight(GenericError("Transaction must be specified"))
+ fee <- parsedTx.fee.toRight(GenericError("Fee must be specified"))
+ _ <- Either.cond(parsedTx.data.isDefined, (), GenericError("Transaction data must be specified"))
+ feeAmount <- toAmountAndAssetId(fee)
+ sender = PublicKeyAccount(parsedTx.senderPublicKey.toByteArray)
+ tx <- createVanilla(
+ parsedTx.version,
+ if (parsedTx.chainId.isEmpty) NoChainId else parsedTx.chainId.byteAt(0),
+ sender,
+ feeAmount._1,
+ Option(feeAmount._2).filterNot(_.isEmpty),
+ parsedTx.timestamp,
+ Proofs(signedTx.proofs.map(bs => ByteStr(bs.toByteArray))),
+ parsedTx.data
+ )
+ } yield tx
+ }
+
+ private[this] def createVanilla(version: Int,
+ chainId: Byte,
+ sender: PublicKeyAccount,
+ feeAmount: Long,
+ feeAssetId: Option[ByteStr],
+ timestamp: Long,
+ proofs: Proofs,
+ data: PBTransaction.Data): Either[ValidationError, VanillaTransaction] = {
+
+ val signature = proofs.toSignature
+ val result: Either[ValidationError, VanillaTransaction] = data match {
+ case Data.Genesis(GenesisTransactionData(recipient, amount)) =>
+ vt.GenesisTransaction.create(Address.fromBytes(recipient.toByteArray).right.get, amount, timestamp)
+
+ case Data.Payment(PaymentTransactionData(recipient, amount)) =>
+ vt.PaymentTransaction.create(sender, Address.fromBytes(recipient.toByteArray).right.get, amount, feeAmount, timestamp, signature)
+
+ case Data.Transfer(TransferTransactionData(Some(recipient), Some(amount), attachment)) =>
+ version match {
+ case 1 =>
+ for {
+ address <- recipient.toAddressOrAlias
+ tx <- vt.transfer.TransferTransactionV1.create(
+ amount.assetId,
+ sender,
+ address,
+ amount.longAmount,
+ timestamp,
+ feeAssetId,
+ feeAmount,
+ attachment.toByteArray,
+ signature
+ )
+ } yield tx
+
+ case 2 =>
+ for {
+ address <- recipient.toAddressOrAlias
+ tx <- vt.transfer.TransferTransactionV2.create(
+ amount.assetId,
+ sender,
+ address,
+ amount.longAmount,
+ timestamp,
+ feeAssetId,
+ feeAmount,
+ attachment.toByteArray,
+ proofs
+ )
+ } yield tx
+
+ case v =>
+ throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.CreateAlias(CreateAliasTransactionData(alias)) =>
+ version match {
+ case 1 =>
+ for {
+ alias <- com.zbsnetwork.account.Alias.buildAlias(chainId, alias)
+ tx <- vt.CreateAliasTransactionV1.create(sender, alias, feeAmount, timestamp, signature)
+ } yield tx
+
+ case 2 =>
+ for {
+ alias <- com.zbsnetwork.account.Alias.buildAlias(chainId, alias)
+ tx <- vt.CreateAliasTransactionV2.create(sender, alias, feeAmount, timestamp, proofs)
+ } yield tx
+
+ case v =>
+ throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.Issue(IssueTransactionData(name, description, quantity, decimals, reissuable, script)) =>
+ version match {
+ case 1 =>
+ vt.assets.IssueTransactionV1.create(
+ sender,
+ name.toByteArray,
+ description.toByteArray,
+ quantity,
+ decimals.toByte,
+ reissuable,
+ feeAmount,
+ timestamp,
+ signature
+ )
+ case 2 =>
+ vt.assets.IssueTransactionV2.create(
+ chainId,
+ sender,
+ name.toByteArray,
+ description.toByteArray,
+ quantity,
+ decimals.toByte,
+ reissuable,
+ script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get),
+ feeAmount,
+ timestamp,
+ proofs
+ )
+ case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.Reissue(ReissueTransactionData(Some(AssetAmount(assetId, amount)), reissuable)) =>
+ version match {
+ case 1 =>
+ vt.assets.ReissueTransactionV1.create(sender, ByteStr(assetId.toByteArray), amount, reissuable, feeAmount, timestamp, signature)
+ case 2 =>
+ vt.assets.ReissueTransactionV2.create(chainId, sender, assetId, amount, reissuable, feeAmount, timestamp, proofs)
+ case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.Burn(BurnTransactionData(Some(AssetAmount(assetId, amount)))) =>
+ version match {
+ case 1 => vt.assets.BurnTransactionV1.create(sender, assetId, amount, feeAmount, timestamp, signature)
+ case 2 => vt.assets.BurnTransactionV2.create(chainId, sender, assetId, amount, feeAmount, timestamp, proofs)
+ case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.SetAssetScript(SetAssetScriptTransactionData(assetId, script)) =>
+ vt.assets.SetAssetScriptTransaction.create(
+ chainId,
+ sender,
+ assetId,
+ script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get),
+ feeAmount,
+ timestamp,
+ proofs
+ )
+
+ case Data.SetScript(SetScriptTransactionData(script)) =>
+ vt.smart.SetScriptTransaction.create(
+ sender,
+ script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get),
+ feeAmount,
+ timestamp,
+ proofs
+ )
+
+ case Data.Lease(LeaseTransactionData(Some(recipient), amount)) =>
+ version match {
+ case 1 =>
+ for {
+ address <- recipient.toAddressOrAlias
+ tx <- vt.lease.LeaseTransactionV1.create(sender, amount, feeAmount, timestamp, address, signature)
+ } yield tx
+
+ case 2 =>
+ for {
+ address <- recipient.toAddressOrAlias
+ tx <- vt.lease.LeaseTransactionV2.create(sender, amount, feeAmount, timestamp, address, proofs)
+ } yield tx
+
+ case v =>
+ throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.LeaseCancel(LeaseCancelTransactionData(leaseId)) =>
+ version match {
+ case 1 => vt.lease.LeaseCancelTransactionV1.create(sender, leaseId.byteStr, feeAmount, timestamp, signature)
+ case 2 => vt.lease.LeaseCancelTransactionV2.create(chainId, sender, leaseId.toByteArray, feeAmount, timestamp, proofs)
+ case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.Exchange(
+ ExchangeTransactionData(amount,
+ price,
+ buyMatcherFee,
+ sellMatcherFee,
+ Orders.BuySellOrders(BuySellOrders(Some(buyOrder), Some(sellOrder))))) =>
+ version match {
+ case 1 =>
+ vt.assets.exchange.ExchangeTransactionV1.create(
+ PBOrders.vanillaV1(buyOrder),
+ PBOrders.vanillaV1(sellOrder),
+ amount,
+ price,
+ buyMatcherFee,
+ sellMatcherFee,
+ feeAmount,
+ timestamp,
+ signature
+ )
+ case 2 =>
+ vt.assets.exchange.ExchangeTransactionV2.create(PBOrders.vanilla(buyOrder),
+ PBOrders.vanilla(sellOrder),
+ amount,
+ price,
+ buyMatcherFee,
+ sellMatcherFee,
+ feeAmount,
+ timestamp,
+ proofs)
+ case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v")
+ }
+
+ case Data.DataTransaction(DataTransactionData(data)) =>
+ import DataTransactionData.DataEntry.Value._
+ val entries = data.toList.map { de =>
+ de.value match {
+ case IntValue(num) => IntegerDataEntry(de.key, num)
+ case BoolValue(bool) => BooleanDataEntry(de.key, bool)
+ case BinaryValue(bytes) => BinaryDataEntry(de.key, bytes.toByteArray)
+ case StringValue(str) => StringDataEntry(de.key, str)
+ case Empty => throw new IllegalArgumentException(s"Empty entries not supported: $data")
+ }
+ }
+ vt.DataTransaction.create(
+ sender,
+ entries,
+ feeAmount,
+ timestamp,
+ proofs
+ )
+
+ case Data.MassTransfer(MassTransferTransactionData(assetId, transfers, attachment)) =>
+ vt.transfer.MassTransferTransaction.create(
+ Some(assetId.toByteArray: ByteStr).filterNot(_.isEmpty),
+ sender,
+ transfers.flatMap(t => t.getAddress.toAddressOrAlias.toOption.map(ParsedTransfer(_, t.amount))).toList,
+ timestamp,
+ feeAmount,
+ attachment.toByteArray,
+ proofs
+ )
+
+ case Data.SponsorFee(SponsorFeeTransactionData(Some(AssetAmount(assetId, minFee)))) =>
+ vt.assets.SponsorFeeTransaction.create(sender, assetId.toByteArray, Option(minFee).filter(_ > 0), feeAmount, timestamp, proofs)
+
+ case data =>
+ throw new IllegalArgumentException(s"Unsupported transaction data: $data")
+ }
+
+ result
+ }
+
+ def protobuf(tx: VanillaTransaction): PBSignedTransaction = {
+ tx match {
+ // Uses version "2" for "modern" transactions with single version and proofs field
+ case vt.GenesisTransaction(recipient, amount, timestamp, signature) =>
+ val data = GenesisTransactionData(ByteString.copyFrom(recipient.bytes), amount)
+ PBTransactions.create(sender = PublicKeyAccount(Array.emptyByteArray), timestamp = timestamp, version = 1, data = Data.Genesis(data))
+
+ case vt.PaymentTransaction(sender, recipient, amount, fee, timestamp, signature) =>
+ val data = PaymentTransactionData(ByteString.copyFrom(recipient.bytes), amount)
+ PBTransactions.create(sender, NoChainId, fee, NoAssetId, timestamp, 1, Seq(signature), Data.Payment(data))
+
+ case vt.transfer.TransferTransactionV1(assetId, sender, recipient, amount, timestamp, feeAssetId, fee, attachment, signature) =>
+ val data = TransferTransactionData(Some(recipient), Some((assetId, amount)), ByteString.copyFrom(attachment))
+ PBTransactions.create(sender, NoChainId, fee, feeAssetId, timestamp, 1, Seq(signature), Data.Transfer(data))
+
+ case vt.transfer.TransferTransactionV2(sender, recipient, assetId, amount, timestamp, feeAssetId, fee, attachment, proofs) =>
+ val data = TransferTransactionData(Some(recipient), Some((assetId, amount)), ByteString.copyFrom(attachment))
+ PBTransactions.create(sender, NoChainId, fee, feeAssetId, timestamp, 2, proofs, Data.Transfer(data))
+
+ case tx @ vt.CreateAliasTransactionV1(sender, alias, fee, timestamp, signature) =>
+ val data = CreateAliasTransactionData(alias.name)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.CreateAlias(data))
+
+ case tx @ vt.CreateAliasTransactionV2(sender, alias, fee, timestamp, proofs) =>
+ val data = CreateAliasTransactionData(alias.name)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.CreateAlias(data))
+
+ case tx @ vt.assets.exchange
+ .ExchangeTransactionV1(buyOrder, sellOrder, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, signature) =>
+ val data = ExchangeTransactionData(
+ amount,
+ price,
+ buyMatcherFee,
+ sellMatcherFee,
+ Orders.BuySellOrders(BuySellOrders(Some(PBOrders.protobuf(buyOrder)), Some(PBOrders.protobuf(sellOrder))))
+ )
+ PBTransactions.create(tx.sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Exchange(data))
+
+ case tx @ vt.assets.exchange.ExchangeTransactionV2(buyOrder, sellOrder, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs) =>
+ val data = ExchangeTransactionData(
+ amount,
+ price,
+ buyMatcherFee,
+ sellMatcherFee,
+ Orders.BuySellOrders(BuySellOrders(Some(PBOrders.protobuf(buyOrder)), Some(PBOrders.protobuf(sellOrder))))
+ )
+ PBTransactions.create(tx.sender, 0: Byte, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Exchange(data))
+
+ case vt.assets.IssueTransactionV1(sender, name, description, quantity, decimals, reissuable, fee, timestamp, signature) =>
+ val data = IssueTransactionData(ByteStr(name), ByteStr(description), quantity, decimals, reissuable, None)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Issue(data))
+
+ case vt.assets.IssueTransactionV2(chainId, sender, name, description, quantity, decimals, reissuable, script, fee, timestamp, proofs) =>
+ val data = IssueTransactionData(ByteStr(name), ByteStr(description), quantity, decimals, reissuable, script.map(s => PBScript(s.bytes())))
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Issue(data))
+
+ case tx @ vt.assets.ReissueTransactionV1(sender, assetId, quantity, reissuable, fee, timestamp, signature) =>
+ val data = ReissueTransactionData(Some(AssetAmount(assetId, quantity)), reissuable)
+ PBTransactions.create(sender, tx.chainByte.getOrElse(NoChainId), fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Reissue(data))
+
+ case tx @ vt.assets.ReissueTransactionV2(chainId, sender, assetId, amount, reissuable, fee, timestamp, proofs) =>
+ val data = ReissueTransactionData(Some(AssetAmount(assetId, amount)), reissuable)
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Reissue(data))
+
+ case tx @ vt.assets.BurnTransactionV1(sender, assetId, amount, fee, timestamp, signature) =>
+ val data = BurnTransactionData(Some(AssetAmount(assetId, amount)))
+ PBTransactions.create(sender, tx.chainByte.getOrElse(NoChainId), fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Burn(data))
+
+ case tx @ vt.assets.BurnTransactionV2(chainId, sender, assetId, amount, fee, timestamp, proofs) =>
+ val data = BurnTransactionData(Some(AssetAmount(assetId, amount)))
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Burn(data))
+
+ case vt.assets.SetAssetScriptTransaction(chainId, sender, assetId, script, fee, timestamp, proofs) =>
+ val data = SetAssetScriptTransactionData(assetId, script.map(s => PBScript(s.bytes())))
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SetAssetScript(data))
+
+ case vt.smart.SetScriptTransaction(chainId, sender, script, fee, timestamp, proofs) =>
+ val data = SetScriptTransactionData(script.map(s => PBScript(s.bytes())))
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SetScript(data))
+
+ case tx @ vt.lease.LeaseTransactionV1(sender, amount, fee, timestamp, recipient, signature) =>
+ val data = LeaseTransactionData(Some(recipient), amount)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Lease(data))
+
+ case tx @ vt.lease.LeaseTransactionV2(sender, amount, fee, timestamp, recipient, proofs) =>
+ val data = LeaseTransactionData(Some(recipient), amount)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Lease(data))
+
+ case tx @ vt.lease.LeaseCancelTransactionV1(sender, leaseId, fee, timestamp, signature) =>
+ val data = LeaseCancelTransactionData(leaseId)
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.LeaseCancel(data))
+
+ case tx @ vt.lease.LeaseCancelTransactionV2(chainId, sender, leaseId, fee, timestamp, proofs) =>
+ val data = LeaseCancelTransactionData(leaseId)
+ PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.LeaseCancel(data))
+
+ case tx @ MassTransferTransaction(assetId, sender, transfers, timestamp, fee, attachment, proofs) =>
+ val data = MassTransferTransactionData(
+ ByteString.copyFrom(assetId.getOrElse(ByteStr.empty)),
+ transfers.map(pt => MassTransferTransactionData.Transfer(Some(pt.address), pt.amount)),
+ attachment: ByteStr
+ )
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.MassTransfer(data))
+
+ case tx @ vt.DataTransaction(sender, data, fee, timestamp, proofs) =>
+ val txData = DataTransactionData(
+ data.map(de =>
+ DataTransactionData.DataEntry(
+ de.key,
+ de match {
+ case IntegerDataEntry(_, value) => DataTransactionData.DataEntry.Value.IntValue(value)
+ case BooleanDataEntry(_, value) => DataTransactionData.DataEntry.Value.BoolValue(value)
+ case BinaryDataEntry(_, value) => DataTransactionData.DataEntry.Value.BinaryValue(value)
+ case StringDataEntry(_, value) => DataTransactionData.DataEntry.Value.StringValue(value)
+ }
+ )))
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.DataTransaction(txData))
+
+ case tx @ vt.assets.SponsorFeeTransaction(sender, assetId, minSponsoredAssetFee, fee, timestamp, proofs) =>
+ val data = SponsorFeeTransactionData(Some(AssetAmount(assetId, minSponsoredAssetFee.getOrElse(0L))))
+ PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SponsorFee(data))
+
+ case _ =>
+ throw new IllegalArgumentException(s"Unsupported transaction: $tx")
+ }
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala
new file mode 100644
index 0000000..5e32759
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala
@@ -0,0 +1,22 @@
+package com.zbsnetwork.protobuf
+
+package object transaction {
+ type PBOrder = com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.Order
+ val PBOrder = com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.Order
+
+ type VanillaOrder = com.zbsnetwork.transaction.assets.exchange.Order
+ val VanillaOrder = com.zbsnetwork.transaction.assets.exchange.Order
+
+ type PBTransaction = com.zbsnetwork.protobuf.transaction.Transaction
+ val PBTransaction = com.zbsnetwork.protobuf.transaction.Transaction
+
+ type PBSignedTransaction = com.zbsnetwork.protobuf.transaction.SignedTransaction
+ val PBSignedTransaction = com.zbsnetwork.protobuf.transaction.SignedTransaction
+
+ type VanillaTransaction = com.zbsnetwork.transaction.Transaction
+ val VanillaTransaction = com.zbsnetwork.transaction.Transaction
+
+ type VanillaSignedTransaction = com.zbsnetwork.transaction.SignedTransaction
+
+ type VanillaAssetId = com.zbsnetwork.transaction.AssetId
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala b/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala
new file mode 100644
index 0000000..da11777
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala
@@ -0,0 +1,86 @@
+package com.zbsnetwork.protobuf.utils
+import com.google.protobuf.ByteString
+import com.zbsnetwork.account.PublicKeyAccount
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.protobuf.account.{Alias, Recipient}
+import com.zbsnetwork.protobuf.transaction.{Amount, AssetAmount, VanillaAssetId}
+import com.zbsnetwork.transaction.ValidationError
+
+private[protobuf] object PBInternalImplicits {
+ import com.google.protobuf.{ByteString => PBByteString}
+ import com.zbsnetwork.account.{AddressOrAlias, Address => VAddress, Alias => VAlias}
+
+ implicit def byteStringToByteStr(bs: PBByteString): ByteStr = bs.toByteArray
+ implicit def byteStrToByteString(bs: ByteStr): PBByteString = PBByteString.copyFrom(bs)
+
+ implicit def fromAddressOrAlias(addressOrAlias: AddressOrAlias): Recipient = addressOrAlias match {
+ case a: VAddress => fromAddress(a)
+ case al: VAlias => fromAlias(al)
+ }
+
+ implicit def fromAddress(address: VAddress): Recipient = {
+ Recipient.defaultInstance.withAddress(address.bytes)
+ }
+
+ implicit def fromAlias(alias: VAlias): Recipient = {
+ Recipient.defaultInstance.withAlias(Alias(alias.chainId: Byte, alias.name))
+ }
+
+ implicit class PBRecipientImplicitConversionOps(recipient: Recipient) {
+ def toAddress: Either[ValidationError, VAddress] = {
+ VAddress.fromBytes(recipient.getAddress.toByteArray)
+ }
+
+ def toAlias: Either[ValidationError, VAlias] = {
+ val alias = recipient.getAlias
+ VAlias.buildAlias(if (alias.chainId.isEmpty) 0: Byte else alias.chainId.byteAt(0), alias.name)
+ }
+
+ def toAddressOrAlias: Either[ValidationError, AddressOrAlias] = recipient.recipient match {
+ case Recipient.Recipient.Alias(_) => this.toAlias
+ case Recipient.Recipient.Address(_) => this.toAddress
+ case Recipient.Recipient.Empty => throw new IllegalArgumentException("Empty address not supported")
+ }
+ }
+
+ implicit def fromAssetIdOptionAndAmount(v: (Option[VanillaAssetId], Long)): Amount = v match {
+ case (Some(assetId), amount) =>
+ Amount.defaultInstance.withAssetAmount(AssetAmount(assetId, amount))
+
+ case (None, amount) =>
+ Amount.defaultInstance.withZbsAmount(amount)
+ }
+
+ implicit def fromAssetIdAndAmount(v: (VanillaAssetId, Long)): Amount = {
+ fromAssetIdOptionAndAmount((Option(v._1).filterNot(_.isEmpty), v._2))
+ }
+
+ implicit class AmountImplicitConversions(a: Amount) {
+ def longAmount: Long = a.amount match {
+ case Amount.Amount.Empty => 0L
+ case Amount.Amount.ZbsAmount(value) => value
+ case Amount.Amount.AssetAmount(value) => value.amount
+ }
+
+ def assetId: ByteStr = a.amount match {
+ case Amount.Amount.ZbsAmount(_) | Amount.Amount.Empty => ByteStr.empty
+ case Amount.Amount.AssetAmount(AssetAmount(assetId, _)) => ByteStr(assetId.toByteArray)
+ }
+ }
+
+ implicit class PBByteStringOps(bs: PBByteString) {
+ def byteStr = ByteStr(bs.toByteArray)
+ def publicKeyAccount = PublicKeyAccount(bs.toByteArray)
+ }
+
+ implicit def byteStringToByte(bytes: ByteString): Byte =
+ if (bytes.isEmpty) 0
+ else bytes.byteAt(0)
+
+ implicit def byteToByteString(chainId: Byte): ByteString = {
+ if (chainId == 0) ByteString.EMPTY else ByteString.copyFrom(Array(chainId))
+ }
+
+ implicit def assetIdToAssetIdOption(assetId: VanillaAssetId): Option[VanillaAssetId] = Option(assetId).filterNot(_.isEmpty)
+ implicit def assetIdOptionToAssetId(assetId: Option[VanillaAssetId]): VanillaAssetId = assetId.getOrElse(ByteStr.empty)
+}
diff --git a/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala b/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala
new file mode 100644
index 0000000..c2dc99c
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala
@@ -0,0 +1,14 @@
+package com.zbsnetwork.protobuf.utils
+import com.google.protobuf.CodedOutputStream
+import scalapb.GeneratedMessage
+
+object PBUtils {
+ def encodeDeterministic(msg: GeneratedMessage): Array[Byte] = {
+ val outArray = new Array[Byte](msg.serializedSize)
+ val outputStream = CodedOutputStream.newInstance(outArray)
+ outputStream.useDeterministicSerialization() // Adds this
+ msg.writeTo(outputStream)
+ outputStream.checkNoSpaceLeft()
+ outArray
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/serialization/Deser.scala b/src/main/scala/com/zbsnetwork/serialization/Deser.scala
index 7ac2d09..ba44c17 100644
--- a/src/main/scala/com/zbsnetwork/serialization/Deser.scala
+++ b/src/main/scala/com/zbsnetwork/serialization/Deser.scala
@@ -6,7 +6,13 @@ object Deser {
def serializeBoolean(b: Boolean): Array[Byte] = if (b) Array(1: Byte) else Array(0: Byte)
- def serializeArray(b: Array[Byte]): Array[Byte] = Shorts.toByteArray(b.length.toShort) ++ b
+ def serializeArray(b: Array[Byte]): Array[Byte] = {
+ val length = b.length
+ if (length.isValidShort)
+ Shorts.toByteArray(length.toShort) ++ b
+ else
+ throw new IllegalArgumentException(s"Attempting to serialize array with size, but the size($length) exceeds MaxShort(${Short.MaxValue})")
+ }
def parseArraySize(bytes: Array[Byte], position: Int): (Array[Byte], Int) = {
val length = Shorts.fromByteArray(bytes.slice(position, position + 2))
diff --git a/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala b/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala
index 5649d60..4e49875 100644
--- a/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala
+++ b/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala
@@ -52,17 +52,17 @@ object FunctionalitySettings {
val MAINNET = apply(
featureCheckBlocksPeriod = 5000,
blocksForFeatureActivation = 4000,
- allowTemporaryNegativeUntil = 1479168000000L,
- generationBalanceDepthFrom50To1000AfterHeight = 232000,
- minimalGeneratingBalanceAfter = 1479168000000L,
- allowTransactionsFromFutureUntil = 1479168000000L,
- allowUnissuedAssetsUntil = 1479416400000L,
- allowInvalidReissueInSameBlockUntilTimestamp = 1492768800000L,
- allowMultipleLeaseCancelTransactionUntilTimestamp = 1492768800000L,
- resetEffectiveBalancesAtHeight = 462000,
- blockVersion3AfterHeight = 795000,
- preActivatedFeatures = Map.empty,
- doubleFeaturesPeriodsAfterHeight = 810000,
+ allowTemporaryNegativeUntil = 0,
+ generationBalanceDepthFrom50To1000AfterHeight = 0,
+ minimalGeneratingBalanceAfter = 0,
+ allowTransactionsFromFutureUntil = 0,
+ allowUnissuedAssetsUntil = 0,
+ allowInvalidReissueInSameBlockUntilTimestamp = 0,
+ allowMultipleLeaseCancelTransactionUntilTimestamp = 0,
+ resetEffectiveBalancesAtHeight = 1,
+ blockVersion3AfterHeight = 0,
+ preActivatedFeatures = Map[Short, Int]((1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0), (9, 0), (10, 0), (11, 0)),
+ doubleFeaturesPeriodsAfterHeight = 0,
maxTransactionTimeBackOffset = 120.minutes,
maxTransactionTimeForwardOffset = 90.minutes
)
@@ -100,13 +100,16 @@ case class GenesisSettings(blockTimestamp: Long,
object GenesisSettings {
val MAINNET = GenesisSettings(
- 1538689931932L,
- 1478000000000L,
+ 1550685686495L,
+ 1550685686495L,
Constants.UnitsInZbs * Constants.TotalZbs,
- ByteStr.decodeBase58("64dMbqe7XGomp7XU2SqfHGhUqGSwiQLD3TJCC4WMYPTAjkh2bprZJm9YT3mRxMqfMH5DwvnhdeyAm2FnUXYnUtm1").toOption,
+ ByteStr.decodeBase58("45c4qsxnnvdj1MNTK1DaBbs4mjuuQACbx46NxFR3URxp836oyhyvEChtVnRU2c5oF635tGVstZWrKaKg4Sp3EYNW").toOption,
List(
- GenesisTransactionSettings("3Qbnb1eSHqztPMvR7qHhEQoGn6HAcLRe4Yz", (Constants.UnitsInZbs * Constants.TotalZbs * 0.5).toLong),
- GenesisTransactionSettings("3QE1Hju1y8CS3efwYwfikjUyABUd9BNfudc", (Constants.UnitsInZbs * Constants.TotalZbs * 0.5).toLong),
+ GenesisTransactionSettings("3QMdCYWmaSVHSNtvrWwYdshG9wekjekRxDa", 2549000000000000L),
+ GenesisTransactionSettings("3QNkA6HHfJ6vBenz41NpKsobe9WVgx7KfzQ", 1800000000000L),
+ GenesisTransactionSettings("3QcmkoyzMdTs6fhS8kiwttzwZjEtKtNdTiw", 2549000000000000L),
+ GenesisTransactionSettings("3QH6unL8QUpT9ptnonC9Eo6PuXBT65VW66D", 100000000000L),
+ GenesisTransactionSettings("3QY1vQeGyZ6sSeNRmw3wQQ33Yrkwuia9fJW", 100000000000L),
),
153722867L,
60.seconds
diff --git a/src/main/scala/com/zbsnetwork/settings/Constants.scala b/src/main/scala/com/zbsnetwork/settings/Constants.scala
index 55c75d5..8e5d534 100644
--- a/src/main/scala/com/zbsnetwork/settings/Constants.scala
+++ b/src/main/scala/com/zbsnetwork/settings/Constants.scala
@@ -11,5 +11,5 @@ object Constants extends ScorexLogging {
val AgentName = s"Zbs v${Version.VersionString}"
val UnitsInZbs = 100000000L
- val TotalZbs = 100000000L
+ val TotalZbs = 51000000L
}
diff --git a/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala b/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala
index c50deef..393e990 100644
--- a/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala
+++ b/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala
@@ -26,7 +26,13 @@ object SynchronizationSettings {
case class HistoryReplierSettings(maxMicroBlockCacheSize: Int, maxBlockCacheSize: Int)
- case class UtxSynchronizerSettings(networkTxCacheSize: Int, networkTxCacheTime: FiniteDuration, maxBufferSize: Int, maxBufferTime: FiniteDuration)
+ case class UtxSynchronizerSettings(networkTxCacheSize: Int,
+ networkTxCacheTime: FiniteDuration,
+ maxBufferSize: Int,
+ maxBufferTime: FiniteDuration,
+ parallelism: Int,
+ maxThreads: Int,
+ maxQueueSize: Int)
val configPath: String = "zbs.synchronization"
diff --git a/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala b/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala
index 59a30f3..bb4bb25 100644
--- a/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala
+++ b/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala
@@ -1,10 +1,7 @@
package com.zbsnetwork.settings
-import scala.concurrent.duration.FiniteDuration
-
case class UtxSettings(maxSize: Int,
maxBytesSize: Long,
blacklistSenderAddresses: Set[String],
allowBlacklistedTransferTo: Set[String],
- cleanupInterval: FiniteDuration,
allowTransactionsFromSmartAccounts: Boolean)
diff --git a/src/main/scala/com/zbsnetwork/state/Blockchain.scala b/src/main/scala/com/zbsnetwork/state/Blockchain.scala
index a853bc7..bfb1ae4 100644
--- a/src/main/scala/com/zbsnetwork/state/Blockchain.scala
+++ b/src/main/scala/com/zbsnetwork/state/Blockchain.scala
@@ -71,14 +71,14 @@ trait Blockchain {
def leaseBalance(address: Address): LeaseBalance
- def balance(address: Address, mayBeAssetId: Option[AssetId]): Long
+ def balance(address: Address, mayBeAssetId: Option[AssetId] = None): Long
def assetDistribution(assetId: ByteStr): AssetDistribution
def assetDistributionAtHeight(assetId: AssetId,
height: Int,
count: Int,
fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage]
- def zbsDistribution(height: Int): Map[Address, Long]
+ def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]]
// the following methods are used exclusively by patches
def allActiveLeases: Set[LeaseTransaction]
diff --git a/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala b/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala
index 2b42e0c..a66d916 100644
--- a/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala
+++ b/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala
@@ -1,6 +1,9 @@
package com.zbsnetwork.state
+import java.util.concurrent.locks.{Lock, ReentrantReadWriteLock}
+
import cats.implicits._
+import cats.kernel.Monoid
import com.zbsnetwork.account.{Address, Alias}
import com.zbsnetwork.block.Block.BlockId
import com.zbsnetwork.block.{Block, BlockHeader, MicroBlock}
@@ -23,7 +26,7 @@ import kamon.metric.MeasurementUnit
import monix.reactive.subjects.ConcurrentSubject
import monix.reactive.{Observable, Observer}
-class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[Address], settings: ZbsSettings, time: Time)
+class BlockchainUpdaterImpl(blockchain: Blockchain, spendableBalanceChanged: Observer[(Address, Option[AssetId])], settings: ZbsSettings, time: Time)
extends BlockchainUpdater
with NG
with ScorexLogging
@@ -32,6 +35,19 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
import com.zbsnetwork.state.BlockchainUpdaterImpl._
import settings.blockchainSettings.functionalitySettings
+ private def inLock[R](l: Lock, f: => R) = {
+ try {
+ l.lock()
+ val res = f
+ res
+ } finally {
+ l.unlock()
+ }
+ }
+ private val lock = new ReentrantReadWriteLock
+ private def writeLock[B](f: => B): B = inLock(lock.writeLock(), f)
+ private def readLock[B](f: => B): B = inLock(lock.readLock(), f)
+
private lazy val maxBlockReadinessAge = settings.minerSettings.intervalAfterLastBlockThenGenerationIsAllowed.toMillis
private var ngState: Option[NgState] = Option.empty
@@ -40,12 +56,14 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
private val service = monix.execution.Scheduler.singleThread("last-block-info-publisher")
private val internalLastBlockInfo = ConcurrentSubject.publish[LastBlockInfo](service)
- override def isLastBlockId(id: ByteStr): Boolean = ngState.exists(_.contains(id)) || lastBlock.exists(_.uniqueId == id)
+ override def isLastBlockId(id: ByteStr): Boolean = readLock {
+ ngState.exists(_.contains(id)) || lastBlock.exists(_.uniqueId == id)
+ }
override val lastBlockInfo: Observable[LastBlockInfo] = internalLastBlockInfo.cache(1)
lastBlockInfo.subscribe()(monix.execution.Scheduler.global) // Start caching
- def blockchainReady: Boolean = {
+ private def blockchainReady: Boolean = {
val lastBlock = ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp).get
lastBlock + maxBlockReadinessAge > time.correctedTime()
}
@@ -99,7 +117,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
}
}
- override def processBlock(block: Block, verify: Boolean = true): Either[ValidationError, Option[DiscardedTransactions]] = {
+ override def processBlock(block: Block, verify: Boolean = true): Either[ValidationError, Option[DiscardedTransactions]] = writeLock {
val height = blockchain.height
val notImplementedFeatures: Set[Short] = blockchain.activatedFeaturesAt(height).diff(BlockchainFeatures.implemented)
@@ -198,8 +216,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
restTotalConstraint = updatedTotalConstraint
val prevNgState = ngState
ngState = Some(new NgState(block, newBlockDiff, carry, featuresApprovedWithBlock(block)))
-
- prevNgState.toIterable.flatMap(_.bestLiquidDiff.portfolios.keys).foreach(portfolioChanged.onNext)
+ notifyChangedSpendable(prevNgState, ngState)
lastBlockId.foreach(id => internalLastBlockInfo.onNext(LastBlockInfo(id, height, score, blockchainReady)))
if ((block.timestamp > time
@@ -211,7 +228,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
})
}
- override def removeAfter(blockId: ByteStr): Either[ValidationError, Seq[Block]] = {
+ override def removeAfter(blockId: ByteStr): Either[ValidationError, Seq[Block]] = writeLock {
log.info(s"Removing blocks after ${blockId.trim} from blockchain")
val prevNgState = ngState
@@ -227,11 +244,29 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
.leftMap(err => GenericError(err))
}
- prevNgState.toIterable.flatMap(_.bestLiquidDiff.portfolios.keys).foreach(portfolioChanged.onNext)
+ notifyChangedSpendable(prevNgState, ngState)
r
}
- override def processMicroBlock(microBlock: MicroBlock, verify: Boolean = true): Either[ValidationError, Unit] = {
+ private def notifyChangedSpendable(prevNgState: Option[NgState], newNgState: Option[NgState]): Unit = {
+ val changedPortfolios = (prevNgState, newNgState) match {
+ case (Some(p), Some(n)) => diff(p.bestLiquidDiff.portfolios, n.bestLiquidDiff.portfolios)
+ case (Some(x), _) => x.bestLiquidDiff.portfolios
+ case (_, Some(x)) => x.bestLiquidDiff.portfolios
+ case _ => Map.empty
+ }
+
+ changedPortfolios.foreach {
+ case (addr, p) =>
+ p.assetIds.view
+ .filter(x => p.spendableBalanceOf(x) != 0)
+ .foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId))
+ }
+ }
+
+ private def diff(p1: Map[Address, Portfolio], p2: Map[Address, Portfolio]) = Monoid.combine(p1, p2.map { case (k, v) => k -> v.negate })
+
+ override def processMicroBlock(microBlock: MicroBlock, verify: Boolean = true): Either[ValidationError, Unit] = writeLock {
ngState match {
case None =>
Left(MicroBlockAppendError("No base block exists", microBlock))
@@ -265,7 +300,11 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
ng.append(microBlock, diff, carry, System.currentTimeMillis)
log.info(s"$microBlock appended")
internalLastBlockInfo.onNext(LastBlockInfo(microBlock.totalResBlockSig, height, score, ready = true))
- diff.portfolios.keys.foreach(portfolioChanged.onNext)
+
+ for {
+ (addr, p) <- diff.portfolios
+ assetId <- p.assetIds
+ } spendableBalanceChanged.onNext(addr -> assetId)
}
}
}
@@ -278,12 +317,15 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
private def newlyApprovedFeatures = ngState.fold(Map.empty[Short, Int])(_.approvedFeatures.map(_ -> height).toMap)
- override def approvedFeatures: Map[Short, Int] = newlyApprovedFeatures ++ blockchain.approvedFeatures
+ override def approvedFeatures: Map[Short, Int] = readLock {
+ newlyApprovedFeatures ++ blockchain.approvedFeatures
+ }
- override def activatedFeatures: Map[Short, Int] =
+ override def activatedFeatures: Map[Short, Int] = readLock {
newlyApprovedFeatures.mapValues(_ + functionalitySettings.activationWindowSize(height)) ++ blockchain.activatedFeatures
+ }
- override def featureVotes(height: Int): Map[Short, Int] = {
+ override def featureVotes(height: Int): Map[Short, Int] = readLock {
val innerVotes = blockchain.featureVotes(height)
ngState match {
case Some(ng) if this.height <= height =>
@@ -300,70 +342,92 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
(s.bestLiquidBlock, s.bestLiquidBlock.bytes().length)
}
- override def blockHeaderAndSize(blockId: BlockId): Option[(BlockHeader, Int)] =
+ override def blockHeaderAndSize(blockId: BlockId): Option[(BlockHeader, Int)] = readLock {
liquidBlockHeaderAndSize().filter(_._1.uniqueId == blockId) orElse blockchain.blockHeaderAndSize(blockId)
+ }
- override def height: Int = blockchain.height + ngState.fold(0)(_ => 1)
+ override def height: Int = readLock {
+ blockchain.height + ngState.fold(0)(_ => 1)
+ }
- override def blockBytes(height: Int): Option[Array[Byte]] =
+ override def blockBytes(height: Int): Option[Array[Byte]] = readLock {
blockchain
.blockBytes(height)
.orElse(ngState.collect { case ng if height == blockchain.height + 1 => ng.bestLiquidBlock.bytes() })
+ }
- override def scoreOf(blockId: BlockId): Option[BigInt] =
+ override def scoreOf(blockId: BlockId): Option[BigInt] = readLock {
blockchain
.scoreOf(blockId)
.orElse(ngState.collect { case ng if ng.contains(blockId) => blockchain.score + ng.base.blockScore() })
+ }
- override def heightOf(blockId: BlockId): Option[Int] =
+ override def heightOf(blockId: BlockId): Option[Int] = readLock {
blockchain
.heightOf(blockId)
.orElse(ngState.collect { case ng if ng.contains(blockId) => this.height })
+ }
- override def lastBlockIds(howMany: Int): Seq[BlockId] =
+ override def lastBlockIds(howMany: Int): Seq[BlockId] = readLock {
ngState.fold(blockchain.lastBlockIds(howMany))(_.bestLiquidBlockId +: blockchain.lastBlockIds(howMany - 1))
+ }
- override def microBlock(id: BlockId): Option[MicroBlock] =
+ override def microBlock(id: BlockId): Option[MicroBlock] = readLock {
for {
ng <- ngState
mb <- ng.microBlock(id)
} yield mb
+ }
- def lastBlockTimestamp: Option[Long] = ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp)
+ def lastBlockTimestamp: Option[Long] = readLock {
+ ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp)
+ }
- def lastBlockId: Option[AssetId] = ngState.map(_.bestLiquidBlockId).orElse(blockchain.lastBlockId)
+ def lastBlockId: Option[AssetId] = readLock {
+ ngState.map(_.bestLiquidBlockId).orElse(blockchain.lastBlockId)
+ }
- def blockAt(height: Int): Option[Block] =
+ def blockAt(height: Int): Option[Block] = readLock {
if (height == this.height)
ngState.map(_.bestLiquidBlock)
else
blockchain.blockAt(height)
+ }
- override def lastPersistedBlockIds(count: Int): Seq[BlockId] = {
+ override def lastPersistedBlockIds(count: Int): Seq[BlockId] = readLock {
blockchain.lastBlockIds(count)
}
- override def microblockIds: Seq[BlockId] = ngState.fold(Seq.empty[BlockId])(_.microBlockIds)
+ override def microblockIds: Seq[BlockId] = readLock {
+ ngState.fold(Seq.empty[BlockId])(_.microBlockIds)
+ }
- override def bestLastBlockInfo(maxTimestamp: Long): Option[BlockMinerInfo] = {
+ override def bestLastBlockInfo(maxTimestamp: Long): Option[BlockMinerInfo] = readLock {
ngState
.map(_.bestLastBlockInfo(maxTimestamp))
.orElse(blockchain.lastBlock.map(b => BlockMinerInfo(b.consensusData, b.timestamp, b.uniqueId)))
}
- override def score: BigInt = blockchain.score + ngState.fold(BigInt(0))(_.bestLiquidBlock.blockScore())
+ override def score: BigInt = readLock {
+ blockchain.score + ngState.fold(BigInt(0))(_.bestLiquidBlock.blockScore())
+ }
- override def lastBlock: Option[Block] = ngState.map(_.bestLiquidBlock).orElse(blockchain.lastBlock)
+ override def lastBlock: Option[Block] = readLock {
+ ngState.map(_.bestLiquidBlock).orElse(blockchain.lastBlock)
+ }
- override def carryFee: Long = ngState.map(_.carryFee).getOrElse(blockchain.carryFee)
+ override def carryFee: Long = readLock {
+ ngState.map(_.carryFee).getOrElse(blockchain.carryFee)
+ }
- override def blockBytes(blockId: ByteStr): Option[Array[Byte]] =
+ override def blockBytes(blockId: ByteStr): Option[Array[Byte]] = readLock {
(for {
ng <- ngState
(block, _, _, _) <- ng.totalDiffOf(blockId)
} yield block.bytes()).orElse(blockchain.blockBytes(blockId))
+ }
- override def blockIdsAfter(parentSignature: ByteStr, howMany: Int): Option[Seq[ByteStr]] = {
+ override def blockIdsAfter(parentSignature: ByteStr, howMany: Int): Option[Seq[ByteStr]] = readLock {
ngState match {
case Some(ng) if ng.contains(parentSignature) => Some(Seq.empty[ByteStr])
case maybeNg =>
@@ -373,7 +437,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
}
}
- override def parent(block: Block, back: Int): Option[Block] = {
+ override def parent(block: Block, back: Int): Option[Block] = readLock {
ngState match {
case Some(ng) if ng.contains(block.reference) =>
if (back == 1) Some(ng.base) else blockchain.parent(ng.base, back - 1)
@@ -382,64 +446,76 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
}
}
- override def blockHeaderAndSize(height: Int): Option[(BlockHeader, Int)] = {
+ override def blockHeaderAndSize(height: Int): Option[(BlockHeader, Int)] = readLock {
if (height == blockchain.height + 1)
ngState.map(x => (x.bestLiquidBlock, x.bestLiquidBlock.bytes().length))
else
blockchain.blockHeaderAndSize(height)
}
- override def portfolio(a: Address): Portfolio = {
+ override def portfolio(a: Address): Portfolio = readLock {
val p = ngState.fold(Portfolio.empty)(_.bestLiquidDiff.portfolios.getOrElse(a, Portfolio.empty))
blockchain.portfolio(a).combine(p)
}
- private[this] def portfolioAt(a: Address, mb: ByteStr): Portfolio = {
+ private[this] def portfolioAt(a: Address, mb: ByteStr): Portfolio = readLock {
val p = ngState.fold(Portfolio.empty)(_.diffFor(mb)._1.portfolios.getOrElse(a, Portfolio.empty))
blockchain.portfolio(a).combine(p)
}
- override def transactionInfo(id: AssetId): Option[(Int, Transaction)] =
+ override def transactionInfo(id: AssetId): Option[(Int, Transaction)] = readLock {
ngState
.fold(Diff.empty)(_.bestLiquidDiff)
.transactions
.get(id)
.map(t => (t._1, t._2))
.orElse(blockchain.transactionInfo(id))
+ }
override def addressTransactions(address: Address, types: Set[Type], count: Int, fromId: Option[ByteStr]): Either[String, Seq[(Int, Transaction)]] =
- addressTransactionsFromDiff(blockchain, ngState.map(_.bestLiquidDiff))(address, types, count, fromId)
+ readLock {
+ addressTransactionsFromDiff(blockchain, ngState.map(_.bestLiquidDiff))(address, types, count, fromId)
+ }
- override def containsTransaction(tx: Transaction): Boolean = ngState.fold(blockchain.containsTransaction(tx)) { ng =>
- ng.bestLiquidDiff.transactions.contains(tx.id()) || blockchain.containsTransaction(tx)
+ override def containsTransaction(tx: Transaction): Boolean = readLock {
+ ngState.fold(blockchain.containsTransaction(tx)) { ng =>
+ ng.bestLiquidDiff.transactions.contains(tx.id()) || blockchain.containsTransaction(tx)
+ }
}
- override def assetDescription(id: AssetId): Option[AssetDescription] = ngState.fold(blockchain.assetDescription(id)) { ng =>
- val diff = ng.bestLiquidDiff
- CompositeBlockchain.composite(blockchain, diff).assetDescription(id)
+ override def assetDescription(id: AssetId): Option[AssetDescription] = readLock {
+ ngState.fold(blockchain.assetDescription(id)) { ng =>
+ val diff = ng.bestLiquidDiff
+ CompositeBlockchain.composite(blockchain, diff).assetDescription(id)
+ }
}
- override def resolveAlias(alias: Alias): Either[ValidationError, Address] = ngState.fold(blockchain.resolveAlias(alias)) { ng =>
- CompositeBlockchain.composite(blockchain, ng.bestLiquidDiff).resolveAlias(alias)
+ override def resolveAlias(alias: Alias): Either[ValidationError, Address] = readLock {
+ ngState.fold(blockchain.resolveAlias(alias)) { ng =>
+ CompositeBlockchain.composite(blockchain, ng.bestLiquidDiff).resolveAlias(alias)
+ }
}
- override def leaseDetails(leaseId: AssetId): Option[LeaseDetails] = ngState match {
- case Some(ng) =>
- blockchain.leaseDetails(leaseId).map(ld => ld.copy(isActive = ng.bestLiquidDiff.leaseState.getOrElse(leaseId, ld.isActive))) orElse
- ng.bestLiquidDiff.transactions.get(leaseId).collect {
- case (h, lt: LeaseTransaction, _) =>
- LeaseDetails(lt.sender, lt.recipient, h, lt.amount, ng.bestLiquidDiff.leaseState(lt.id()))
- }
- case None =>
- blockchain.leaseDetails(leaseId)
+ override def leaseDetails(leaseId: AssetId): Option[LeaseDetails] = readLock {
+ ngState match {
+ case Some(ng) =>
+ blockchain.leaseDetails(leaseId).map(ld => ld.copy(isActive = ng.bestLiquidDiff.leaseState.getOrElse(leaseId, ld.isActive))) orElse
+ ng.bestLiquidDiff.transactions.get(leaseId).collect {
+ case (h, lt: LeaseTransaction, _) =>
+ LeaseDetails(lt.sender, lt.recipient, h, lt.amount, ng.bestLiquidDiff.leaseState(lt.id()))
+ }
+ case None =>
+ blockchain.leaseDetails(leaseId)
+ }
}
- override def filledVolumeAndFee(orderId: AssetId): VolumeAndFee =
+ override def filledVolumeAndFee(orderId: AssetId): VolumeAndFee = readLock {
ngState.fold(blockchain.filledVolumeAndFee(orderId))(
_.bestLiquidDiff.orderFills.get(orderId).orEmpty.combine(blockchain.filledVolumeAndFee(orderId)))
+ }
/** Retrieves Zbs balance snapshot in the [from, to] range (inclusive) */
- override def balanceSnapshots(address: Address, from: Int, to: BlockId): Seq[BalanceSnapshot] = {
+ override def balanceSnapshots(address: Address, from: Int, to: BlockId): Seq[BalanceSnapshot] = readLock {
val blockchainBlock = blockchain.heightOf(to)
if (blockchainBlock.nonEmpty || ngState.isEmpty) {
blockchain.balanceSnapshots(address, from, to)
@@ -449,14 +525,16 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
}
}
- override def accountScript(address: Address): Option[Script] = ngState.fold(blockchain.accountScript(address)) { ng =>
- ng.bestLiquidDiff.scripts.get(address) match {
- case None => blockchain.accountScript(address)
- case Some(scr) => scr
+ override def accountScript(address: Address): Option[Script] = readLock {
+ ngState.fold(blockchain.accountScript(address)) { ng =>
+ ng.bestLiquidDiff.scripts.get(address) match {
+ case None => blockchain.accountScript(address)
+ case Some(scr) => scr
+ }
}
}
- override def hasScript(address: Address): Boolean =
+ override def hasScript(address: Address): Boolean = readLock {
ngState
.flatMap(
_.bestLiquidDiff.scripts
@@ -464,33 +542,42 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
.map(_.nonEmpty)
)
.getOrElse(blockchain.hasScript(address))
+ }
- override def assetScript(asset: AssetId): Option[Script] = ngState.fold(blockchain.assetScript(asset)) { ng =>
- ng.bestLiquidDiff.assetScripts.get(asset) match {
- case None => blockchain.assetScript(asset)
- case Some(scr) => scr
+ override def assetScript(asset: AssetId): Option[Script] = readLock {
+ ngState.fold(blockchain.assetScript(asset)) { ng =>
+ ng.bestLiquidDiff.assetScripts.get(asset) match {
+ case None => blockchain.assetScript(asset)
+ case Some(scr) => scr
+ }
}
}
- override def hasAssetScript(asset: AssetId): Boolean = ngState.fold(blockchain.hasAssetScript(asset)) { ng =>
- ng.bestLiquidDiff.assetScripts.get(asset) match {
- case None => blockchain.hasAssetScript(asset)
- case Some(x) => x.nonEmpty
+ override def hasAssetScript(asset: AssetId): Boolean = readLock {
+ ngState.fold(blockchain.hasAssetScript(asset)) { ng =>
+ ng.bestLiquidDiff.assetScripts.get(asset) match {
+ case None => blockchain.hasAssetScript(asset)
+ case Some(x) => x.nonEmpty
+ }
}
}
- override def accountData(acc: Address): AccountDataInfo = ngState.fold(blockchain.accountData(acc)) { ng =>
- val fromInner = blockchain.accountData(acc)
- val fromDiff = ng.bestLiquidDiff.accountData.get(acc).orEmpty
- fromInner.combine(fromDiff)
+ override def accountData(acc: Address): AccountDataInfo = readLock {
+ ngState.fold(blockchain.accountData(acc)) { ng =>
+ val fromInner = blockchain.accountData(acc)
+ val fromDiff = ng.bestLiquidDiff.accountData.get(acc).orEmpty
+ fromInner.combine(fromDiff)
+ }
}
- override def accountData(acc: Address, key: String): Option[DataEntry[_]] = ngState.fold(blockchain.accountData(acc, key)) { ng =>
- val diffData = ng.bestLiquidDiff.accountData.get(acc).orEmpty
- diffData.data.get(key).orElse(blockchain.accountData(acc, key))
+ override def accountData(acc: Address, key: String): Option[DataEntry[_]] = readLock {
+ ngState.fold(blockchain.accountData(acc, key)) { ng =>
+ val diffData = ng.bestLiquidDiff.accountData.get(acc).orEmpty
+ diffData.data.get(key).orElse(blockchain.accountData(acc, key))
+ }
}
- private def changedBalances(pred: Portfolio => Boolean, f: Address => Long): Map[Address, Long] =
+ private def changedBalances(pred: Portfolio => Boolean, f: Address => Long): Map[Address, Long] = readLock {
ngState
.fold(Map.empty[Address, Long]) { ng =>
for {
@@ -498,10 +585,11 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
if pred(p)
} yield address -> f(address)
}
+ }
- override def assetDistribution(assetId: AssetId): AssetDistribution = {
+ override def assetDistribution(assetId: AssetId): AssetDistribution = readLock {
val fromInner = blockchain.assetDistribution(assetId)
- val fromNg = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, portfolio(_).assets.getOrElse(assetId, 0L)))
+ val fromNg = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, balance(_, Some(assetId))))
fromInner |+| fromNg
}
@@ -509,64 +597,78 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A
override def assetDistributionAtHeight(assetId: AssetId,
height: Int,
count: Int,
- fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage] = {
+ fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage] = readLock {
blockchain.assetDistributionAtHeight(assetId, height, count, fromAddress)
}
- override def zbsDistribution(height: Int): Map[Address, Long] = ngState.fold(blockchain.zbsDistribution(height)) { ng =>
- val innerDistribution = blockchain.zbsDistribution(height)
- if (height < this.height) innerDistribution
- else {
- innerDistribution ++ changedBalances(_.balance != 0, portfolio(_).balance)
+ override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = readLock {
+ ngState.fold(blockchain.zbsDistribution(height)) { ng =>
+ val innerDistribution = blockchain.zbsDistribution(height)
+ if (height < this.height) innerDistribution
+ else {
+ innerDistribution.map(_ ++ changedBalances(_.balance != 0, balance(_)))
+ }
}
}
- override def allActiveLeases: Set[LeaseTransaction] = ngState.fold(blockchain.allActiveLeases) { ng =>
- val (active, canceled) = ng.bestLiquidDiff.leaseState.partition(_._2)
- val fromDiff = active.keys
- .map { id =>
- ng.bestLiquidDiff.transactions(id)._2
- }
- .collect { case lt: LeaseTransaction => lt }
- .toSet
- val fromInner = blockchain.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id()))
- fromDiff ++ fromInner
+ override def allActiveLeases: Set[LeaseTransaction] = readLock {
+ ngState.fold(blockchain.allActiveLeases) { ng =>
+ val (active, canceled) = ng.bestLiquidDiff.leaseState.partition(_._2)
+ val fromDiff = active.keys
+ .map { id =>
+ ng.bestLiquidDiff.transactions(id)._2
+ }
+ .collect { case lt: LeaseTransaction => lt }
+ .toSet
+ val fromInner = blockchain.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id()))
+ fromDiff ++ fromInner
+ }
}
/** Builds a new portfolio map by applying a partial function to all portfolios on which the function is defined.
*
* @note Portfolios passed to `pf` only contain Zbs and Leasing balances to improve performance */
- override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] =
+ override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = readLock {
ngState.fold(blockchain.collectLposPortfolios(pf)) { ng =>
val b = Map.newBuilder[Address, A]
for ((a, p) <- ng.bestLiquidDiff.portfolios if p.lease != LeaseBalance.empty || p.balance != 0) {
- pf.runWith(b += a -> _)(a -> portfolio(a).copy(assets = Map.empty))
+ pf.runWith(b += a -> _)(a -> this.zbsPortfolio(a))
}
blockchain.collectLposPortfolios(pf) ++ b.result()
}
+ }
- override def append(diff: Diff, carry: Long, block: Block): Unit = blockchain.append(diff, carry, block)
+ override def append(diff: Diff, carry: Long, block: Block): Unit = readLock {
+ blockchain.append(diff, carry, block)
+ }
- override def rollbackTo(targetBlockId: AssetId): Either[String, Seq[Block]] = blockchain.rollbackTo(targetBlockId)
+ override def rollbackTo(targetBlockId: AssetId): Either[String, Seq[Block]] = readLock {
+ blockchain.rollbackTo(targetBlockId)
+ }
- override def transactionHeight(id: AssetId): Option[Int] =
+ override def transactionHeight(id: AssetId): Option[Int] = readLock {
ngState flatMap { ng =>
ng.bestLiquidDiff.transactions.get(id).map(_._1)
} orElse blockchain.transactionHeight(id)
+ }
- override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = ngState match {
- case Some(ng) =>
- blockchain.balance(address, mayBeAssetId) + ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).balanceOf(mayBeAssetId)
- case None =>
- blockchain.balance(address, mayBeAssetId)
+ override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = readLock {
+ ngState match {
+ case Some(ng) =>
+ blockchain.balance(address, mayBeAssetId) + ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).balanceOf(mayBeAssetId)
+ case None =>
+ blockchain.balance(address, mayBeAssetId)
+ }
}
- override def leaseBalance(address: Address): LeaseBalance = ngState match {
- case Some(ng) =>
- cats.Monoid.combine(blockchain.leaseBalance(address), ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).lease)
- case None =>
- blockchain.leaseBalance(address)
+ override def leaseBalance(address: Address): LeaseBalance = readLock {
+ ngState match {
+ case Some(ng) =>
+ cats.Monoid.combine(blockchain.leaseBalance(address), ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).lease)
+ case None =>
+ blockchain.leaseBalance(address)
+ }
}
}
diff --git a/src/main/scala/com/zbsnetwork/state/NgState.scala b/src/main/scala/com/zbsnetwork/state/NgState.scala
index 24529ed..54ea685 100644
--- a/src/main/scala/com/zbsnetwork/state/NgState.scala
+++ b/src/main/scala/com/zbsnetwork/state/NgState.scala
@@ -1,7 +1,6 @@
package com.zbsnetwork.state
import java.util.concurrent.TimeUnit
-import java.util.concurrent.locks.{Lock, ReentrantReadWriteLock}
import cats.kernel.Monoid
import com.google.common.cache.CacheBuilder
@@ -11,14 +10,15 @@ import com.zbsnetwork.block.{Block, MicroBlock}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.transaction.{DiscardedMicroBlocks, Transaction}
+import scala.collection.mutable.{ListBuffer => MList, Map => MMap}
+
+/* This is not thread safe, used only from BlockchainUpdaterImpl */
class NgState(val base: Block, val baseBlockDiff: Diff, val baseBlockCarry: Long, val approvedFeatures: Set[Short]) extends ScorexLogging {
private val MaxTotalDiffs = 3
- private val state = new SynchronizedAppendState[MicroBlock, BlockId, (Diff, Long, Long)](_.totalResBlockSig)
-
- private def microDiffs = state.mapping // microDiff, carryFee, timestamp
- private def micros = state.stack // fresh head
+ private val microDiffs: MMap[BlockId, (Diff, Long, Long)] = MMap.empty // microDiff, carryFee, timestamp
+ private val micros: MList[MicroBlock] = MList.empty // fresh head
private val totalBlockDiffCache = CacheBuilder
.newBuilder()
@@ -110,48 +110,9 @@ class NgState(val base: Block, val baseBlockDiff: Diff, val baseBlockCarry: Long
}
def append(m: MicroBlock, diff: Diff, microblockCarry: Long, timestamp: Long): Unit = {
- state.append(m, (diff, microblockCarry, timestamp))
+ microDiffs.put(m.totalResBlockSig, (diff, microblockCarry, timestamp))
+ micros.prepend(m)
}
def carryFee: Long = baseBlockCarry + microDiffs.values.map(_._2).sum
}
-
-/**
- * Allow atomically appends to state
- * Return internal stack and mapping state without dirty reads
- */
-private class SynchronizedAppendState[T, K, V](toKey: T => K) {
- private def inLock[R](l: Lock, f: => R) = {
- try {
- l.lock()
- val res = f
- res
- } finally {
- l.unlock()
- }
- }
- private val lock = new ReentrantReadWriteLock
- private def writeLock[B](f: => B): B = inLock(lock.writeLock(), f)
- private def readLock[B](f: => B): B = inLock(lock.readLock(), f)
-
- @volatile private var internalStack = List.empty[T]
- @volatile private var internalMap = Map.empty[K, V]
-
- /**
- * Stack state
- */
- def stack: List[T] = readLock(internalStack)
-
- /**
- * Mapping state
- */
- def mapping: Map[K, V] = readLock(internalMap)
-
- /**
- * Atomically appends to state both stack and map
- */
- def append(t: T, v: V): Unit = writeLock {
- internalStack = t :: internalStack
- internalMap = internalMap.updated(toKey(t), v)
- }
-}
diff --git a/src/main/scala/com/zbsnetwork/state/Portfolio.scala b/src/main/scala/com/zbsnetwork/state/Portfolio.scala
index bbf4a55..9218fd7 100644
--- a/src/main/scala/com/zbsnetwork/state/Portfolio.scala
+++ b/src/main/scala/com/zbsnetwork/state/Portfolio.scala
@@ -36,6 +36,7 @@ object Portfolio {
}
implicit class PortfolioExt(self: Portfolio) {
+ def spendableBalanceOf(assetId: Option[AssetId]): Long = assetId.fold(self.spendableBalance)(self.assets.getOrElse(_, 0L))
def pessimistic: Portfolio = Portfolio(
balance = Math.min(self.balance, 0),
@@ -52,7 +53,21 @@ object Portfolio {
def minus(other: Portfolio): Portfolio =
Portfolio(self.balance - other.balance, LeaseBalance.empty, Monoid.combine(self.assets, other.assets.mapValues(-_)))
- def negate = Portfolio.empty minus self
+ def negate: Portfolio = Portfolio.empty minus self
+
+ def assetIds: Set[Option[AssetId]] = {
+ val r: Set[Option[AssetId]] = self.assets.keySet.map(Some(_))
+ r + None
+ }
+
+ def changedAssetIds(that: Portfolio): Set[Option[AssetId]] = {
+ val a1 = assetIds
+ val a2 = that.assetIds
+
+ val intersection = a1 & a2
+ val sureChanged = (a1 | a2) -- intersection
+ intersection.filter(x => spendableBalanceOf(x) != that.spendableBalanceOf(x)) ++ sureChanged
+ }
}
}
diff --git a/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala b/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala
index ee28aff..bca6b92 100644
--- a/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala
+++ b/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala
@@ -1,6 +1,7 @@
package com.zbsnetwork.state.diffs
import cats._
+import cats.implicits._
import com.zbsnetwork.account.Address
import com.zbsnetwork.features.FeatureProvider._
import com.zbsnetwork.features.{BlockchainFeature, BlockchainFeatures}
@@ -9,16 +10,15 @@ import com.zbsnetwork.settings.FunctionalitySettings
import com.zbsnetwork.state._
import com.zbsnetwork.transaction.ValidationError._
import com.zbsnetwork.transaction.assets._
-import com.zbsnetwork.transaction.assets.exchange._
+import com.zbsnetwork.transaction.assets.exchange.{Order, _}
import com.zbsnetwork.transaction.lease._
-import com.zbsnetwork.transaction.smart.script.ContractScript
-import com.zbsnetwork.transaction.smart.script.Script
-import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl
+import com.zbsnetwork.transaction.smart.script.v1.ExprScript
+import com.zbsnetwork.transaction.smart.script.{ContractScript, Script}
import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction}
import com.zbsnetwork.transaction.transfer._
import com.zbsnetwork.transaction.{smart, _}
-import scala.util.{Left, Right}
+import scala.util.{Left, Right, Try}
object CommonValidation {
@@ -28,20 +28,20 @@ object CommonValidation {
val FeeConstants: Map[Byte, Long] = Map(
GenesisTransaction.typeId -> 0,
PaymentTransaction.typeId -> 1,
- IssueTransaction.typeId -> 1000,
- ReissueTransaction.typeId -> 1000,
- BurnTransaction.typeId -> 1,
- TransferTransaction.typeId -> 1,
- MassTransferTransaction.typeId -> 1,
- LeaseTransaction.typeId -> 1,
- LeaseCancelTransaction.typeId -> 1,
- ExchangeTransaction.typeId -> 3,
- CreateAliasTransaction.typeId -> 1,
- DataTransaction.typeId -> 1,
- SetScriptTransaction.typeId -> 10,
- SponsorFeeTransaction.typeId -> 1000,
- SetAssetScriptTransaction.typeId -> (1000 - 4),
- smart.ContractInvocationTransaction.typeId -> 5
+ IssueTransaction.typeId -> 500000,
+ ReissueTransaction.typeId -> 200000,
+ BurnTransaction.typeId -> 5000,
+ TransferTransaction.typeId -> 50,
+ MassTransferTransaction.typeId -> 50,
+ LeaseTransaction.typeId -> 5000,
+ LeaseCancelTransaction.typeId -> 1000,
+ ExchangeTransaction.typeId -> 200,
+ CreateAliasTransaction.typeId -> 10000,
+ DataTransaction.typeId -> 30,
+ SetScriptTransaction.typeId -> 10000,
+ SponsorFeeTransaction.typeId -> 50000,
+ SetAssetScriptTransaction.typeId -> (10000 - 4),
+ smart.ContractInvocationTransaction.typeId -> 100
)
def disallowSendingGreaterThanBalance[T <: Transaction](blockchain: Blockchain,
@@ -90,7 +90,9 @@ object CommonValidation {
s"${blockchain.balance(ptx.sender, None)} is less than ${ptx.amount + ptx.fee}"))
case ttx: TransferTransaction => checkTransfer(ttx.sender, ttx.assetId, ttx.amount, ttx.feeAssetId, ttx.fee)
case mtx: MassTransferTransaction => checkTransfer(mtx.sender, mtx.assetId, mtx.transfers.map(_.amount).sum, None, mtx.fee)
- case _ => Right(tx)
+ case citx: ContractInvocationTransaction =>
+ checkTransfer(citx.sender, citx.payment.flatMap(_.assetId), citx.payment.map(_.amount).getOrElse(0), None, citx.fee)
+ case _ => Right(tx)
}
} else Right(tx)
@@ -106,51 +108,67 @@ object CommonValidation {
def disallowBeforeActivationTime[T <: Transaction](blockchain: Blockchain, height: Int, tx: T): Either[ValidationError, T] = {
- def activationBarrier(b: BlockchainFeature, msg: Option[String] = None) =
+ def activationBarrier(b: BlockchainFeature, msg: Option[String] = None): Either[ActivationError, T] =
Either.cond(
blockchain.isFeatureActivated(b, height),
tx,
ValidationError.ActivationError(msg.getOrElse(tx.getClass.getSimpleName) + " has not been activated yet")
)
- def scriptActivation(sc: Script) = {
+ def scriptActivation(sc: Script): Either[ActivationError, T] = {
+
val ab = activationBarrier(BlockchainFeatures.Ride4DApps, Some("Ride4DApps has not been activated yet"))
- def scriptVersionActivation(sc: Script) = sc.stdLibVersion match {
+
+ def scriptVersionActivation(sc: Script): Either[ActivationError, T] = sc.stdLibVersion match {
case V1 | V2 if sc.containsBlockV2.value => ab
case V1 | V2 => Right(tx)
case V3 => ab
}
- def scriptTypeActivation(sc: Script) = sc match {
- case e: ExprScriprImpl => Right(tx)
+
+ def scriptTypeActivation(sc: Script): Either[ActivationError, T] = sc match {
+ case e: ExprScript => Right(tx)
case c: ContractScript.ContractScriptImpl => ab
}
+
for {
_ <- scriptVersionActivation(sc)
_ <- scriptTypeActivation(sc)
} yield tx
}
+
tx match {
- case _: BurnTransactionV1 => Right(tx)
- case _: PaymentTransaction => Right(tx)
- case _: GenesisTransaction => Right(tx)
- case _: TransferTransactionV1 => Right(tx)
- case _: IssueTransactionV1 => Right(tx)
- case _: ReissueTransactionV1 => Right(tx)
- case _: ExchangeTransactionV1 => Right(tx)
- case _: ExchangeTransactionV2 => activationBarrier(BlockchainFeatures.SmartAccountTrading)
+ case _: BurnTransactionV1 => Right(tx)
+ case _: PaymentTransaction => Right(tx)
+ case _: GenesisTransaction => Right(tx)
+ case _: TransferTransactionV1 => Right(tx)
+ case _: IssueTransactionV1 => Right(tx)
+ case _: ReissueTransactionV1 => Right(tx)
+ case _: ExchangeTransactionV1 => Right(tx)
+
+ case exv2: ExchangeTransactionV2 =>
+ activationBarrier(BlockchainFeatures.SmartAccountTrading).flatMap { tx =>
+ (exv2.buyOrder, exv2.sellOrder) match {
+ case (_: OrderV3, _: Order) | (_: Order, _: OrderV3) => activationBarrier(BlockchainFeatures.OrderV3, Some("Order Version 3"))
+ case _ => Right(tx)
+ }
+ }
+
case _: LeaseTransactionV1 => Right(tx)
case _: LeaseCancelTransactionV1 => Right(tx)
case _: CreateAliasTransactionV1 => Right(tx)
case _: MassTransferTransaction => activationBarrier(BlockchainFeatures.MassTransfer)
case _: DataTransaction => activationBarrier(BlockchainFeatures.DataTransaction)
+
case sst: SetScriptTransaction =>
sst.script match {
case None => Right(tx)
case Some(sc) => scriptActivation(sc)
}
+
case _: TransferTransactionV2 => activationBarrier(BlockchainFeatures.SmartAccounts)
case it: IssueTransactionV2 => activationBarrier(if (it.script.isEmpty) BlockchainFeatures.SmartAccounts else BlockchainFeatures.SmartAssets)
+
case it: SetAssetScriptTransaction =>
it.script match {
case None => Left(GenericError("Cannot set empty script"))
@@ -286,4 +304,12 @@ object CommonValidation {
}
def cond[A](c: Boolean)(a: A, b: A): A = if (c) a else b
+
+ def validateOverflow(dataList: Traversable[Long], errMsg: String): Either[ValidationError, Unit] = {
+ Try(dataList.foldLeft(0L)(Math.addExact))
+ .fold(
+ _ => GenericError(errMsg).asLeft[Unit],
+ _ => ().asRight[ValidationError]
+ )
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala
index 37fabe8..c2ec5c2 100644
--- a/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala
+++ b/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala
@@ -7,7 +7,7 @@ import com.zbsnetwork.account.{Address, AddressScheme}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.lang.contract.Contract
-import com.zbsnetwork.lang.v1.FunctionHeader
+import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader}
import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
@@ -16,6 +16,7 @@ import com.zbsnetwork.lang.v1.traits.domain.Tx.ContractTransfer
import com.zbsnetwork.lang.v1.traits.domain.{DataItem, Recipient}
import com.zbsnetwork.lang.{Global, StdLibVersion}
import com.zbsnetwork.state._
+import com.zbsnetwork.state.diffs.CommonValidation._
import com.zbsnetwork.state.reader.CompositeBlockchain
import com.zbsnetwork.transaction.ValidationError
import com.zbsnetwork.transaction.ValidationError._
@@ -62,13 +63,35 @@ object ContractInvocationTransactionDiff {
case ContractResult(ds, ps) =>
import cats.implicits._
- val dataAndPaymentDiff = payableAndDataPart(height, tx, ds)
val pmts: List[Map[Address, Map[Option[ByteStr], Long]]] = ps.map {
case (Recipient.Address(addrBytes), amt, maybeAsset) =>
Map(Address.fromBytes(addrBytes.arr).explicitGet() -> Map(maybeAsset -> amt))
}
for {
- _ <- Either.cond(pmts.flatMap(_.values).flatMap(_.values).forall(_ >= 0), (), ValidationError.NegativeAmount(-42, ""))
+ feeInfo <- (tx.assetFee._1 match {
+ case None => Right((tx.fee, Map(tx.sender.toAddress -> Portfolio(-tx.fee, LeaseBalance.empty, Map.empty))))
+ case Some(assetId) =>
+ for {
+ assetInfo <- blockchain
+ .assetDescription(assetId)
+ .toRight(GenericError(s"Asset $assetId does not exist, cannot be used to pay fees"))
+ zbsFee <- Either.cond(
+ assetInfo.sponsorship > 0,
+ Sponsorship.toZbs(tx.fee, assetInfo.sponsorship),
+ GenericError(s"Asset $assetId is not sponsored, cannot be used to pay fees")
+ )
+ } yield {
+ (zbsFee,
+ Map(
+ tx.sender.toAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -tx.fee)),
+ assetInfo.issuer.toAddress -> Portfolio(-zbsFee, LeaseBalance.empty, Map(assetId -> tx.fee))
+ ))
+ }
+ })
+ zbsFee = feeInfo._1
+ dataAndPaymentDiff <- payableAndDataPart(height, tx, ds, feeInfo._2)
+ _ <- Either.cond(pmts.flatMap(_.values).flatMap(_.values).forall(_ >= 0), (), ValidationError.NegativeAmount(-42, ""))
+ _ <- validateOverflow(pmts.flatMap(_.values).flatMap(_.values), "Attempt to transfer unavailable funds in contract payment")
_ <- Either.cond(
pmts
.flatMap(_.values)
@@ -78,8 +101,17 @@ object ContractInvocationTransactionDiff {
(),
GenericError(s"Unissued assets are not allowed")
)
- _ <- Either.cond(true, (), ValidationError.NegativeAmount(-42, "")) // - sum doesn't overflow
- _ <- Either.cond(true, (), ValidationError.NegativeAmount(-42, "")) // - whatever else tranfser/massTransfer ensures
+ _ <- {
+ val totalScriptsInvoked = tx.checkedAssets().count(blockchain.hasAssetScript) +
+ ps.count(_._3.fold(false)(blockchain.hasAssetScript))
+ val minZbs = totalScriptsInvoked * ScriptExtraFee + FeeConstants(ContractInvocationTransaction.typeId) * FeeUnit
+ Either.cond(
+ minZbs <= zbsFee,
+ (),
+ GenericError(s"Fee in ${tx.assetFee._1
+ .fold("ZBS")(_.toString)} for ${tx.builder.classTag} with $totalScriptsInvoked total scripts invoked does not exceed minimal value of $minZbs ZBS: ${tx.assetFee._2}")
+ )
+ }
_ <- foldContractTransfers(blockchain, tx)(ps, dataAndPaymentDiff)
} yield {
val paymentReceiversMap: Map[Address, Portfolio] = Monoid
@@ -97,13 +129,15 @@ object ContractInvocationTransactionDiff {
}
- private def payableAndDataPart(height: Int, tx: ContractInvocationTransaction, ds: List[DataItem[_]]) = {
+ private def payableAndDataPart(height: Int, tx: ContractInvocationTransaction, ds: List[DataItem[_]], feePart: Map[Address, Portfolio]) = {
val r: Seq[DataEntry[_]] = ds.map {
case DataItem.Bool(k, b) => BooleanDataEntry(k, b)
case DataItem.Str(k, b) => StringDataEntry(k, b)
case DataItem.Lng(k, b) => IntegerDataEntry(k, b)
case DataItem.Bin(k, b) => BinaryDataEntry(k, b)
}
+ val totalDataBytes = r.map(_.toBytes.size).sum
+
val payablePart: Map[Address, Portfolio] = tx.payment match {
case None => Map.empty
case Some(ContractInvocationTransaction.Payment(amt, assetOpt)) =>
@@ -117,45 +151,48 @@ object ContractInvocationTransactionDiff {
.combine(Map(tx.contractAddress -> Portfolio(amt, LeaseBalance.empty, Map.empty)))
}
}
- val feePart = Map(tx.sender.toAddress -> Portfolio(-tx.fee, LeaseBalance.empty, Map.empty))
- Diff(
- height = height,
- tx = tx,
- portfolios = feePart combine payablePart,
- accountData = Map(tx.contractAddress -> AccountDataInfo(r.map(d => d.key -> d).toMap))
- )
-
+ if (totalDataBytes <= ContractLimits.MaxWriteSetSizeInBytes)
+ Right(
+ Diff(
+ height = height,
+ tx = tx,
+ portfolios = feePart combine payablePart,
+ accountData = Map(tx.contractAddress -> AccountDataInfo(r.map(d => d.key -> d).toMap))
+ ))
+ else Left(GenericError(s"WriteSet size can't exceed ${ContractLimits.MaxWriteSetSizeInBytes} bytes, actual: $totalDataBytes bytes"))
}
+
private def foldContractTransfers(blockchain: Blockchain, tx: ContractInvocationTransaction)(ps: List[(Recipient.Address, Long, Option[ByteStr])],
dataDiff: Diff): Either[ValidationError, Diff] = {
-
- ps.foldLeft(Either.right[ValidationError, Diff](dataDiff)) { (diffEi, payment) =>
- val (addressRepr, amount, asset) = payment
- val address = Address.fromBytes(addressRepr.bytes.arr).explicitGet()
- asset match {
- case None =>
- diffEi combine Right(
- Diff.stateOps(
- portfolios = Map(
- address -> Portfolio(amount, LeaseBalance.empty, Map.empty),
- tx.contractAddress -> Portfolio(-amount, LeaseBalance.empty, Map.empty)
- )))
- case Some(assetId) =>
- diffEi combine {
- val nextDiff = Diff.stateOps(
- portfolios = Map(
- address -> Portfolio(0, LeaseBalance.empty, Map(assetId -> amount)),
- tx.contractAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -amount))
- ))
- blockchain.assetScript(assetId) match {
- case None =>
- Right(nextDiff)
- case Some(script) =>
- diffEi flatMap (d => validateContractTransferWithSmartAssetScript(blockchain, tx)(d, addressRepr, amount, asset, nextDiff, script))
+ if (ps.length <= ContractLimits.MaxPaymentAmount)
+ ps.foldLeft(Either.right[ValidationError, Diff](dataDiff)) { (diffEi, payment) =>
+ val (addressRepr, amount, asset) = payment
+ val address = Address.fromBytes(addressRepr.bytes.arr).explicitGet()
+ asset match {
+ case None =>
+ diffEi combine Right(
+ Diff.stateOps(
+ portfolios = Map(
+ address -> Portfolio(amount, LeaseBalance.empty, Map.empty),
+ tx.contractAddress -> Portfolio(-amount, LeaseBalance.empty, Map.empty)
+ )))
+ case Some(assetId) =>
+ diffEi combine {
+ val nextDiff = Diff.stateOps(
+ portfolios = Map(
+ address -> Portfolio(0, LeaseBalance.empty, Map(assetId -> amount)),
+ tx.contractAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -amount))
+ ))
+ blockchain.assetScript(assetId) match {
+ case None =>
+ Right(nextDiff)
+ case Some(script) =>
+ diffEi flatMap (d => validateContractTransferWithSmartAssetScript(blockchain, tx)(d, addressRepr, amount, asset, nextDiff, script))
+ }
}
- }
- }
- }
+ }
+ } else
+ Left(GenericError(s"Too many ContractTransfers: max: ${ContractLimits.MaxPaymentAmount}, actual: ${ps.length}"))
}
private def validateContractTransferWithSmartAssetScript(blockchain: Blockchain, tx: ContractInvocationTransaction)(
diff --git a/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala
index 5584ed1..8174447 100644
--- a/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala
+++ b/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala
@@ -2,27 +2,32 @@ package com.zbsnetwork.state.diffs
import cats._
import cats.implicits._
+import com.zbsnetwork.account.Address
import com.zbsnetwork.features.BlockchainFeatures
import com.zbsnetwork.state._
-import com.zbsnetwork.transaction.ValidationError
+import com.zbsnetwork.transaction.{AssetId, ValidationError}
import com.zbsnetwork.transaction.ValidationError.{GenericError, OrderValidationError}
-import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction
+import com.zbsnetwork.transaction.assets.exchange.{ExchangeTransaction, Order, OrderV3}
import scala.util.Right
object ExchangeTransactionDiff {
def apply(blockchain: Blockchain, height: Int)(tx: ExchangeTransaction): Either[ValidationError, Diff] = {
+
val matcher = tx.buyOrder.matcherPublicKey.toAddress
val buyer = tx.buyOrder.senderPublicKey.toAddress
val seller = tx.sellOrder.senderPublicKey.toAddress
+
val assetIds = Set(tx.buyOrder.assetPair.amountAsset,
tx.buyOrder.assetPair.priceAsset,
tx.sellOrder.assetPair.amountAsset,
tx.sellOrder.assetPair.priceAsset).flatten
+
val assets = assetIds.map(blockchain.assetDescription)
val smartTradesEnabled = blockchain.activatedFeatures.contains(BlockchainFeatures.SmartAccountTrading.id)
val smartAssetsEnabled = blockchain.activatedFeatures.contains(BlockchainFeatures.SmartAssets.id)
+
for {
_ <- Either.cond(assets.forall(_.isDefined), (), GenericError("Assets should be issued before they can be traded"))
_ <- Either.cond(
@@ -47,37 +52,32 @@ object ExchangeTransactionDiff {
sellAmountAssetChange <- t.sellOrder.getSpendAmount(t.amount, t.price).liftValidationError(tx).map(-_)
} yield {
- def zbsPortfolio(amt: Long) = Portfolio(amt, LeaseBalance.empty, Map.empty)
-
- val feeDiff = Monoid.combineAll(
- Seq(
- Map(matcher -> zbsPortfolio(t.buyMatcherFee + t.sellMatcherFee - t.fee)),
- Map(buyer -> zbsPortfolio(-t.buyMatcherFee)),
- Map(seller -> zbsPortfolio(-t.sellMatcherFee))
- ))
-
- val priceDiff = t.buyOrder.assetPair.priceAsset match {
- case Some(assetId) =>
- Monoid.combine(
- Map(buyer -> Portfolio(0, LeaseBalance.empty, Map(assetId -> buyPriceAssetChange))),
- Map(seller -> Portfolio(0, LeaseBalance.empty, Map(assetId -> sellPriceAssetChange)))
- )
- case None =>
- Monoid.combine(Map(buyer -> Portfolio(buyPriceAssetChange, LeaseBalance.empty, Map.empty)),
- Map(seller -> Portfolio(sellPriceAssetChange, LeaseBalance.empty, Map.empty)))
+ def getAssetDiff(asset: Option[AssetId], buyAssetChange: Long, sellAssetChange: Long): Map[Address, Portfolio] = {
+ Monoid.combine(
+ Map(buyer → getAssetPortfolio(asset, buyAssetChange)),
+ Map(seller → getAssetPortfolio(asset, sellAssetChange)),
+ )
}
- val amountDiff = t.buyOrder.assetPair.amountAsset match {
- case Some(assetId) =>
- Monoid.combine(
- Map(buyer -> Portfolio(0, LeaseBalance.empty, Map(assetId -> buyAmountAssetChange))),
- Map(seller -> Portfolio(0, LeaseBalance.empty, Map(assetId -> sellAmountAssetChange)))
+ val matcherPortfolio =
+ Monoid.combineAll(
+ Seq(
+ getOrderFeePortfolio(t.buyOrder, t.buyMatcherFee),
+ getOrderFeePortfolio(t.sellOrder, t.sellMatcherFee),
+ zbsPortfolio(-t.fee),
)
- case None =>
- Monoid.combine(Map(buyer -> Portfolio(buyAmountAssetChange, LeaseBalance.empty, Map.empty)),
- Map(seller -> Portfolio(sellAmountAssetChange, LeaseBalance.empty, Map.empty)))
- }
+ )
+ val feeDiff = Monoid.combineAll(
+ Seq(
+ Map(matcher -> matcherPortfolio),
+ Map(buyer -> getOrderFeePortfolio(t.buyOrder, -t.buyMatcherFee)),
+ Map(seller -> getOrderFeePortfolio(t.sellOrder, -t.sellMatcherFee))
+ )
+ )
+
+ val priceDiff = getAssetDiff(t.buyOrder.assetPair.priceAsset, buyPriceAssetChange, sellPriceAssetChange)
+ val amountDiff = getAssetDiff(t.buyOrder.assetPair.amountAsset, buyAmountAssetChange, sellAmountAssetChange)
val portfolios = Monoid.combineAll(Seq(feeDiff, priceDiff, amountDiff))
Diff(
@@ -93,26 +93,40 @@ object ExchangeTransactionDiff {
}
private def enoughVolume(exTrans: ExchangeTransaction, blockchain: Blockchain): Either[ValidationError, ExchangeTransaction] = {
+
val filledBuy = blockchain.filledVolumeAndFee(exTrans.buyOrder.id())
val filledSell = blockchain.filledVolumeAndFee(exTrans.sellOrder.id())
- val buyTotal = filledBuy.volume + exTrans.amount
- val sellTotal = filledSell.volume + exTrans.amount
+ val buyTotal = filledBuy.volume + exTrans.amount
+ val sellTotal = filledSell.volume + exTrans.amount
+
lazy val buyAmountValid = exTrans.buyOrder.amount >= buyTotal
lazy val sellAmountValid = exTrans.sellOrder.amount >= sellTotal
- def isFeeValid(feeTotal: Long, amountTotal: Long, maxfee: Long, maxAmount: Long): Boolean =
- feeTotal <= BigInt(maxfee) * BigInt(amountTotal) / BigInt(maxAmount)
+ def isFeeValid(feeTotal: Long, amountTotal: Long, maxfee: Long, maxAmount: Long, order: Order): Boolean = {
+ feeTotal <= (order match {
+ case _: OrderV3 => BigInt(maxfee)
+ case _ => BigInt(maxfee) * BigInt(amountTotal) / BigInt(maxAmount)
+ })
+ }
- lazy val buyFeeValid = isFeeValid(feeTotal = filledBuy.fee + exTrans.buyMatcherFee,
- amountTotal = buyTotal,
- maxfee = exTrans.buyOrder.matcherFee,
- maxAmount = exTrans.buyOrder.amount)
+ lazy val buyFeeValid =
+ isFeeValid(
+ feeTotal = filledBuy.fee + exTrans.buyMatcherFee,
+ amountTotal = buyTotal,
+ maxfee = exTrans.buyOrder.matcherFee,
+ maxAmount = exTrans.buyOrder.amount,
+ order = exTrans.buyOrder
+ )
- lazy val sellFeeValid = isFeeValid(feeTotal = filledSell.fee + exTrans.sellMatcherFee,
- amountTotal = sellTotal,
- maxfee = exTrans.sellOrder.matcherFee,
- maxAmount = exTrans.sellOrder.amount)
+ lazy val sellFeeValid =
+ isFeeValid(
+ feeTotal = filledSell.fee + exTrans.sellMatcherFee,
+ amountTotal = sellTotal,
+ maxfee = exTrans.sellOrder.matcherFee,
+ maxAmount = exTrans.sellOrder.amount,
+ order = exTrans.sellOrder
+ )
if (!buyAmountValid) Left(OrderValidationError(exTrans.buyOrder, s"Too much buy. Already filled volume for the order: ${filledBuy.volume}"))
else if (!sellAmountValid)
@@ -121,4 +135,13 @@ object ExchangeTransactionDiff {
else if (!sellFeeValid) Left(OrderValidationError(exTrans.sellOrder, s"Insufficient sell fee"))
else Right(exTrans)
}
+
+ def zbsPortfolio(amt: Long) = Portfolio(amt, LeaseBalance.empty, Map.empty)
+
+ def getAssetPortfolio(asset: Option[AssetId], amt: Long): Portfolio = {
+ asset.fold(zbsPortfolio(amt))(assetId => Portfolio(0, LeaseBalance.empty, Map(assetId -> amt)))
+ }
+
+ /*** Calculates fee portfolio from the order (taking into account that in OrderV3 fee can be paid in asset != Zbs) */
+ def getOrderFeePortfolio(order: Order, fee: Long): Portfolio = getAssetPortfolio(order.matcherFeeAssetId, fee)
}
diff --git a/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala
index 0f01e78..725f576 100644
--- a/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala
+++ b/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala
@@ -4,11 +4,12 @@ import cats.implicits._
import com.zbsnetwork.settings.FunctionalitySettings
import com.zbsnetwork.state._
import com.zbsnetwork.account.Address
+import com.zbsnetwork.features.BlockchainFeatures
import com.zbsnetwork.transaction.ValidationError
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction.transfer._
-import scala.util.Right
+import scala.util.{Right, Try}
object TransferTransactionDiff {
def apply(blockchain: Blockchain, s: FunctionalitySettings, blockTime: Long, height: Int)(
@@ -20,6 +21,9 @@ object TransferTransactionDiff {
_ <- Either.cond((tx.feeAssetId >>= blockchain.assetDescription >>= (_.script)).isEmpty,
(),
GenericError("Smart assets can't participate in TransferTransactions as a fee"))
+
+ _ <- validateOverflow(blockchain, tx)
+
portfolios = (tx.assetId match {
case None =>
Map(sender -> Portfolio(-tx.amount, LeaseBalance.empty, Map.empty)).combine(
@@ -60,4 +64,16 @@ object TransferTransactionDiff {
Right(Diff(height, tx, portfolios))
}
}
+
+ private def validateOverflow(blockchain: Blockchain, tx: TransferTransaction) = {
+ if (blockchain.activatedFeatures.contains(BlockchainFeatures.Ride4DApps.id)) {
+ Right(()) // lets transaction validates itself
+ } else {
+ Try(Math.addExact(tx.fee, tx.amount))
+ .fold(
+ _ => ValidationError.OverflowError.asLeft[Unit],
+ _ => ().asRight[ValidationError]
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/state/package.scala b/src/main/scala/com/zbsnetwork/state/package.scala
index 991927d..eecda68 100644
--- a/src/main/scala/com/zbsnetwork/state/package.scala
+++ b/src/main/scala/com/zbsnetwork/state/package.scala
@@ -133,6 +133,12 @@ package object state {
blockchain
.heightOf(id)
.getOrElse(throw new IllegalStateException(s"Can't find a block: $id"))
+
+ def zbsPortfolio(address: Address): Portfolio = Portfolio(
+ blockchain.balance(address),
+ blockchain.leaseBalance(address),
+ Map.empty
+ )
}
object AssetDistribution extends TaggedType[Map[Address, Long]]
diff --git a/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala b/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala
index d55735e..e949dbb 100644
--- a/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala
+++ b/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala
@@ -114,7 +114,7 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry:
override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = {
val b = Map.newBuilder[Address, A]
for ((a, p) <- diff.portfolios if p.lease != LeaseBalance.empty || p.balance != 0) {
- pf.runWith(b += a -> _)(a -> portfolio(a).copy(assets = Map.empty))
+ pf.runWith(b += a -> _)(a -> this.zbsPortfolio(a))
}
inner.collectLposPortfolios(pf) ++ b.result()
@@ -169,7 +169,7 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry:
override def assetDistribution(assetId: ByteStr): AssetDistribution = {
val fromInner = inner.assetDistribution(assetId)
- val fromDiff = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, portfolio(_).assets.getOrElse(assetId, 0L)))
+ val fromDiff = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, balance(_, Some(assetId))))
fromInner |+| fromDiff
}
@@ -181,11 +181,11 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry:
inner.assetDistributionAtHeight(assetId, height, count, fromAddress)
}
- override def zbsDistribution(height: Int): Map[Address, Long] = {
+ override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = {
val innerDistribution = inner.zbsDistribution(height)
if (height < this.height) innerDistribution
else {
- innerDistribution ++ changedBalances(_.balance != 0, portfolio(_).balance)
+ innerDistribution.map(_ ++ changedBalances(_.balance != 0, balance(_)))
}
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala
index f5072eb..529cbbc 100644
--- a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala
@@ -1,13 +1,15 @@
package com.zbsnetwork.transaction
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account._
import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.crypto._
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class CreateAliasTransactionV1 private (sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, signature: ByteStr)
extends CreateAliasTransaction
@@ -26,15 +28,11 @@ object CreateAliasTransactionV1 extends TransactionParserFor[CreateAliasTransact
override val typeId: Byte = CreateAliasTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- for {
- (sender, alias, fee, timestamp, end) <- CreateAliasTransaction.parseBase(0, bytes)
- signature = ByteStr(bytes.slice(end, end + SignatureLength))
- tx <- CreateAliasTransactionV1
- .create(sender, alias, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- } yield tx
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.fee > 0, tx, ValidationError.InsufficientFee)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, signature: ByteStr): Either[ValidationError, TransactionT] = {
@@ -54,4 +52,14 @@ object CreateAliasTransactionV1 extends TransactionParserFor[CreateAliasTransact
def selfSigned(sender: PrivateKeyAccount, alias: Alias, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, alias, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[CreateAliasTransactionV1] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ AliasBytes(tailIndex(2), "Alias object"),
+ LongBytes(tailIndex(3), "Fee"),
+ LongBytes(tailIndex(4), "Timestamp"),
+ SignatureBytes(tailIndex(5), "Signature")
+ ) mapN CreateAliasTransactionV1.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala
index d4264c4..14e7481 100644
--- a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala
@@ -2,12 +2,14 @@ package com.zbsnetwork.transaction
import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{Alias, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
final case class CreateAliasTransactionV2 private (sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, proofs: Proofs)
extends CreateAliasTransaction {
@@ -27,15 +29,11 @@ object CreateAliasTransactionV2 extends TransactionParserFor[CreateAliasTransact
override def supportedVersions: Set[Byte] = Set(2)
override protected def parseTail(bytes: Array[Byte]): Try[CreateAliasTransactionV2] = {
- Try {
- for {
- (sender, alias, fee, timestamp, end) <- CreateAliasTransaction.parseBase(0, bytes)
- result <- (for {
- proofs <- Proofs.fromBytes(bytes.drop(end))
- tx <- CreateAliasTransactionV2.create(sender, alias, fee, timestamp, proofs)
- } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- } yield result
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.fee > 0, tx, ValidationError.InsufficientFee)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -64,4 +62,14 @@ object CreateAliasTransactionV2 extends TransactionParserFor[CreateAliasTransact
def selfSigned(sender: PrivateKeyAccount, alias: Alias, fee: Long, timestamp: Long): Either[ValidationError, CreateAliasTransactionV2] = {
signed(sender, alias, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[CreateAliasTransactionV2] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ AliasBytes(tailIndex(2), "Alias object"),
+ LongBytes(tailIndex(3), "Fee"),
+ LongBytes(tailIndex(4), "Timestamp"),
+ ProofsBytes(tailIndex(5))
+ ) mapN CreateAliasTransactionV2.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala
index 0e83073..ae19e71 100644
--- a/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala
@@ -1,16 +1,17 @@
package com.zbsnetwork.transaction
+import cats.implicits._
import com.google.common.primitives.{Bytes, Longs, Shorts}
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto._
import com.zbsnetwork.state._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
import play.api.libs.json._
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class DataTransaction private (sender: PublicKeyAccount, data: List[DataEntry[_]], fee: Long, timestamp: Long, proofs: Proofs)
extends ProvenTransaction
@@ -54,25 +55,21 @@ object DataTransaction extends TransactionParserFor[DataTransaction] with Transa
val MaxEntryCount = 100
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val p0 = KeyLength
- val sender = PublicKeyAccount(bytes.slice(0, p0))
-
- val entryCount = Shorts.fromByteArray(bytes.drop(p0))
- val (entries, p1) =
- if (entryCount > 0) {
- val parsed = List.iterate(DataEntry.parse(bytes, p0 + 2), entryCount) { case (e, p) => DataEntry.parse(bytes, p) }
- (parsed.map(_._1), parsed.last._2)
- } else (List.empty, p0 + 2)
-
- val timestamp = Longs.fromByteArray(bytes.drop(p1))
- val feeAmount = Longs.fromByteArray(bytes.drop(p1 + 8))
- val txEi = for {
- proofs <- Proofs.fromBytes(bytes.drop(p1 + 16))
- tx <- create(sender, entries, feeAmount, timestamp, proofs)
- } yield tx
- txEi.fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ (
+ if (tx.data.lengthCompare(MaxEntryCount) > 0 || tx.data.exists(!_.valid)) {
+ Left(ValidationError.TooBigArray)
+ } else if (tx.data.exists(_.key.isEmpty)) {
+ Left(ValidationError.GenericError("Empty key found"))
+ } else if (tx.data.map(_.key).distinct.lengthCompare(tx.data.size) < 0) {
+ Left(ValidationError.GenericError("Duplicate keys found"))
+ } else if (tx.fee <= 0) {
+ Left(ValidationError.InsufficientFee)
+ } else {
+ Either.cond(tx.bytes().length <= MaxBytes, tx, ValidationError.TooBigArray)
+ }
+ ).foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -107,4 +104,23 @@ object DataTransaction extends TransactionParserFor[DataTransaction] with Transa
def selfSigned(sender: PrivateKeyAccount, data: List[DataEntry[_]], feeAmount: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, data, feeAmount, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[DataTransaction] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ ListDataEntryBytes(tailIndex(2)),
+ LongBytes(tailIndex(3), "Timestamp"),
+ LongBytes(tailIndex(4), "Fee"),
+ ProofsBytes(tailIndex(5))
+ ) mapN {
+ case (senderPublicKey, data, timestamp, fee, proofs) =>
+ DataTransaction(
+ sender = senderPublicKey,
+ data = data,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala b/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala
index ee9c91f..6d12ce1 100644
--- a/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala
@@ -5,6 +5,11 @@ import com.zbsnetwork.crypto
import monix.eval.Coeval
trait FastHashId extends ProvenTransaction {
+ val id: Coeval[AssetId] = Coeval.evalOnce(FastHashId.create(this.bodyBytes()))
+}
- val id: Coeval[AssetId] = Coeval.evalOnce(ByteStr(crypto.fastHash(bodyBytes())))
+object FastHashId {
+ def create(bodyBytes: Array[Byte]): AssetId = {
+ ByteStr(crypto.fastHash(bodyBytes))
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala
index e6f4e98..c30f0d0 100644
--- a/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala
@@ -1,15 +1,17 @@
package com.zbsnetwork.transaction
+import cats.implicits._
import com.google.common.primitives.{Bytes, Ints, Longs}
import com.zbsnetwork.account.Address
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction.TransactionParsers._
+import com.zbsnetwork.transaction.description.{AddressBytes, ByteEntity, LongBytes}
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class GenesisTransaction private (recipient: Address, amount: Long, timestamp: Long, signature: ByteStr) extends Transaction {
@@ -66,22 +68,14 @@ object GenesisTransaction extends TransactionParserFor[GenesisTransaction] with
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
Try {
- require(bytes.length >= BASE_LENGTH, "Data does not match base length")
-
- var position = 0
-
- val timestampBytes = java.util.Arrays.copyOfRange(bytes, position, position + TimestampLength)
- val timestamp = Longs.fromByteArray(timestampBytes)
- position += TimestampLength
-
- val recipientBytes = java.util.Arrays.copyOfRange(bytes, position, position + RECIPIENT_LENGTH)
- val recipient = Address.fromBytes(recipientBytes).explicitGet()
- position += RECIPIENT_LENGTH
- val amountBytes = java.util.Arrays.copyOfRange(bytes, position, position + AmountLength)
- val amount = Longs.fromByteArray(amountBytes)
+ require(bytes.length >= BASE_LENGTH, "Data does not match base length")
- GenesisTransaction.create(recipient, amount, timestamp).fold(left => Failure(new Exception(left.toString)), right => Success(right))
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.amount >= 0, tx, ValidationError.NegativeAmount(tx.amount, "zbs"))
+ .foldToTry
+ }
}.flatten
}
@@ -93,4 +87,20 @@ object GenesisTransaction extends TransactionParserFor[GenesisTransaction] with
Right(GenesisTransaction(recipient, amount, timestamp, signature))
}
}
+
+ val byteTailDescription: ByteEntity[GenesisTransaction] = {
+ (
+ LongBytes(tailIndex(1), "Timestamp"),
+ AddressBytes(tailIndex(2), "Recipient's address"),
+ LongBytes(tailIndex(3), "Amount")
+ ) mapN {
+ case (timestamp, recipient, amount) =>
+ GenesisTransaction(
+ recipient = recipient,
+ amount = amount,
+ timestamp = timestamp,
+ signature = ByteStr(generateSignature(recipient, amount, timestamp))
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala
index 3a79362..a2a9198 100644
--- a/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala
@@ -1,7 +1,6 @@
package com.zbsnetwork.transaction
-import java.util
-
+import cats.implicits._
import com.google.common.primitives.{Bytes, Ints, Longs}
import com.zbsnetwork.account.{Address, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
@@ -9,10 +8,11 @@ import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.crypto._
import com.zbsnetwork.transaction.TransactionParsers._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class PaymentTransaction private (sender: PublicKeyAccount, recipient: Address, amount: Long, fee: Long, timestamp: Long, signature: ByteStr)
extends SignedTransaction {
@@ -78,41 +78,43 @@ object PaymentTransaction extends TransactionParserFor[PaymentTransaction] with
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
Try {
- require(bytes.length >= BaseLength, "Data does not match base length")
-
- var position = 0
-
- //READ TIMESTAMP
- val timestampBytes = bytes.take(TimestampLength)
- val timestamp = Longs.fromByteArray(timestampBytes)
- position += TimestampLength
-
- //READ SENDER
- val senderBytes = util.Arrays.copyOfRange(bytes, position, position + SenderLength)
- val sender = PublicKeyAccount(senderBytes)
- position += SenderLength
-
- //READ RECIPIENT
- val recipientBytes = util.Arrays.copyOfRange(bytes, position, position + RecipientLength)
- val recipient = Address.fromBytes(recipientBytes).explicitGet()
- position += RecipientLength
- //READ AMOUNT
- val amountBytes = util.Arrays.copyOfRange(bytes, position, position + AmountLength)
- val amount = Longs.fromByteArray(amountBytes)
- position += AmountLength
-
- //READ FEE
- val feeBytes = util.Arrays.copyOfRange(bytes, position, position + FeeLength)
- val fee = Longs.fromByteArray(feeBytes)
- position += FeeLength
-
- //READ SIGNATURE
- val signatureBytes = util.Arrays.copyOfRange(bytes, position, position + SignatureLength)
+ require(bytes.length >= BaseLength, "Data does not match base length")
- PaymentTransaction
- .create(sender, recipient, amount, fee, timestamp, ByteStr(signatureBytes))
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ (
+ if (tx.amount <= 0) {
+ Left(ValidationError.NegativeAmount(tx.amount, "zbs")) //CHECK IF AMOUNT IS POSITIVE
+ } else if (tx.fee <= 0) {
+ Left(ValidationError.InsufficientFee) //CHECK IF FEE IS POSITIVE
+ } else if (Try(Math.addExact(tx.amount, tx.fee)).isFailure) {
+ Left(ValidationError.OverflowError) // CHECK THAT fee+amount won't overflow Long
+ } else {
+ Right(tx)
+ }
+ ).foldToTry
+ }
}.flatten
}
+
+ val byteTailDescription: ByteEntity[PaymentTransaction] = {
+ (
+ LongBytes(tailIndex(1), "Timestamp"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ AddressBytes(tailIndex(3), "Recipient's address"),
+ LongBytes(tailIndex(4), "Amount"),
+ LongBytes(tailIndex(5), "Fee"),
+ SignatureBytes(tailIndex(6), "Signature")
+ ) mapN {
+ case (timestamp, senderPublicKey, recipient, amount, fee, signature) =>
+ PaymentTransaction(
+ sender = senderPublicKey,
+ recipient = recipient,
+ amount = amount,
+ fee = fee,
+ timestamp = timestamp,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/Proofs.scala b/src/main/scala/com/zbsnetwork/transaction/Proofs.scala
index 8905392..c818c8e 100644
--- a/src/main/scala/com/zbsnetwork/transaction/Proofs.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/Proofs.scala
@@ -1,7 +1,7 @@
package com.zbsnetwork.transaction
import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.common.utils.{Base58, EitherExt2}
+import com.zbsnetwork.common.utils.Base58
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.utils.base58Length
@@ -12,19 +12,17 @@ import scala.util.Try
case class Proofs(proofs: List[ByteStr]) {
val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Proofs.Version +: Deser.serializeArrays(proofs.map(_.arr)))
val base58: Coeval[Seq[String]] = Coeval.evalOnce(proofs.map(p => Base58.encode(p.arr)))
+ def toSignature: ByteStr = proofs.headOption.getOrElse(ByteStr.empty)
override def toString: String = s"Proofs(${proofs.mkString(", ")})"
}
object Proofs {
-
- def apply(proofs: Seq[AssetId]): Proofs = new Proofs(proofs.toList)
-
val Version = 1: Byte
val MaxProofs = 8
val MaxProofSize = 64
val MaxProofStringSize = base58Length(MaxProofSize)
- lazy val empty = create(List.empty).explicitGet()
+ lazy val empty = new Proofs(Nil)
def create(proofs: Seq[ByteStr]): Either[ValidationError, Proofs] =
for {
@@ -38,4 +36,7 @@ object Proofs {
arrs <- Try(Deser.parseArrays(ab.tail)).toEither.left.map(er => GenericError(er.toString))
r <- create(arrs.map(ByteStr(_)).toList)
} yield r
+
+ implicit def apply(proofs: Seq[ByteStr]): Proofs = new Proofs(proofs.toList)
+ implicit def toSeq(proofs: Proofs): Seq[ByteStr] = proofs.proofs
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/Signed.scala b/src/main/scala/com/zbsnetwork/transaction/Signed.scala
index fb3a0e1..e7d0df8 100644
--- a/src/main/scala/com/zbsnetwork/transaction/Signed.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/Signed.scala
@@ -11,10 +11,13 @@ import scala.concurrent.duration.Duration
trait Signed extends Authorized {
protected val signatureValid: Coeval[Boolean]
+
@ApiModelProperty(hidden = true)
protected val signedDescendants: Coeval[Seq[Signed]] = Coeval.evalOnce(Seq.empty)
+
@ApiModelProperty(hidden = true)
protected val signaturesValidMemoized: Task[Either[InvalidSignature, this.type]] = Signed.validateTask[this.type](this).memoize
+
@ApiModelProperty(hidden = true)
val signaturesValid: Coeval[Either[InvalidSignature, this.type]] =
Coeval.evalOnce(Await.result(signaturesValidMemoized.runAsync(Signed.scheduler), Duration.Inf))
diff --git a/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala b/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala
index 8a57773..fd7161a 100644
--- a/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala
@@ -1,5 +1,8 @@
package com.zbsnetwork.transaction
+import cats.implicits._
+import com.zbsnetwork.transaction.description.{ByteEntity, ConstantByte, OneByte}
+
import scala.reflect.ClassTag
import scala.util.Try
@@ -12,11 +15,49 @@ trait TransactionParser {
def supportedVersions: Set[Byte]
def parseBytes(bytes: Array[Byte]): Try[TransactionT] =
- parseHeader(bytes) flatMap (offset => parseTail(bytes.drop(offset)))
+ parseHeader(bytes) flatMap (offset => parseTail(bytes drop offset))
/** @return offset */
protected def parseHeader(bytes: Array[Byte]): Try[Int]
protected def parseTail(bytes: Array[Byte]): Try[TransactionT]
+
+ /** Byte description of the header of the transaction */
+ val byteHeaderDescription: ByteEntity[Unit]
+
+ /**
+ * Byte description of the transaction. Can be used for deserialization.
+ *
+ * Implementation example:
+ * {{{
+ * val bytesTailDescription: ByteEntity[Transaction] =
+ * (
+ * OneByte(1, "Transaction type"),
+ * OneByte(2, "Version"),
+ * LongBytes(3, "Fee")
+ * ) mapN { case (txType, version, fee) => Transaction(txType, version, fee) }
+ *
+ * // deserialization from buf: Array[Byte]
+ * val tx: Try[Transaction] = byteTailDescription.deserializeFromByteArray(buf)
+ * }}}
+ */
+ val byteTailDescription: ByteEntity[TransactionT]
+
+ /**
+ * Returns index of byte entity in `byteTailDescription`
+ * taking into account the last index in `byteHeaderDescription`
+ */
+ protected def tailIndex(index: Int): Int = byteHeaderDescription.index + index
+
+ /**
+ * Full byte description of the transaction (header + tail). Can be used for deserialization and generation of the documentation.
+ *
+ * Usage example:
+ * {{{
+ * // generation of the documentation
+ * val txStringDocumentationForMD: String = byteDescription.getStringDocForMD
+ * }}}
+ */
+ lazy val byteDescription: ByteEntity[TransactionT] = (byteHeaderDescription, byteTailDescription) mapN { case (_, tx) => tx }
}
object TransactionParser {
@@ -35,6 +76,10 @@ object TransactionParser {
1
}
+
+ lazy val byteHeaderDescription: ByteEntity[Unit] = {
+ ConstantByte(1, typeId, "Transaction type") map (_ => Unit)
+ }
}
trait OneVersion extends TransactionParser {
@@ -55,6 +100,13 @@ object TransactionParser {
2
}
+
+ lazy val byteHeaderDescription: ByteEntity[Unit] = {
+ (
+ ConstantByte(1, value = typeId, name = "Transaction type"),
+ ConstantByte(2, value = version, name = "Version")
+ ) mapN ((_, _) => Unit)
+ }
}
trait MultipleVersions extends TransactionParser {
@@ -72,8 +124,15 @@ object TransactionParser {
3
}
- }
+ lazy val byteHeaderDescription: ByteEntity[Unit] = {
+ (
+ ConstantByte(1, value = 0, name = "Transaction multiple version mark"),
+ ConstantByte(2, value = typeId, name = "Transaction type"),
+ OneByte(3, "Version")
+ ) mapN ((_, _, _) => Unit)
+ }
+ }
}
abstract class TransactionParserFor[T <: Transaction](implicit override val classTag: ClassTag[T]) extends TransactionParser {
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala
index 98f6be8..39cfde1 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala
@@ -1,9 +1,7 @@
package com.zbsnetwork.transaction.assets
import com.google.common.primitives.{Bytes, Longs}
-import com.zbsnetwork.account.PublicKeyAccount
import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.crypto._
import com.zbsnetwork.transaction._
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
@@ -50,16 +48,8 @@ object BurnTransaction {
val typeId: Byte = 6
- def parseBase(start: Int, bytes: Array[Byte]): (PublicKeyAccount, AssetId, Long, Long, Long, Int) = {
- val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength))
- val assetId = ByteStr(bytes.slice(start + KeyLength, start + KeyLength + AssetIdLength))
- val quantityStart = start + KeyLength + AssetIdLength
-
- val quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8))
- val fee = Longs.fromByteArray(bytes.slice(quantityStart + 8, quantityStart + 16))
- val timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 16, quantityStart + 24))
-
- (sender, assetId, quantity, fee, timestamp, quantityStart + 24)
+ def validateBurnParams(tx: BurnTransaction): Either[ValidationError, Unit] = {
+ validateBurnParams(tx.quantity, tx.fee)
}
def validateBurnParams(amount: Long, fee: Long): Either[ValidationError, Unit] =
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala
index 4b81c87..7d464d2 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
-import com.zbsnetwork.crypto._
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class BurnTransactionV1 private (sender: PublicKeyAccount, assetId: ByteStr, quantity: Long, fee: Long, timestamp: Long, signature: ByteStr)
extends BurnTransaction
@@ -28,13 +30,12 @@ object BurnTransactionV1 extends TransactionParserFor[BurnTransactionV1] with Tr
override val typeId: Byte = BurnTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val (sender, assetId, quantity, fee, timestamp, end) = BurnTransaction.parseBase(0, bytes)
- val signature = ByteStr(bytes.slice(end, end + SignatureLength))
- BurnTransactionV1
- .create(sender, assetId, quantity, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ BurnTransaction
+ .validateBurnParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -62,4 +63,15 @@ object BurnTransactionV1 extends TransactionParserFor[BurnTransactionV1] with Tr
def selfSigned(sender: PrivateKeyAccount, assetId: ByteStr, quantity: Long, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, assetId, quantity, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[BurnTransactionV1] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(2), "Asset ID", AssetIdLength),
+ LongBytes(tailIndex(3), "Quantity"),
+ LongBytes(tailIndex(4), "Fee"),
+ LongBytes(tailIndex(5), "Timestamp"),
+ SignatureBytes(tailIndex(6), "Signature")
+ ) mapN BurnTransactionV1.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala
index 19121b7..5ff0cd9 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
-import com.zbsnetwork.transaction._
-import cats.implicits._
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
+import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
final case class BurnTransactionV2 private (chainId: Byte,
sender: PublicKeyAccount,
@@ -39,18 +41,12 @@ object BurnTransactionV2 extends TransactionParserFor[BurnTransactionV2] with Tr
override val supportedVersions: Set[Byte] = Set(2)
override protected def parseTail(bytes: Array[Byte]): Try[BurnTransactionV2] = {
- Try {
- val chainId = bytes(0)
- val (sender, assetId, quantity, fee, timestamp, end) = BurnTransaction.parseBase(1, bytes)
-
- (for {
- proofs <- Proofs.fromBytes(bytes.drop(end))
- tx <- create(chainId, sender, assetId, quantity, fee, timestamp, proofs)
- } yield tx).fold(
- err => Failure(new Exception(err.toString)),
- t => Success(t)
- )
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ BurnTransaction
+ .validateBurnParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(chainId: Byte,
@@ -86,4 +82,16 @@ object BurnTransactionV2 extends TransactionParserFor[BurnTransactionV2] with Tr
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(chainId, sender, assetId, quantity, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[BurnTransactionV2] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength),
+ LongBytes(tailIndex(4), "Quantity"),
+ LongBytes(tailIndex(5), "Fee"),
+ LongBytes(tailIndex(6), "Timestamp"),
+ ProofsBytes(tailIndex(7))
+ ) mapN BurnTransactionV2.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala
index beb5650..f67b25c 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala
@@ -4,8 +4,6 @@ import java.nio.charset.StandardCharsets
import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
-import com.zbsnetwork.account.PublicKeyAccount
-import com.zbsnetwork.crypto._
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.smart.script.Script
import com.zbsnetwork.transaction.validation._
@@ -57,6 +55,10 @@ object IssueTransaction {
val MinAssetNameLength = 4
val MaxDecimals = 8
+ def validateIssueParams(tx: IssueTransaction): Either[ValidationError, Unit] = {
+ validateIssueParams(tx.name, tx.description, tx.quantity, tx.decimals, tx.reissuable, tx.fee)
+ }
+
def validateIssueParams(name: Array[Byte],
description: Array[Byte],
quantity: Long,
@@ -73,16 +75,4 @@ object IssueTransaction {
.leftMap(_.head)
.toEither
}
-
- def parseBase(bytes: Array[Byte], start: Int) = {
- val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength))
- val (assetName, descriptionStart) = Deser.parseArraySize(bytes, start + KeyLength)
- val (description, quantityStart) = Deser.parseArraySize(bytes, descriptionStart)
- val quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8))
- val decimals = bytes.slice(quantityStart + 8, quantityStart + 9).head
- val reissuable = bytes.slice(quantityStart + 9, quantityStart + 10).head == (1: Byte)
- val fee = Longs.fromByteArray(bytes.slice(quantityStart + 10, quantityStart + 18))
- val timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 18, quantityStart + 26))
- (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, quantityStart + 26)
- }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala
index 8e891d0..0e1046b 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala
@@ -1,16 +1,18 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto.SignatureLength
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import com.zbsnetwork.transaction.smart.script.Script
import monix.eval.Coeval
import play.api.libs.json.JsObject
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class IssueTransactionV1 private (sender: PublicKeyAccount,
name: Array[Byte],
@@ -37,16 +39,14 @@ object IssueTransactionV1 extends TransactionParserFor[IssueTransactionV1] with
override val typeId: Byte = IssueTransaction.typeId
- override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] =
- Try {
- val signature = ByteStr(bytes.slice(0, SignatureLength))
- val txId = bytes(SignatureLength)
- require(txId == typeId, s"Signed tx id is not match")
- val (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, _) = IssueTransaction.parseBase(bytes, SignatureLength + 1)
- IssueTransactionV1
- .create(sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ IssueTransaction
+ .validateIssueParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
+ }
def create(sender: PublicKeyAccount,
name: Array[Byte],
@@ -86,4 +86,33 @@ object IssueTransactionV1 extends TransactionParserFor[IssueTransactionV1] with
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, name, description, quantity, decimals, reissuable, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[IssueTransactionV1] = {
+ (
+ SignatureBytes(tailIndex(1), "Signature"),
+ ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"),
+ PublicKeyAccountBytes(tailIndex(3), "Sender's public key"),
+ BytesArrayUndefinedLength(tailIndex(4), "Asset name"),
+ BytesArrayUndefinedLength(tailIndex(5), "Description"),
+ LongBytes(tailIndex(6), "Quantity"),
+ OneByte(tailIndex(7), "Decimals"),
+ BooleanByte(tailIndex(8), "Reissuable flag (1 - True, 0 - False)"),
+ LongBytes(tailIndex(9), "Fee"),
+ LongBytes(tailIndex(10), "Timestamp")
+ ) mapN {
+ case (signature, txId, senderPublicKey, name, desc, quantity, decimals, reissuable, fee, timestamp) =>
+ require(txId == typeId, s"Signed tx id is not match")
+ IssueTransactionV1(
+ sender = senderPublicKey,
+ name = name,
+ description = desc,
+ quantity = quantity,
+ decimals = decimals,
+ reissuable = reissuable,
+ fee = fee,
+ timestamp = timestamp,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala
index 6c7ee66..53872bf 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala
@@ -1,13 +1,16 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
-import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader}
+import com.zbsnetwork.transaction.description._
+import com.zbsnetwork.transaction.smart.script.Script
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
@@ -53,25 +56,13 @@ object IssueTransactionV2 extends TransactionParserFor[IssueTransactionV2] with
private def currentChainId = AddressScheme.current.chainId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val chainId = bytes(0)
- val (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, scriptStart) = IssueTransaction.parseBase(bytes, 1)
- val (scriptOptEi: Option[Either[ValidationError.ScriptParseError, Script]], scriptEnd) =
- Deser.parseOption(bytes, scriptStart)(ScriptReader.fromBytes)
- val scriptEiOpt: Either[ValidationError.ScriptParseError, Option[Script]] = scriptOptEi match {
- case None => Right(None)
- case Some(Right(sc)) => Right(Some(sc))
- case Some(Left(err)) => Left(err)
- }
-
- (for {
- proofs <- Proofs.fromBytes(bytes.drop(scriptEnd))
- script <- scriptEiOpt
- tx <- IssueTransactionV2
- .create(chainId, sender, assetName, description, quantity, decimals, reissuable, script, fee, timestamp, proofs)
- } yield tx).left.map(e => new Throwable(e.toString)).toTry
-
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId"))
+ .flatMap(_ => IssueTransaction.validateIssueParams(tx))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(chainId: Byte,
@@ -120,4 +111,35 @@ object IssueTransactionV2 extends TransactionParserFor[IssueTransactionV2] with
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(chainId, sender, name, description, quantity, decimals, reissuable, script, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[IssueTransactionV2] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ BytesArrayUndefinedLength(tailIndex(3), "Name"),
+ BytesArrayUndefinedLength(tailIndex(4), "Description"),
+ LongBytes(tailIndex(5), "Quantity"),
+ OneByte(tailIndex(6), "Decimals"),
+ BooleanByte(tailIndex(7), "Reissuable flag (1 - True, 0 - False)"),
+ LongBytes(tailIndex(8), "Fee"),
+ LongBytes(tailIndex(9), "Timestamp"),
+ OptionBytes(index = tailIndex(10), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(10), "Script")),
+ ProofsBytes(tailIndex(11))
+ ) mapN {
+ case (chainId, senderPublicKey, name, desc, quantity, decimals, reissuable, fee, timestamp, script, proofs) =>
+ IssueTransactionV2(
+ chainId = chainId,
+ sender = senderPublicKey,
+ name = name,
+ description = desc,
+ quantity = quantity,
+ decimals = decimals,
+ reissuable = reissuable,
+ script = script,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala
index 4ff73b3..70a4e76 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala
@@ -2,13 +2,11 @@ package com.zbsnetwork.transaction.assets
import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
-import monix.eval.Coeval
-import play.api.libs.json.{JsObject, Json}
-import com.zbsnetwork.account.PublicKeyAccount
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.transaction.validation._
import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, _}
-import com.zbsnetwork.crypto._
+import monix.eval.Coeval
+import play.api.libs.json.{JsObject, Json}
trait ReissueTransaction extends ProvenTransaction with VersionedTransaction {
def assetId: ByteStr
@@ -45,23 +43,13 @@ object ReissueTransaction {
val typeId: Byte = 5
+ def validateReissueParams(tx: ReissueTransaction): Either[ValidationError, Unit] = {
+ validateReissueParams(tx.quantity, tx.fee)
+ }
+
def validateReissueParams(quantity: Long, fee: Long): Either[ValidationError, Unit] =
(validateAmount(quantity, "assets"), validateFee(fee))
.mapN { case _ => () }
.leftMap(_.head)
.toEither
-
- def parseBase(bytes: Array[Byte], start: Int): (PublicKeyAccount, AssetId, Long, Boolean, Long, Long, Int) = {
- val senderEnd = start + KeyLength
- val assetIdEnd = senderEnd + AssetIdLength
- val sender = PublicKeyAccount(bytes.slice(start, senderEnd))
- val assetId = ByteStr(bytes.slice(senderEnd, assetIdEnd))
- val quantity = Longs.fromByteArray(bytes.slice(assetIdEnd, assetIdEnd + 8))
- val reissuable = bytes.slice(assetIdEnd + 8, assetIdEnd + 9).head == (1: Byte)
- val fee = Longs.fromByteArray(bytes.slice(assetIdEnd + 9, assetIdEnd + 17))
- val end = assetIdEnd + 25
- val timestamp = Longs.fromByteArray(bytes.slice(assetIdEnd + 17, end))
-
- (sender, assetId, quantity, reissuable, fee, timestamp, end)
- }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala
index f5b4dea..2c5e43e 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
-import com.zbsnetwork.crypto._
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class ReissueTransactionV1 private (sender: PublicKeyAccount,
assetId: ByteStr,
@@ -34,15 +36,12 @@ object ReissueTransactionV1 extends TransactionParserFor[ReissueTransactionV1] w
override val typeId: Byte = ReissueTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val signature = ByteStr(bytes.slice(0, SignatureLength))
- val txId = bytes(SignatureLength)
- require(txId == typeId, s"Signed tx id is not match")
- val (sender, assetId, quantity, reissuable, fee, timestamp, _) = ReissueTransaction.parseBase(bytes, SignatureLength + 1)
- ReissueTransactionV1
- .create(sender, assetId, quantity, reissuable, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ ReissueTransaction
+ .validateReissueParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -79,4 +78,29 @@ object ReissueTransactionV1 extends TransactionParserFor[ReissueTransactionV1] w
unsigned.copy(signature = ByteStr(crypto.sign(sender, unsigned.bodyBytes())))
}
}
+
+ val byteTailDescription: ByteEntity[ReissueTransactionV1] = {
+ (
+ SignatureBytes(tailIndex(1), "Signature"),
+ ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"),
+ PublicKeyAccountBytes(tailIndex(3), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(4), "Asset ID", AssetIdLength),
+ LongBytes(tailIndex(5), "Quantity"),
+ BooleanByte(tailIndex(6), "Reissuable flag (1 - True, 0 - False)"),
+ LongBytes(tailIndex(7), "Fee"),
+ LongBytes(tailIndex(8), "Timestamp")
+ ) mapN {
+ case (signature, txId, sender, assetId, quantity, reissuable, fee, timestamp) =>
+ require(txId == typeId, s"Signed tx id is not match")
+ ReissueTransactionV1(
+ sender = sender,
+ assetId = assetId,
+ quantity = quantity,
+ reissuable = reissuable,
+ fee = fee,
+ timestamp = timestamp,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala
index a3ac06d..74e5c9f 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala
@@ -1,11 +1,14 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
import scala.util._
@@ -45,16 +48,13 @@ object ReissueTransactionV2 extends TransactionParserFor[ReissueTransactionV2] w
private def currentChainId: Byte = AddressScheme.current.chainId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val chainId = bytes(0)
- val (sender, assetId, quantity, reissuable, fee, timestamp, end) = ReissueTransaction.parseBase(bytes, 1)
- (for {
- proofs <- Proofs.fromBytes(bytes.drop(end))
- tx <- ReissueTransactionV2
- .create(chainId, sender, assetId, quantity, reissuable, fee, timestamp, proofs)
- } yield tx)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId"))
+ .flatMap(_ => ReissueTransaction.validateReissueParams(tx))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(chainId: Byte,
@@ -94,4 +94,17 @@ object ReissueTransactionV2 extends TransactionParserFor[ReissueTransactionV2] w
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(chainId, sender, assetId, quantity, reissuable, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[ReissueTransactionV2] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength),
+ LongBytes(tailIndex(4), "Quantity"),
+ BooleanByte(tailIndex(5), "Reissuable flag (1 - True, 0 - False)"),
+ LongBytes(tailIndex(6), "Fee"),
+ LongBytes(tailIndex(7), "Timestamp"),
+ ProofsBytes(tailIndex(8))
+ ) mapN ReissueTransactionV2.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala
index d51eea4..d323e42 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala
@@ -1,18 +1,20 @@
package com.zbsnetwork.transaction.assets
-import cats.data.State
+import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
import com.zbsnetwork.account._
import com.zbsnetwork.common.state.ByteStr
-import monix.eval.Coeval
-import play.api.libs.json.{JsObject, Json}
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto._
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction._
-import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader}
+import com.zbsnetwork.transaction.description._
+import com.zbsnetwork.transaction.smart.script.Script
+import com.zbsnetwork.transaction.smart.script.v1.ExprScript
+import monix.eval.Coeval
+import play.api.libs.json.{JsObject, Json}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class SetAssetScriptTransaction private (chainId: Byte,
sender: PublicKeyAccount,
@@ -70,7 +72,11 @@ object SetAssetScriptTransaction extends TransactionParserFor[SetAssetScriptTran
fee: Long,
timestamp: Long,
proofs: Proofs): Either[ValidationError, TransactionT] = {
+
for {
+ _ <- Either.cond(script.fold(true)(_.isInstanceOf[ExprScript]),
+ (),
+ ValidationError.GenericError(s"Asset can oly be assigned with Expression script, not Contract"))
_ <- Either.cond(chainId == currentChainId,
(),
ValidationError.GenericError(s"Wrong chainId actual: ${chainId.toInt}, expected: $currentChainId"))
@@ -90,40 +96,34 @@ object SetAssetScriptTransaction extends TransactionParserFor[SetAssetScriptTran
}
}
override def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- val readByte: State[Int, Byte] = State { from =>
- (from + 1, bytes(from))
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == currentChainId, (), ValidationError.GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId"))
+ .map(_ => tx)
+ .foldToTry
}
- def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from =>
- val end = from + size
- (end, f(bytes.slice(from, end)))
- }
- def readUnsized[T](f: (Array[Byte], Int) => (T, Int)): State[Int, T] = State { from =>
- val (v, end) = f(bytes, from)
- (end, v)
- }
- def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from =>
- (from, f(bytes.drop(from)))
- }
-
- Try {
- val makeTransaction = for {
- chainId <- readByte
- sender <- read(PublicKeyAccount.apply, KeyLength)
- assetId <- read(ByteStr.apply, AssetIdLength)
- fee <- read(Longs.fromByteArray _, 8)
- timestamp <- read(Longs.fromByteArray _, 8)
- scriptOrE <- readUnsized((b: Array[Byte], p: Int) => Deser.parseOption(b, p)(ScriptReader.fromBytes))
- proofs <- readEnd(Proofs.fromBytes)
- } yield {
- (scriptOrE match {
- case Some(Left(err)) => Left(err)
- case Some(Right(s)) => Right(Some(s))
- case None => Right(None)
- }).flatMap(script => create(chainId, sender, assetId, script, fee, timestamp, proofs.right.get))
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }
- makeTransaction.run(0).value._2
- }.flatten
}
+ val byteTailDescription: ByteEntity[SetAssetScriptTransaction] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength),
+ LongBytes(tailIndex(4), "Fee"),
+ LongBytes(tailIndex(5), "Timestamp"),
+ OptionBytes(index = tailIndex(6), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(6), "Script")),
+ ProofsBytes(tailIndex(7))
+ ) mapN {
+ case (chainId, sender, assetId, fee, timestamp, script, proofs) =>
+ SetAssetScriptTransaction(
+ chainId = chainId,
+ sender = sender,
+ assetId = assetId,
+ script = script,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala
index 9f21bba..617ea88 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala
@@ -1,16 +1,17 @@
package com.zbsnetwork.transaction.assets
+import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto._
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class SponsorFeeTransaction private (sender: PublicKeyAccount,
assetId: ByteStr,
@@ -60,26 +61,17 @@ object SponsorFeeTransaction extends TransactionParserFor[SponsorFeeTransaction]
override val supportedVersions: Set[Byte] = Set(version)
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val txId = bytes(0)
- require(txId == typeId, s"Signed tx id is not match")
- val bodyVersion = bytes(1)
- require(bodyVersion == version, s"versions are not match ($version, $bodyVersion)")
- val sender = PublicKeyAccount(bytes.slice(2, KeyLength + 2))
- val assetId = ByteStr(bytes.slice(KeyLength + 2, KeyLength + AssetIdLength + 2))
- val minFeeStart = KeyLength + AssetIdLength + 2
-
- val minFee = Longs.fromByteArray(bytes.slice(minFeeStart, minFeeStart + 8))
- val fee = Longs.fromByteArray(bytes.slice(minFeeStart + 8, minFeeStart + 16))
- val timestamp = Longs.fromByteArray(bytes.slice(minFeeStart + 16, minFeeStart + 24))
- val tx = for {
- proofs <- Proofs.fromBytes(bytes.drop(minFeeStart + 24))
- tx <- SponsorFeeTransaction.create(sender, assetId, Some(minFee).filter(_ != 0), fee, timestamp, proofs)
- } yield {
- tx
- }
- tx.fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ (
+ if (tx.minSponsoredAssetFee.exists(_ < 0)) {
+ Left(ValidationError.NegativeMinFee(tx.minSponsoredAssetFee.get, "asset"))
+ } else if (tx.fee <= 0) {
+ Left(ValidationError.InsufficientFee())
+ } else {
+ Right(tx)
+ }
+ ).foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -115,4 +107,29 @@ object SponsorFeeTransaction extends TransactionParserFor[SponsorFeeTransaction]
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, assetId, minSponsoredAssetFee, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[SponsorFeeTransaction] = {
+ (
+ OneByte(tailIndex(1), "Transaction type"),
+ OneByte(tailIndex(2), "Version"),
+ PublicKeyAccountBytes(tailIndex(3), "Sender's public key"),
+ ByteStrDefinedLength(tailIndex(4), "Asset ID", AssetIdLength),
+ SponsorFeeOptionLongBytes(tailIndex(5), "Minimal fee in assets*"),
+ LongBytes(tailIndex(6), "Fee"),
+ LongBytes(tailIndex(7), "Timestamp"),
+ ProofsBytes(tailIndex(8))
+ ) mapN {
+ case (txId, bodyVersion, sender, assetId, minSponsoredAssetFee, fee, timestamp, proofs) =>
+ require(txId == typeId, s"Signed tx id is not match")
+ require(bodyVersion == version, s"versions are not match ($version, $bodyVersion)")
+ SponsorFeeTransaction(
+ sender = sender,
+ assetId = assetId,
+ minSponsoredAssetFee = minSponsoredAssetFee,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala
index a030211..e84bc3e 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala
@@ -58,6 +58,10 @@ object ExchangeTransaction {
else ExchangeTransactionV1.parseBytes(bytes)
}
+ def validateExchangeParams(tx: ExchangeTransaction): Either[ValidationError, Unit] = {
+ validateExchangeParams(tx.buyOrder, tx.sellOrder, tx.amount, tx.price, tx.buyMatcherFee, tx.sellMatcherFee, tx.fee, tx.timestamp)
+ }
+
def validateExchangeParams(buyOrder: Order,
sellOrder: Order,
amount: Long,
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala
index 0f2c4e7..6bf03cf 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala
@@ -1,17 +1,18 @@
package com.zbsnetwork.transaction.assets.exchange
-import cats.data.State
+import cats.implicits._
import com.google.common.primitives.{Ints, Longs}
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto._
import com.zbsnetwork.transaction._
import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction._
+import com.zbsnetwork.transaction.description._
import io.swagger.annotations.ApiModelProperty
import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class ExchangeTransactionV1(buyOrder: OrderV1,
sellOrder: OrderV1,
@@ -25,7 +26,8 @@ case class ExchangeTransactionV1(buyOrder: OrderV1,
extends ExchangeTransaction
with SignedTransaction {
- override def version: Byte = 1
+ override def version: Byte = 1
+
override val builder = ExchangeTransactionV1
override val assetFee: (Option[AssetId], Long) = (None, fee)
@@ -88,29 +90,40 @@ object ExchangeTransactionV1 extends TransactionParserFor[ExchangeTransactionV1]
}
override def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from =>
- val end = from + size
- (end, f(bytes.slice(from, end)))
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ ExchangeTransaction
+ .validateExchangeParams(tx)
+ .map(_ => tx)
+ .foldToTry
}
+ }
- Try {
- val makeTransaction = for {
- o1Size <- read(Ints.fromByteArray _, 4)
- o2Size <- read(Ints.fromByteArray _, 4)
- o1 <- read(OrderV1.parseBytes _, o1Size).map(_.get)
- o2 <- read(OrderV1.parseBytes _, o2Size).map(_.get)
- price <- read(Longs.fromByteArray _, 8)
- amount <- read(Longs.fromByteArray _, 8)
- buyMatcherFee <- read(Longs.fromByteArray _, 8)
- sellMatcherFee <- read(Longs.fromByteArray _, 8)
- fee <- read(Longs.fromByteArray _, 8)
- timestamp <- read(Longs.fromByteArray _, 8)
- signature <- read(ByteStr.apply, SignatureLength)
- } yield {
- create(o1, o2, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }
- makeTransaction.run(0).value._2
- }.flatten
+ val byteTailDescription: ByteEntity[ExchangeTransactionV1] = {
+ (
+ IntBytes(tailIndex(1), "Buy order object length (BN)"),
+ IntBytes(tailIndex(2), "Sell order object length (SN)"),
+ OrderV1Bytes(tailIndex(3), "Buy order object", "BN"),
+ OrderV1Bytes(tailIndex(4), "Sell order object", "SN"),
+ LongBytes(tailIndex(5), "Price"),
+ LongBytes(tailIndex(6), "Amount"),
+ LongBytes(tailIndex(7), "Buy matcher fee"),
+ LongBytes(tailIndex(8), "Sell matcher fee"),
+ LongBytes(tailIndex(9), "Fee"),
+ LongBytes(tailIndex(10), "Timestamp"),
+ SignatureBytes(tailIndex(11), "Signature")
+ ) mapN {
+ case (_, _, buyOrder, sellOrder, price, amount, buyMatcherFee, sellMatcherFee, fee, timestamp, signature) =>
+ ExchangeTransactionV1(
+ buyOrder = buyOrder,
+ sellOrder = sellOrder,
+ amount = amount,
+ price = price,
+ buyMatcherFee = buyMatcherFee,
+ sellMatcherFee = sellMatcherFee,
+ fee = fee,
+ timestamp = timestamp,
+ signature = signature
+ )
+ }
}
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala
index 9427e4e..37d3ea3 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala
@@ -1,16 +1,18 @@
package com.zbsnetwork.transaction.assets.exchange
-import cats.data.State
+import cats.implicits._
import com.google.common.primitives.{Ints, Longs}
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction._
+import com.zbsnetwork.transaction.description._
import io.swagger.annotations.ApiModelProperty
import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class ExchangeTransactionV2(buyOrder: Order,
sellOrder: Order,
@@ -96,42 +98,38 @@ object ExchangeTransactionV2 extends TransactionParserFor[ExchangeTransactionV2]
}
override def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- def back(off: Int): State[Int, Unit] = State { from =>
- (from - off, ())
- }
- val readByte: State[Int, Byte] = State { from =>
- (from + 1, bytes(from))
- }
- def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from =>
- val end = from + size
- (end, f(bytes.slice(from, end)))
- }
- def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from =>
- (from, f(bytes.drop(from)))
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ ExchangeTransaction
+ .validateExchangeParams(tx)
+ .map(_ => tx)
+ .foldToTry
}
+ }
- Try {
- val makeTransaction = for {
- o1Size <- read(Ints.fromByteArray _, 4)
- o1Ver <- readByte
- _ <- back(if (o1Ver != 1) { 1 } else { 0 })
- o1 <- read(if (o1Ver == 1) { OrderV1.parseBytes _ } else { OrderV2.parseBytes _ }, o1Size).map(_.get)
- o2Size <- read(Ints.fromByteArray _, 4)
- o2Ver <- readByte
- _ <- back(if (o2Ver != 1) { 1 } else { 0 })
- o2 <- read(if (o2Ver == 1) { OrderV1.parseBytes _ } else { OrderV2.parseBytes _ }, o2Size).map(_.get)
- price <- read(Longs.fromByteArray _, 8)
- amount <- read(Longs.fromByteArray _, 8)
- buyMatcherFee <- read(Longs.fromByteArray _, 8)
- sellMatcherFee <- read(Longs.fromByteArray _, 8)
- fee <- read(Longs.fromByteArray _, 8)
- timestamp <- read(Longs.fromByteArray _, 8)
- proofs <- readEnd(Proofs.fromBytes)
- } yield {
- create(o1, o2, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs.right.get)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }
- makeTransaction.run(0).value._2
- }.flatten
+ val byteTailDescription: ByteEntity[ExchangeTransactionV2] = {
+ (
+ OrderBytes(tailIndex(1), "Buy order"),
+ OrderBytes(tailIndex(2), "Sell order"),
+ LongBytes(tailIndex(3), "Price"),
+ LongBytes(tailIndex(4), "Amount"),
+ LongBytes(tailIndex(5), "Buy matcher fee"),
+ LongBytes(tailIndex(6), "Sell matcher fee"),
+ LongBytes(tailIndex(7), "Fee"),
+ LongBytes(tailIndex(8), "Timestamp"),
+ ProofsBytes(tailIndex(9))
+ ) mapN {
+ case (buyOrder, sellOrder, price, amount, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs) =>
+ ExchangeTransactionV2(
+ buyOrder = buyOrder,
+ sellOrder = sellOrder,
+ amount = amount,
+ price = price,
+ buyMatcherFee = buyMatcherFee,
+ sellMatcherFee = sellMatcherFee,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
}
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala
index 82e6e80..7004603 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala
@@ -7,6 +7,7 @@ import com.zbsnetwork.crypto
import com.zbsnetwork.serialization.{BytesSerializable, JsonSerializable}
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.assets.exchange.OrderOps._
import com.zbsnetwork.transaction.assets.exchange.Validation.booleanOperators
import com.zbsnetwork.utils.byteStrWrites
import io.swagger.annotations.ApiModelProperty
@@ -44,6 +45,8 @@ trait Order extends BytesSerializable with JsonSerializable with Proven {
def signature: Array[Byte] = proofs.proofs(0).arr
+ def matcherFeeAssetId: Option[AssetId] = None
+
import Order._
@ApiModelProperty(hidden = true)
@@ -160,7 +163,8 @@ trait Order extends BytesSerializable with JsonSerializable with Proven {
@ApiModelProperty(hidden = true)
override def toString: String = {
- s"OrderV$version(id=${idStr()}, sender=$senderPublicKey, matcher=$matcherPublicKey, pair=$assetPair, tpe=$orderType, amount=$amount, price=$price, ts=$timestamp, exp=$expiration, fee=$matcherFee, proofs=$proofs)"
+ val matcherFeeAssetIdStr = if (version == 3) s" matcherFeeAssetId=${matcherFeeAssetId.fold("Zbs")(_.toString)}," else ""
+ s"OrderV$version(id=${idStr()}, sender=$senderPublicKey, matcher=$matcherPublicKey, pair=$assetPair, tpe=$orderType, amount=$amount, price=$price, ts=$timestamp, exp=$expiration, fee=$matcherFee,$matcherFeeAssetIdStr proofs=$proofs)"
}
}
@@ -181,12 +185,9 @@ object Order {
expiration: Long,
matcherFee: Long,
proofs: Proofs,
- version: Byte = 1): Order = {
- if (version == 1) {
- OrderV1(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs)
- } else {
- OrderV2(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs)
- }
+ version: Byte = 1): Order = version match {
+ case 1 => OrderV1(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs)
+ case 2 => OrderV2(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs)
}
def apply(senderPublicKey: PublicKeyAccount,
@@ -198,17 +199,11 @@ object Order {
timestamp: Long,
expiration: Long,
matcherFee: Long,
- signature: Array[Byte]): Order = {
- OrderV1(senderPublicKey,
- matcherPublicKey,
- assetPair,
- orderType,
- amount,
- price,
- timestamp,
- expiration,
- matcherFee,
- Proofs(Seq(ByteStr(signature))))
+ proofs: Proofs,
+ version: Byte,
+ matcherFeeAssetId: Option[AssetId]): Order = version match {
+ case 3 =>
+ OrderV3(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, proofs)
}
def correctAmount(a: Long, price: Long): Long = {
@@ -226,8 +221,13 @@ object Order {
timestamp: Long,
expiration: Long,
matcherFee: Long,
- version: Byte = 1): Order = {
- val unsigned = Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version)
+ version: Byte = 1,
+ matcherFeeAssetId: Option[AssetId] = None): Order = {
+ val unsigned = version match {
+ case 3 =>
+ Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId)
+ case _ => Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version)
+ }
sign(unsigned, sender)
}
@@ -239,8 +239,13 @@ object Order {
timestamp: Long,
expiration: Long,
matcherFee: Long,
- version: Byte = 1): Order = {
- val unsigned = Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version)
+ version: Byte = 1,
+ matcherFeeAssetId: Option[AssetId] = None): Order = {
+ val unsigned = version match {
+ case 3 =>
+ Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId)
+ case _ => Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version)
+ }
sign(unsigned, sender)
}
@@ -258,15 +263,25 @@ object Order {
sign(unsigned, sender)
}
+ def apply(sender: PrivateKeyAccount,
+ matcher: PublicKeyAccount,
+ pair: AssetPair,
+ orderType: OrderType,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ version: Byte,
+ matcherFeeAssetId: Option[AssetId]): Order = {
+ val unsigned = Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId)
+ sign(unsigned, sender)
+ }
+
def sign(unsigned: Order, sender: PrivateKeyAccount): Order = {
require(unsigned.senderPublicKey == sender)
val sig = crypto.sign(sender, unsigned.bodyBytes())
- unsigned match {
- case o @ OrderV2(_, _, _, _, _, _, _, _, _, _) =>
- o.copy(proofs = Proofs(Seq(ByteStr(sig))))
- case o @ OrderV1(_, _, _, _, _, _, _, _, _, _) =>
- o.copy(proofs = Proofs(Seq(ByteStr(sig))))
- }
+ unsigned.updateProofs(Proofs(Seq(ByteStr(sig))))
}
def splitByType(o1: Order, o2: Order): (Order, Order) = {
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala
index 1eece71..968ec60 100644
--- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala
@@ -4,7 +4,7 @@ import com.zbsnetwork.account.PublicKeyAccount
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.Base58
import com.zbsnetwork.crypto.SignatureLength
-import com.zbsnetwork.transaction.Proofs
+import com.zbsnetwork.transaction.{AssetId, Proofs}
import play.api.libs.json._
import scala.util.{Failure, Success}
@@ -42,19 +42,36 @@ object OrderJson {
case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.expected.jsstring"))))
}
- def readOrder(sender: PublicKeyAccount,
- matcher: PublicKeyAccount,
- assetPair: AssetPair,
- orderType: OrderType,
- amount: Long,
- price: Long,
- timestamp: Long,
- expiration: Long,
- matcherFee: Long,
- signature: Option[Array[Byte]],
- proofs: Option[Array[Array[Byte]]],
- version: Option[Byte]): Order = {
- val eproofs = proofs.map(p => Proofs(p.map(ByteStr.apply))).orElse(signature.map(s => Proofs(Seq(ByteStr(s))))).getOrElse(Proofs.empty)
+ implicit val assetIdReads: Reads[AssetId] = {
+ case JsString(s) =>
+ Base58.decode(s) match {
+ case Success(bytes) => JsSuccess(ByteStr(bytes))
+ case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.incorrect.assetId"))))
+ }
+ case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.expected.jsstring"))))
+ }
+
+ def readOrderV1V2(sender: PublicKeyAccount,
+ matcher: PublicKeyAccount,
+ assetPair: AssetPair,
+ orderType: OrderType,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ signature: Option[Array[Byte]],
+ proofs: Option[Array[Array[Byte]]],
+ version: Option[Byte]): Order = {
+
+ val eproofs =
+ proofs
+ .map(p => Proofs(p.map(ByteStr.apply)))
+ .orElse(signature.map(s => Proofs(Seq(ByteStr(s)))))
+ .getOrElse(Proofs.empty)
+
+ val vrsn: Byte = version.getOrElse(if (eproofs.proofs.size == 1 && eproofs.proofs.head.arr.length == SignatureLength) 1 else 2)
+
Order(
sender,
matcher,
@@ -66,7 +83,43 @@ object OrderJson {
expiration,
matcherFee,
eproofs,
- version.getOrElse(if (eproofs.proofs.size == 1 && eproofs.proofs.head.arr.length == SignatureLength) 1 else 2)
+ vrsn
+ )
+ }
+
+ def readOrderV3(sender: PublicKeyAccount,
+ matcher: PublicKeyAccount,
+ assetPair: AssetPair,
+ orderType: OrderType,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ signature: Option[Array[Byte]],
+ proofs: Option[Array[Array[Byte]]],
+ version: Byte,
+ matcherFeeAssetId: Option[AssetId]): Order = {
+
+ val eproofs =
+ proofs
+ .map(p => Proofs(p.map(ByteStr.apply)))
+ .orElse(signature.map(s => Proofs(Seq(ByteStr(s)))))
+ .getOrElse(Proofs.empty)
+
+ Order(
+ sender,
+ matcher,
+ assetPair,
+ orderType,
+ amount,
+ price,
+ timestamp,
+ expiration,
+ matcherFee,
+ eproofs,
+ version,
+ matcherFeeAssetId
)
}
@@ -83,7 +136,7 @@ object OrderJson {
implicit val orderTypeReads: Reads[OrderType] =
JsPath.read[String].map(OrderType.apply)
- implicit val orderReads: Reads[Order] = {
+ private val orderV1V2Reads: Reads[Order] = {
val r = (JsPath \ "senderPublicKey").read[PublicKeyAccount] and
(JsPath \ "matcherPublicKey").read[PublicKeyAccount] and
(JsPath \ "assetPair").read[AssetPair] and
@@ -96,7 +149,33 @@ object OrderJson {
(JsPath \ "signature").readNullable[Array[Byte]] and
(JsPath \ "proofs").readNullable[Array[Array[Byte]]] and
(JsPath \ "version").readNullable[Byte]
- r(readOrder _)
+ r(readOrderV1V2 _)
+ }
+
+ private val orderV3Reads: Reads[Order] = {
+ val r = (JsPath \ "senderPublicKey").read[PublicKeyAccount] and
+ (JsPath \ "matcherPublicKey").read[PublicKeyAccount] and
+ (JsPath \ "assetPair").read[AssetPair] and
+ (JsPath \ "orderType").read[OrderType] and
+ (JsPath \ "amount").read[Long] and
+ (JsPath \ "price").read[Long] and
+ (JsPath \ "timestamp").read[Long] and
+ (JsPath \ "expiration").read[Long] and
+ (JsPath \ "matcherFee").read[Long] and
+ (JsPath \ "signature").readNullable[Array[Byte]] and
+ (JsPath \ "proofs").readNullable[Array[Array[Byte]]] and
+ (JsPath \ "version").read[Byte] and
+ (JsPath \ "matcherFeeAssetId").readNullable[AssetId]
+ r(readOrderV3 _)
+ }
+
+ implicit val orderReads: Reads[Order] = {
+ case jsOrder @ JsObject(map) =>
+ map.getOrElse("version", JsNumber(1)) match {
+ case JsNumber(x) if x.byteValue() == 3 => orderV3Reads.reads(jsOrder)
+ case _ => orderV1V2Reads.reads(jsOrder)
+ }
+ case invalidOrder => JsError(s"Can't parse invalid order $invalidOrder")
}
implicit val orderFormat: Format[Order] = Format(orderReads, Writes[Order](_.json()))
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala
new file mode 100644
index 0000000..b64f809
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala
@@ -0,0 +1,90 @@
+package com.zbsnetwork.transaction.assets.exchange
+
+import com.zbsnetwork.account.PrivateKeyAccount
+import com.zbsnetwork.transaction.Proofs
+
+class OrderOps(val o: Order) extends AnyVal {
+ @inline def copy(withV1: OrderV1 => OrderV1, withV2: OrderV2 => OrderV2, withV3: OrderV3 => OrderV3): Order = {
+ o match {
+ case o1: OrderV1 => withV1(o1)
+ case o2: OrderV2 => withV2(o2)
+ case o3: OrderV3 => withV3(o3)
+ }
+ }
+
+ @inline def updateProofs(p: Proofs): Order = {
+ copy(
+ _.copy(proofs = p),
+ _.copy(proofs = p),
+ _.copy(proofs = p)
+ )
+ }
+
+ @inline def updateExpiration(expiration: Long): Order = {
+ copy(
+ _.copy(expiration = expiration),
+ _.copy(expiration = expiration),
+ _.copy(expiration = expiration)
+ )
+ }
+ @inline def updateTimestamp(timestamp: Long): Order = {
+ copy(
+ _.copy(timestamp = timestamp),
+ _.copy(timestamp = timestamp),
+ _.copy(timestamp = timestamp)
+ )
+ }
+ @inline def updateFee(fee: Long): Order = {
+ copy(
+ _.copy(matcherFee = fee),
+ _.copy(matcherFee = fee),
+ _.copy(matcherFee = fee)
+ )
+ }
+ @inline def updateAmount(amount: Long): Order = {
+ copy(
+ _.copy(amount = amount),
+ _.copy(amount = amount),
+ _.copy(amount = amount)
+ )
+ }
+ @inline def updatePrice(price: Long): Order = {
+ copy(
+ _.copy(price = price),
+ _.copy(price = price),
+ _.copy(price = price)
+ )
+ }
+ @inline def updateMatcher(pk: PrivateKeyAccount): Order = {
+ copy(
+ _.copy(matcherPublicKey = pk),
+ _.copy(matcherPublicKey = pk),
+ _.copy(matcherPublicKey = pk)
+ )
+ }
+ @inline def updateSender(pk: PrivateKeyAccount): Order = {
+ copy(
+ _.copy(senderPublicKey = pk),
+ _.copy(senderPublicKey = pk),
+ _.copy(senderPublicKey = pk)
+ )
+ }
+ @inline def updatePair(pair: AssetPair): Order = {
+ copy(
+ _.copy(assetPair = pair),
+ _.copy(assetPair = pair),
+ _.copy(assetPair = pair)
+ )
+ }
+ @inline def updateType(t: OrderType): Order = {
+ copy(
+ _.copy(orderType = t),
+ _.copy(orderType = t),
+ _.copy(orderType = t)
+ )
+ }
+}
+
+object OrderOps {
+ implicit def toOps(o: Order): OrderOps = new OrderOps(o)
+}
diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala
new file mode 100644
index 0000000..e68d989
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala
@@ -0,0 +1,185 @@
+package com.zbsnetwork.transaction.assets.exchange
+
+import cats.data.State
+import com.google.common.primitives.Longs
+import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.Base58
+import com.zbsnetwork.crypto
+import com.zbsnetwork.crypto.KeyLength
+import com.zbsnetwork.serialization.Deser
+import com.zbsnetwork.transaction.{AssetId, Proofs}
+import com.zbsnetwork.utils.byteStrWrites
+import monix.eval.Coeval
+import play.api.libs.json.{JsObject, Json}
+
+import scala.util.Try
+
+case class OrderV3(senderPublicKey: PublicKeyAccount,
+ matcherPublicKey: PublicKeyAccount,
+ assetPair: AssetPair,
+ orderType: OrderType,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ override val matcherFeeAssetId: Option[AssetId],
+ proofs: Proofs)
+ extends Order {
+
+ def version: Byte = 3
+
+ override def signature: Array[Byte] = proofs.proofs.head.arr
+
+ val bodyBytes: Coeval[Array[Byte]] =
+ Coeval.evalOnce(
+ Array(version) ++
+ senderPublicKey.publicKey ++
+ matcherPublicKey.publicKey ++
+ assetPair.bytes ++
+ orderType.bytes ++
+ Longs.toByteArray(amount) ++
+ Longs.toByteArray(price) ++
+ Longs.toByteArray(timestamp) ++
+ Longs.toByteArray(expiration) ++
+ Longs.toByteArray(matcherFee) ++
+ Order.assetIdBytes(matcherFeeAssetId)
+ )
+
+ val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(bodyBytes() ++ proofs.bytes())
+
+ override val json: Coeval[JsObject] =
+ Coeval.evalOnce(
+ {
+ val sig = Base58.encode(signature)
+ Json.obj(
+ "version" -> version,
+ "id" -> idStr(),
+ "sender" -> senderPublicKey.address,
+ "senderPublicKey" -> Base58.encode(senderPublicKey.publicKey),
+ "matcherPublicKey" -> Base58.encode(matcherPublicKey.publicKey),
+ "assetPair" -> assetPair.json,
+ "orderType" -> orderType.toString,
+ "amount" -> amount,
+ "price" -> price,
+ "timestamp" -> timestamp,
+ "expiration" -> expiration,
+ "matcherFee" -> matcherFee,
+ "matcherFeeAssetId" -> matcherFeeAssetId.map(_.base58),
+ "signature" -> sig,
+ "proofs" -> proofs.proofs
+ )
+ }
+ )
+}
+
+object OrderV3 {
+
+ private val AssetIdLength = 32
+
+ def buy(sender: PrivateKeyAccount,
+ matcher: PublicKeyAccount,
+ pair: AssetPair,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ matcherFeeAssetId: Option[AssetId]): Order = {
+
+ val unsigned = OrderV3(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty)
+ val sig = crypto.sign(sender, unsigned.bodyBytes())
+
+ unsigned.copy(proofs = Proofs(Seq(ByteStr(sig))))
+ }
+
+ def sell(sender: PrivateKeyAccount,
+ matcher: PublicKeyAccount,
+ pair: AssetPair,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ matcherFeeAssetId: Option[AssetId]): Order = {
+
+ val unsigned = OrderV3(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty)
+ val sig = crypto.sign(sender, unsigned.bodyBytes())
+
+ unsigned.copy(proofs = Proofs(Seq(ByteStr(sig))))
+ }
+
+ def apply(sender: PrivateKeyAccount,
+ matcher: PublicKeyAccount,
+ pair: AssetPair,
+ orderType: OrderType,
+ amount: Long,
+ price: Long,
+ timestamp: Long,
+ expiration: Long,
+ matcherFee: Long,
+ matcherFeeAssetId: Option[AssetId]): Order = {
+
+ val unsigned = OrderV3(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty)
+ val sig = crypto.sign(sender, unsigned.bodyBytes())
+
+ unsigned.copy(proofs = Proofs(Seq(ByteStr(sig))))
+ }
+
+ def parseBytes(bytes: Array[Byte]): Try[Order] = Try {
+
+ val longLength = 8
+
+ val readByte: State[Int, Byte] = State { from =>
+ (from + 1, bytes(from))
+ }
+
+ def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from =>
+ val end = from + size
+ (end, f(bytes.slice(from, end)))
+ }
+
+ def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from =>
+ (from, f(bytes.drop(from)))
+ }
+
+ def parse[T](f: (Array[Byte], Int, Int) => (T, Int), size: Int): State[Int, T] = State { from =>
+ val (res, off) = f(bytes, from, size)
+ (off, res)
+ }
+
+ val makeOrder = for {
+ version <- readByte
+ _ = if (version != 3) { throw new Exception(s"Incorrect order version: expect 3 but found $version") }
+ sender <- read(PublicKeyAccount.apply, KeyLength)
+ matcher <- read(PublicKeyAccount.apply, KeyLength)
+ amountAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength)
+ priceAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength)
+ orderType <- readByte
+ amount <- read(Longs.fromByteArray, longLength)
+ price <- read(Longs.fromByteArray, longLength)
+ timestamp <- read(Longs.fromByteArray, longLength)
+ expiration <- read(Longs.fromByteArray, longLength)
+ matcherFee <- read(Longs.fromByteArray, longLength)
+ matcherFeeAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength)
+ maybeProofs <- readEnd(Proofs.fromBytes)
+ } yield {
+ OrderV3(
+ senderPublicKey = sender,
+ matcherPublicKey = matcher,
+ assetPair = AssetPair(amountAssetId.map(ByteStr.apply), priceAssetId.map(ByteStr.apply)),
+ orderType = OrderType(orderType),
+ amount = amount,
+ price = price,
+ timestamp = timestamp,
+ expiration = expiration,
+ matcherFee = matcherFee,
+ matcherFeeAssetId = matcherFeeAssetId.map(ByteStr.apply),
+ proofs = maybeProofs.right.get
+ )
+ }
+
+ makeOrder.run(0).value._2
+ }
+}
diff --git a/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala b/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala
new file mode 100644
index 0000000..0159dc8
--- /dev/null
+++ b/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala
@@ -0,0 +1,481 @@
+package com.zbsnetwork.transaction.description
+
+import cats.{Functor, Semigroupal}
+import com.google.common.primitives.{Ints, Longs, Shorts}
+import com.zbsnetwork.account.{Address, AddressOrAlias, Alias, PublicKeyAccount}
+import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto.{KeyLength, SignatureLength}
+import com.zbsnetwork.lang.v1.Serde
+import com.zbsnetwork.lang.v1.compiler.Terms
+import com.zbsnetwork.lang.v1.compiler.Terms.FUNCTION_CALL
+import com.zbsnetwork.serialization.Deser
+import com.zbsnetwork.state.DataEntry
+import com.zbsnetwork.transaction.ValidationError.Validation
+import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.assets.exchange._
+import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment
+import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader}
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Represents description of the byte entity
+ * Field `additionalInfo` can be used for specifying of the repeating byte entities
+ */
+case class ByteEntityDescription(index: Int, name: String, tpe: String, length: String, subIndex: Int = 0, additionalInfo: String = "")
+
+/**
+ * Describes byte representation of the different types. Composition of Byte Entities can be used for deserialization
+ * and generation of the documentation of the complex data structures, such as transactions, messages, orders, etc
+ */
+sealed trait ByteEntity[T] { self =>
+
+ private[description] val ByteType = "Byte"
+ private[description] val BooleanType = "Boolean"
+ private[description] val IntType = "Int"
+ private[description] val LongType = "Long"
+ private[description] val ByteArrayType = "Array[Byte]"
+ private[description] val ByteStrType = s"ByteStr ($ByteArrayType)"
+ private[description] val AddressType = "Address"
+ private[description] val AliasType = "Alias"
+ private[description] val AddressOrAliasType = "Address or Alias"
+ private[description] val OrderV1Type = "OrderV1"
+ private[description] val OrderType = "Order"
+ private[description] val UnimportantType = ""
+
+ /** Index of the byte entity. In case of composition of byte entities returns index of the last one */
+ val index: Int
+
+ private[description] def generateDoc: Seq[ByteEntityDescription]
+
+ private[description] def deserialize(buf: Array[Byte], offset: Int): Try[(T, Int)]
+
+ def deserializeFromByteArray(buf: Array[Byte]): Try[T] = deserialize(buf, 0) map { case (value, _) => value }
+
+ def map[U](f: T => U): ByteEntity[U] = new ByteEntity[U] {
+
+ val index: Int = self.index
+
+ def generateDoc: Seq[ByteEntityDescription] = self.generateDoc
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(U, Int)] = self.deserialize(buf, offset).map { case (t, o) => f(t) -> o }
+ }
+
+ /** Generates documentation ready for pasting into .md files */
+ def getStringDocForMD: String = {
+
+ val docs = generateDoc
+
+ docs
+ .map {
+ case ByteEntityDescription(idx, name, tpe, length, subIndex, additionalInfo) =>
+ s"| $idx${Option(subIndex).filter(_ != 0).fold("")(si => s".$si")} | $name | $tpe | $length $additionalInfo\n"
+ .replace("...", "| ... | ... | ... | ... |")
+ .replace("(", "\\(")
+ .replace(")", "\\)")
+ .replace("*", "\\*")
+ }
+ .foldLeft("""| \# | Field name | Type | Length |""" + "\n| --- | --- | --- | --- |\n")(_ + _)
+ }
+}
+
+object ByteEntity {
+
+ implicit def byteEntityFunctor: Functor[ByteEntity] = new Functor[ByteEntity] {
+ def map[A, B](fa: ByteEntity[A])(f: A => B): ByteEntity[B] = fa map f
+ }
+
+ implicit def byteEntitySemigroupal: Semigroupal[ByteEntity] = new Semigroupal[ByteEntity] {
+ def product[A, B](fa: ByteEntity[A], fb: ByteEntity[B]): ByteEntity[(A, B)] = Composition(fa, fb)
+ }
+}
+
+case class ConstantByte(index: Int, value: Byte, name: String) extends ByteEntity[Byte] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, s"$ByteType (constant, value = $value)", "1"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Byte, Int)] = {
+ Try { value -> (offset + 1) }
+ }
+}
+
+case class OneByte(index: Int, name: String) extends ByteEntity[Byte] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteType, "1"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Byte, Int)] = {
+ Try { buf(offset) -> (offset + 1) }
+ }
+}
+
+case class LongBytes(index: Int, name: String) extends ByteEntity[Long] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, LongType, "8"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Long, Int)] = {
+ Try { Longs.fromByteArray(buf.slice(offset, offset + 8)) -> (offset + 8) }
+ }
+}
+
+case class IntBytes(index: Int, name: String) extends ByteEntity[Int] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, IntType, "4"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Int, Int)] = {
+ Try { Ints.fromByteArray(buf.slice(offset, offset + 4)) -> (offset + 4) }
+ }
+}
+
+case class BooleanByte(index: Int, name: String) extends ByteEntity[Boolean] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, BooleanType, "1"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Boolean, Int)] = {
+ Try { (buf(offset) == 1) -> (offset + 1) }
+ }
+}
+
+case class BytesArrayDefinedLength(index: Int, name: String, length: Int) extends ByteEntity[Array[Byte]] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteArrayType, length.toString))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Array[Byte], Int)] = {
+ Try { buf.slice(offset, offset + length) -> (offset + length) }
+ }
+}
+
+case class BytesArrayUndefinedLength(index: Int, name: String) extends ByteEntity[Array[Byte]] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"$name length (N)", UnimportantType, "2", subIndex = 1),
+ ByteEntityDescription(index, name, ByteArrayType, "N", subIndex = 2)
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Array[Byte], Int)] = {
+ Try {
+ val length = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+ val (arrayStart, arrayEnd) = (offset + 2, offset + 2 + length)
+ buf.slice(arrayStart, arrayEnd) -> arrayEnd
+ }
+ }
+}
+
+case class ByteStrDefinedLength(index: Int, name: String, length: Int) extends ByteEntity[ByteStr] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(ByteEntityDescription(index, name, ByteStrType, length.toString))
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(ByteStr, Int)] = {
+ Try { ByteStr(buf.slice(offset, offset + length)) -> (offset + length) }
+ }
+}
+
+case class PublicKeyAccountBytes(index: Int, name: String) extends ByteEntity[PublicKeyAccount] {
+
+ def generateDoc: Seq[ByteEntityDescription] =
+ Seq(ByteEntityDescription(index, name, s"PublicKeyAccount ($ByteArrayType)", KeyLength.toString))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(PublicKeyAccount, Int)] = {
+ Try { PublicKeyAccount(buf.slice(offset, offset + KeyLength)) -> (offset + KeyLength) }
+ }
+}
+
+case class SignatureBytes(index: Int, name: String) extends ByteEntity[ByteStr] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteStrType, SignatureLength.toString))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(ByteStr, Int)] = {
+ Try { ByteStr(buf.slice(offset, offset + SignatureLength)) -> (offset + SignatureLength) }
+ }
+}
+
+case class SponsorFeeOptionLongBytes(index: Int, name: String) extends ByteEntity[Option[Long]] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(ByteEntityDescription(index, name, LongType, "8"))
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Option[Long], Int)] = {
+ Try { Option(Longs.fromByteArray(buf.slice(offset, offset + 8))).filter(_ != 0) -> (offset + 8) }
+ }
+}
+
+case class AddressBytes(index: Int, name: String) extends ByteEntity[Address] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(ByteEntityDescription(index, name, AddressType, s"${Address.AddressLength}"))
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Address, Int)] = {
+ Try {
+
+ val recipientBytes = java.util.Arrays.copyOfRange(buf, offset, offset + Address.AddressLength)
+ val recipient = Address.fromBytes(recipientBytes).explicitGet()
+
+ recipient -> (offset + Address.AddressLength)
+ }
+ }
+}
+
+case class AliasBytes(index: Int, name: String) extends ByteEntity[Alias] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"$name length (A)", UnimportantType, "2", subIndex = 1),
+ ByteEntityDescription(index, s"$name", AliasType, "A", subIndex = 2)
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Alias, Int)] = {
+ val aliasLength = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+ Alias
+ .fromBytes(buf.slice(offset + 2, offset + 2 + aliasLength))
+ .map(alias => alias -> (offset + 2 + aliasLength))
+ .fold(err => Failure(new Exception(err.toString)), Success.apply)
+ }
+}
+
+case class AddressOrAliasBytes(index: Int, name: String) extends ByteEntity[AddressOrAlias] {
+
+ def generateDoc: Seq[ByteEntityDescription] =
+ Seq(ByteEntityDescription(index, name, AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(AddressOrAlias, Int)] = {
+ Try { AddressOrAlias.fromBytes(buf, offset).explicitGet() }
+ }
+}
+
+case class ProofsBytes(index: Int) extends ByteEntity[Proofs] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"Proofs version (${Proofs.Version})", UnimportantType, "1", subIndex = 1),
+ ByteEntityDescription(index, "Proofs count", UnimportantType, "2", subIndex = 2),
+ ByteEntityDescription(index, "Proof 1 length (P1)", UnimportantType, "2", subIndex = 3),
+ ByteEntityDescription(index, "Proof 1", ByteStrType, "P1", subIndex = 4),
+ ByteEntityDescription(index, "Proof 2 length (P2)", UnimportantType, "2", subIndex = 5),
+ ByteEntityDescription(index, "Proof 2 ", ByteStrType, "P2", subIndex = 6, additionalInfo = "\n...")
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Proofs, Int)] = {
+ Try { Proofs.fromBytes(buf.drop(offset)).map(p => p -> (offset + p.bytes.value.length)).explicitGet() }
+ }
+}
+
+case class TransfersBytes(index: Int) extends ByteEntity[List[ParsedTransfer]] {
+
+ import cats.implicits._
+
+ private def readTransfer(buf: Array[Byte], offset: Int): (Validation[ParsedTransfer], Int) = {
+ AddressOrAlias.fromBytes(buf, offset) match {
+ case Right((addressOrAlias, ofs)) =>
+ val amount = Longs.fromByteArray(buf.slice(ofs, ofs + 8))
+ Right[ValidationError, ParsedTransfer](ParsedTransfer(addressOrAlias, amount)) -> (ofs + 8)
+ case Left(validationError) => Left(validationError) -> offset
+ }
+ }
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, "Number of transfers", UnimportantType, "2", 1),
+ ByteEntityDescription(index, "Address or alias for transfer 1", AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)", 2),
+ ByteEntityDescription(index, "Amount for transfer 1", LongType, "8", 3),
+ ByteEntityDescription(index, "Address or alias for transfer 2", AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)", 4),
+ ByteEntityDescription(index, "Amount for transfer 2", LongType, "8", 5, additionalInfo = "\n...")
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(List[ParsedTransfer], Int)] = {
+ Try {
+
+ val transferCount = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+
+ val transfersList: List[(Validation[ParsedTransfer], Int)] =
+ List.iterate(readTransfer(buf, offset + 2), transferCount) { case (_, offst) => readTransfer(buf, offst) }
+
+ val resultOffset = transfersList.lastOption.map(_._2).getOrElse(offset + 2)
+ val resultList = transfersList.map { case (ei, _) => ei }.sequence.explicitGet()
+
+ resultList -> resultOffset
+ }
+ }
+
+}
+
+case class OrderBytes(index: Int, name: String) extends ByteEntity[Order] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"$name size (N)", UnimportantType, "4", subIndex = 1),
+ ByteEntityDescription(index, s"$name version mark", UnimportantType, "1 (version 1) / 0 (version 2)", subIndex = 2),
+ ByteEntityDescription(index, name, OrderType, "N", subIndex = 3)
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Order, Int)] = {
+ Try {
+
+ val orderSize = Ints.fromByteArray(buf.slice(offset, offset + 4))
+ val orderMark = buf(offset + 4)
+
+ orderMark match {
+ case 1 => OrderV1.parseBytes(buf.drop(offset + 5)).map(order => order -> (offset + 5 + orderSize))
+ case 2 => OrderV2.parseBytes(buf.drop(offset + 4)).map(order => order -> (offset + 4 + orderSize))
+ case 3 => OrderV3.parseBytes(buf.drop(offset + 4)).map(order => order -> (offset + 4 + orderSize))
+ }
+ }.flatten
+ }
+}
+
+case class OrderV1Bytes(index: Int, name: String, length: String) extends ByteEntity[OrderV1] {
+
+ def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, OrderV1Type, s"$length"))
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(OrderV1, Int)] = {
+ OrderV1.parseBytes(buf.drop(offset)).map { order =>
+ order -> (offset + order.bytes.value.length)
+ }
+ }
+}
+
+case class ListDataEntryBytes(index: Int) extends ByteEntity[List[DataEntry[_]]] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, "Data entries count", UnimportantType, "2", subIndex = 1),
+ ByteEntityDescription(index, "Key 1 length (K1)", UnimportantType, "2", subIndex = 2),
+ ByteEntityDescription(index, "Key 1 bytes", "UTF-8 encoded", "K1", subIndex = 3),
+ ByteEntityDescription(index, "Value 1 type (0 = integer, 1 = boolean, 2 = binary array, 3 = string)", UnimportantType, "1", subIndex = 4),
+ ByteEntityDescription(index, "Value 1 bytes", "Value 1 type", "depends on value type", subIndex = 5, additionalInfo = "\n...")
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(List[DataEntry[_]], Int)] = {
+ Try {
+
+ val entryCount = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+
+ if (entryCount > 0) {
+ val parsed = List.iterate(DataEntry.parse(buf, offset + 2), entryCount) { case (_, p) => DataEntry.parse(buf, p) }
+ parsed.map(_._1) -> parsed.last._2
+ } else
+ List.empty -> (offset + 2)
+ }
+ }
+}
+
+case class FunctionCallBytes(index: Int, name: String) extends ByteEntity[Terms.FUNCTION_CALL] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(ByteEntityDescription(index, name, "EXPR", "F"))
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Terms.FUNCTION_CALL, Int)] = {
+ Try {
+ val (expr, remaining) = Serde.deserialize(buf.drop(offset), all = false).explicitGet()
+ expr.asInstanceOf[FUNCTION_CALL] -> (buf.length - remaining)
+ }
+ }
+}
+
+case class AssetIdBytes(index: Int, name: String) extends ByteEntity[AssetId] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(ByteEntityDescription(index, name, "AssetId (ByteStr = Array[Byte])", AssetIdLength.toString))
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(AssetId, Int)] = {
+ Try { ByteStr(buf.slice(offset, offset + AssetIdLength)) -> (offset + AssetIdLength) }
+ }
+}
+
+case class ScriptBytes(index: Int, name: String) extends ByteEntity[Script] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"$name length (S)", UnimportantType, "2", subIndex = 1),
+ ByteEntityDescription(index, name, "Script", "S", subIndex = 2)
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Script, Int)] = {
+ Try {
+ val scriptLength = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+ ScriptReader.fromBytes(buf.slice(offset + 2, offset + 2 + scriptLength)).explicitGet() -> (offset + 2 + scriptLength)
+ }
+ }
+}
+
+case class PaymentBytes(index: Int, name: String) extends ByteEntity[Payment] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ Seq(
+ ByteEntityDescription(index, s"$name length (P)", UnimportantType, "2", subIndex = 1),
+ ByteEntityDescription(index, name, "Payment (Long, Option[AssetId])", "P", subIndex = 2)
+ )
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Payment, Int)] = {
+ Try {
+
+ val paymentLength = Shorts.fromByteArray(buf.slice(offset, offset + 2))
+ val arr = buf.slice(offset + 2, offset + 2 + paymentLength)
+ val amt: Long = Longs.fromByteArray(arr.take(8))
+ val (maybeAsset: Option[AssetId], _) = Deser.parseOption(arr, 8)(ByteStr.apply)
+
+ Payment(amt, maybeAsset) -> (offset + 2 + paymentLength)
+ }
+ }
+}
+
+/**
+ * Represents byte description of Option[U]
+ *
+ * @param nestedByteEntity describes byte entity of type U
+ * @param firstByteInterpretation how to interpret first byte
+ */
+class OptionBytes[U](val index: Int, name: String, nestedByteEntity: ByteEntity[U], firstByteInterpretation: String = "existence flag (1/0)")
+ extends ByteEntity[Option[U]] {
+
+ def generateDoc: Seq[ByteEntityDescription] = {
+ ByteEntityDescription(index, s"$name $firstByteInterpretation", UnimportantType, "1", subIndex = 1) +:
+ nestedByteEntity.generateDoc.map { desc =>
+ desc.copy(
+ length = desc.length + s"/0 (depends on byte in $index.1)",
+ subIndex = if (desc.subIndex != 0) desc.subIndex + 1 else desc.subIndex + 2
+ )
+ }
+ }
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[(Option[U], Int)] = {
+ if (buf(offset) == 1) nestedByteEntity.deserialize(buf, offset + 1).map { case (value, offst) => Some(value) -> offst } else
+ Try { None -> (offset + 1) }
+ }
+}
+
+object OptionBytes {
+ def apply[U](index: Int,
+ name: String,
+ nestedByteEntity: ByteEntity[U],
+ firstByteInterpretation: String = "existence flag (1/0)"): ByteEntity[Option[U]] =
+ new OptionBytes(index, name, nestedByteEntity, firstByteInterpretation)
+}
+
+case class Composition[T1, T2](e1: ByteEntity[T1], e2: ByteEntity[T2]) extends ByteEntity[(T1, T2)] {
+
+ val index: Int = e2.index // use last index in composition
+
+ def generateDoc: Seq[ByteEntityDescription] = e1.generateDoc ++ e2.generateDoc
+
+ def deserialize(buf: Array[Byte], offset: Int): Try[((T1, T2), Int)] =
+ for {
+ (value1, offset1) <- e1.deserialize(buf, offset)
+ (value2, offset2) <- e2.deserialize(buf, offset1)
+ } yield ((value1, value2), offset2)
+}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala
index 8b1fced..86eaccc 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala
@@ -1,13 +1,11 @@
package com.zbsnetwork.transaction.lease
import com.google.common.primitives.{Bytes, Longs}
+import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.crypto
+import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction}
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
-import com.zbsnetwork.account.PublicKeyAccount
-import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction}
-import com.zbsnetwork.crypto.KeyLength
trait LeaseCancelTransaction extends ProvenTransaction with VersionedTransaction {
def chainByte: Option[Byte]
@@ -31,19 +29,14 @@ object LeaseCancelTransaction {
val typeId: Byte = 9
- def validateLeaseCancelParams(leaseId: ByteStr, fee: Long) =
+ def validateLeaseCancelParams(tx: LeaseCancelTransaction): Either[ValidationError, Unit] = {
+ validateLeaseCancelParams(tx.leaseId, tx.fee)
+ }
+
+ def validateLeaseCancelParams(leaseId: ByteStr, fee: Long): Either[ValidationError, Unit] =
if (leaseId.arr.length != crypto.DigestSize) {
Left(ValidationError.GenericError("Lease transaction id is invalid"))
} else if (fee <= 0) {
Left(ValidationError.InsufficientFee())
} else Right(())
-
- def parseBase(bytes: Array[Byte], start: Int) = {
- val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength))
- val fee = Longs.fromByteArray(bytes.slice(start + KeyLength, start + KeyLength + 8))
- val timestamp = Longs.fromByteArray(bytes.slice(start + KeyLength + 8, start + KeyLength + 16))
- val end = start + KeyLength + 16 + crypto.DigestSize
- val leaseId = ByteStr(bytes.slice(start + KeyLength + 16, end))
- (sender, fee, timestamp, leaseId, end)
- }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala
index 3f45a83..ab04e77 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.lease
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
-import com.zbsnetwork.crypto._
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class LeaseCancelTransactionV1 private (sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, signature: ByteStr)
extends LeaseCancelTransaction
@@ -32,13 +34,12 @@ object LeaseCancelTransactionV1 extends TransactionParserFor[LeaseCancelTransact
override val typeId: Byte = LeaseCancelTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val (sender, fee, timestamp, leaseId, end) = LeaseCancelTransaction.parseBase(bytes, 0)
- val signature = ByteStr(bytes.slice(end, KeyLength + 16 + crypto.DigestSize + SignatureLength))
- LeaseCancelTransactionV1
- .create(sender, leaseId, fee, timestamp, signature)
- .fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ LeaseCancelTransaction
+ .validateLeaseCancelParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, signature: ByteStr): Either[ValidationError, TransactionT] = {
@@ -58,4 +59,23 @@ object LeaseCancelTransactionV1 extends TransactionParserFor[LeaseCancelTransact
def selfSigned(sender: PrivateKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, leaseId, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[LeaseCancelTransactionV1] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ LongBytes(tailIndex(2), "Fee"),
+ LongBytes(tailIndex(3), "Timestamp"),
+ ByteStrDefinedLength(tailIndex(4), "Lease ID", crypto.DigestSize),
+ SignatureBytes(tailIndex(5), "Signature")
+ ) mapN {
+ case (sender, fee, timestamp, leaseId, signature) =>
+ LeaseCancelTransactionV1(
+ sender = sender,
+ leaseId = leaseId,
+ fee = fee,
+ timestamp = timestamp,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala
index 4fd2641..c9aa264 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala
@@ -1,5 +1,6 @@
package com.zbsnetwork.transaction.lease
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
@@ -7,9 +8,10 @@ import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class LeaseCancelTransactionV2 private (chainId: Byte, sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, proofs: Proofs)
extends LeaseCancelTransaction
@@ -35,14 +37,13 @@ object LeaseCancelTransactionV2 extends TransactionParserFor[LeaseCancelTransact
private def currentChainId: Byte = AddressScheme.current.chainId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val chainId = bytes(0)
- val (sender, fee, timestamp, leaseId, end) = LeaseCancelTransaction.parseBase(bytes, 1)
- (for {
- proofs <- Proofs.fromBytes(bytes.drop(end))
- tx <- LeaseCancelTransactionV2.create(chainId, sender, leaseId, fee, timestamp, proofs)
- } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId"))
+ .flatMap(_ => LeaseCancelTransaction.validateLeaseCancelParams(tx))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(chainId: Byte,
@@ -71,4 +72,25 @@ object LeaseCancelTransactionV2 extends TransactionParserFor[LeaseCancelTransact
def selfSigned(chainId: Byte, sender: PrivateKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(chainId, sender, leaseId, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[LeaseCancelTransactionV2] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ LongBytes(tailIndex(3), "Fee"),
+ LongBytes(tailIndex(4), "Timestamp"),
+ ByteStrDefinedLength(tailIndex(5), "Lease ID", crypto.DigestSize),
+ ProofsBytes(tailIndex(6))
+ ) mapN {
+ case (chainId, senderPublicKey, fee, timestamp, leaseId, proofs) =>
+ LeaseCancelTransactionV2(
+ chainId = chainId,
+ sender = senderPublicKey,
+ leaseId = leaseId,
+ fee = fee,
+ timestamp = timestamp,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala
index 3b595a5..433f0d3 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala
@@ -1,11 +1,11 @@
package com.zbsnetwork.transaction.lease
import com.google.common.primitives.{Bytes, Longs}
-import monix.eval.Coeval
-import play.api.libs.json.{JsObject, Json}
import com.zbsnetwork.account.{Address, AddressOrAlias, PublicKeyAccount}
import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction}
-import com.zbsnetwork.crypto._
+import monix.eval.Coeval
+import play.api.libs.json.{JsObject, Json}
+
import scala.util.Try
trait LeaseTransaction extends ProvenTransaction with VersionedTransaction {
@@ -36,7 +36,11 @@ object LeaseTransaction {
val Canceled = "canceled"
}
- def validateLeaseParams(amount: Long, fee: Long, recipient: AddressOrAlias, sender: PublicKeyAccount) =
+ def validateLeaseParams(tx: LeaseTransaction): Either[ValidationError, Unit] = {
+ validateLeaseParams(tx.amount, tx.fee, tx.recipient, tx.sender)
+ }
+
+ def validateLeaseParams(amount: Long, fee: Long, recipient: AddressOrAlias, sender: PublicKeyAccount): Either[ValidationError, Unit] =
if (amount <= 0) {
Left(ValidationError.NegativeAmount(amount, "zbs"))
} else if (Try(Math.addExact(amount, fee)).isFailure) {
@@ -46,18 +50,4 @@ object LeaseTransaction {
} else if (recipient.isInstanceOf[Address] && sender.stringRepr == recipient.stringRepr) {
Left(ValidationError.ToSelf)
} else Right(())
-
- def parseBase(bytes: Array[Byte], start: Int) = {
- val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength))
- for {
- recRes <- AddressOrAlias.fromBytes(bytes, start + KeyLength)
- (recipient, recipientEnd) = recRes
- quantityStart = recipientEnd
- quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8))
- fee = Longs.fromByteArray(bytes.slice(quantityStart + 8, quantityStart + 16))
- end = quantityStart + 24
- timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 16, end))
- } yield (sender, recipient, quantity, fee, timestamp, end)
- }
-
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala
index 1fe35ff..44c2030 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.lease
+import cats.implicits._
import com.google.common.primitives.Bytes
-import com.zbsnetwork.crypto
-import monix.eval.Coeval
import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
-import com.zbsnetwork.crypto.SignatureLength
+import com.zbsnetwork.transaction.description._
+import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class LeaseTransactionV1 private (sender: PublicKeyAccount,
amount: Long,
@@ -32,14 +34,12 @@ object LeaseTransactionV1 extends TransactionParserFor[LeaseTransactionV1] with
override val typeId: Byte = LeaseTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- (for {
- parsed <- LeaseTransaction.parseBase(bytes, 0)
- (sender, recipient, quantity, fee, timestamp, end) = parsed
- signature = ByteStr(bytes.slice(end, end + SignatureLength))
- lt <- LeaseTransactionV1.create(sender, quantity, fee, timestamp, recipient, signature)
- } yield lt).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ LeaseTransaction
+ .validateLeaseParams(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -71,4 +71,25 @@ object LeaseTransactionV1 extends TransactionParserFor[LeaseTransactionV1] with
recipient: AddressOrAlias): Either[ValidationError, TransactionT] = {
signed(sender, amount, fee, timestamp, recipient, sender)
}
+
+ val byteTailDescription: ByteEntity[LeaseTransactionV1] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ AddressOrAliasBytes(tailIndex(2), "Recipient"),
+ LongBytes(tailIndex(3), "Amount"),
+ LongBytes(tailIndex(4), "Fee"),
+ LongBytes(tailIndex(5), "Timestamp"),
+ SignatureBytes(tailIndex(6), "Signature")
+ ) mapN {
+ case (sender, recipient, amount, fee, timestamp, signature) =>
+ LeaseTransactionV1(
+ sender = sender,
+ amount = amount,
+ fee = fee,
+ timestamp = timestamp,
+ recipient = recipient,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala
index b01e066..9e7aa4d 100644
--- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala
@@ -1,14 +1,17 @@
package com.zbsnetwork.transaction.lease
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
-import scala.util.{Either, Failure, Success, Try}
+import scala.util.{Either, Try}
case class LeaseTransactionV2 private (sender: PublicKeyAccount, amount: Long, fee: Long, timestamp: Long, recipient: AddressOrAlias, proofs: Proofs)
extends LeaseTransaction
@@ -33,16 +36,14 @@ object LeaseTransactionV2 extends TransactionParserFor[LeaseTransactionV2] with
override val typeId: Byte = LeaseTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, 0, AssetIdLength)
- (for {
- _ <- Either.cond(assetIdOpt.isEmpty, (), ValidationError.GenericError("Leasing assets is not supported yet"))
- parsed <- LeaseTransaction.parseBase(bytes, s0)
- (sender, recipient, quantity, fee, timestamp, end) = parsed
- proofs <- Proofs.fromBytes(bytes.drop(end))
- lt <- LeaseTransactionV2.create(sender, quantity, fee, timestamp, recipient, proofs)
- } yield lt).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ val (assetIdOpt, _) = Deser.parseByteArrayOption(bytes, 0, AssetIdLength)
+ Either
+ .cond(assetIdOpt.isEmpty, (), ValidationError.GenericError("Leasing assets is not supported yet"))
+ .flatMap(_ => LeaseTransaction.validateLeaseParams(tx))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -75,4 +76,26 @@ object LeaseTransactionV2 extends TransactionParserFor[LeaseTransactionV2] with
recipient: AddressOrAlias): Either[ValidationError, TransactionT] = {
signed(sender, amount, fee, timestamp, recipient, sender)
}
+
+ val byteTailDescription: ByteEntity[LeaseTransactionV2] = {
+ (
+ OptionBytes(tailIndex(1), "Leasing asset", AssetIdBytes(tailIndex(1), "Leasing asset"), "flag (1 - asset, 0 - Zbs)"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ AddressOrAliasBytes(tailIndex(3), "Recipient"),
+ LongBytes(tailIndex(4), "Amount"),
+ LongBytes(tailIndex(5), "Fee"),
+ LongBytes(tailIndex(6), "Timestamp"),
+ ProofsBytes(tailIndex(7))
+ ) mapN {
+ case (_, senderPublicKey, recipient, amount, fee, timestamp, proofs) =>
+ LeaseTransactionV2(
+ sender = senderPublicKey,
+ amount = amount,
+ fee = fee,
+ timestamp = timestamp,
+ recipient = recipient,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala
index a7147ef..2f32ebf 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala
@@ -1,23 +1,24 @@
package com.zbsnetwork.transaction.smart
+import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
import com.zbsnetwork.account._
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto.KeyLength
-import com.zbsnetwork.lang.v1.Serde
import com.zbsnetwork.lang.v1.compiler.Terms
-import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FUNCTION_CALL, REF}
+import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, REF}
+import com.zbsnetwork.lang.v1.{ContractLimits, Serde}
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment
import com.zbsnetwork.utils.byteStrWrites
import monix.eval.Coeval
-import play.api.libs.json.{Format, JsObject}
+import play.api.libs.json.JsObject
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class ContractInvocationTransaction private (chainId: Byte,
sender: PublicKeyAccount,
@@ -70,8 +71,7 @@ case class ContractInvocationTransaction private (chainId: Byte,
object ContractInvocationTransaction extends TransactionParserFor[ContractInvocationTransaction] with TransactionParser.MultipleVersions {
- import play.api.libs.json._
- import play.api.libs.json.Json
+ import play.api.libs.json.{Json, _}
case class Payment(amount: Long, assetId: Option[AssetId])
@@ -82,10 +82,10 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca
"function" -> JsString(fc.function.asInstanceOf[com.zbsnetwork.lang.v1.FunctionHeader.User].name),
"args" -> JsArray(
fc.args.map {
- case Terms.CONST_LONG(l) => Json.obj("key" -> "", "type" -> "integer", "value" -> l)
- case Terms.CONST_BOOLEAN(l) => Json.obj("key" -> "", "type" -> "boolean", "value" -> l)
- case Terms.CONST_BYTESTR(l) => Json.obj("key" -> "", "type" -> "binary", "value" -> l.base64)
- case Terms.CONST_STRING(l) => Json.obj("key" -> "", "type" -> "string", "value" -> l)
+ case Terms.CONST_LONG(l) => Json.obj("type" -> "integer", "value" -> l)
+ case Terms.CONST_BOOLEAN(l) => Json.obj("type" -> "boolean", "value" -> l)
+ case Terms.CONST_BYTESTR(l) => Json.obj("type" -> "binary", "value" -> l.base64)
+ case Terms.CONST_STRING(l) => Json.obj("type" -> "string", "value" -> l)
case _ => ???
}
)
@@ -95,31 +95,25 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca
override val typeId: Byte = 16
override val supportedVersions: Set[Byte] = Set(1)
- private def currentChainId = AddressScheme.current.chainId
+ private def currentChainId: Byte = AddressScheme.current.chainId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val chainId = bytes(0)
- val sender = PublicKeyAccount(bytes.slice(1, KeyLength + 1))
- val contractAddress = Address.fromBytes(bytes.drop(KeyLength + 1).take(Address.AddressLength)).explicitGet()
- val fcStart = KeyLength + 1 + Address.AddressLength
- val rest = bytes.drop(fcStart)
- val (fc, remaining) = Serde.deserialize(rest, all = false).explicitGet()
- val paymentFeeTsProofs = rest.takeRight(remaining)
- val (payment: Option[(Option[AssetId], Long)], offset) = Deser.parseOption(paymentFeeTsProofs, 0)(arr => {
- val amt: Long = Longs.fromByteArray(arr.take(8))
- val (maybeAsset: Option[AssetId], offset) = Deser.parseOption(arr, 8)(ByteStr(_))
- (maybeAsset, amt)
- })
- val feeTsProofs = paymentFeeTsProofs.drop(offset)
- val fee = Longs.fromByteArray(feeTsProofs.slice(0, 8))
- val timestamp = Longs.fromByteArray(feeTsProofs.slice(8, 16))
- (for {
- _ <- Either.cond(chainId == currentChainId, (), GenericError(s"Wrong chainId ${chainId.toInt}"))
- proofs <- Proofs.fromBytes(feeTsProofs.drop(16))
- tx <- create(sender, contractAddress, fc.asInstanceOf[FUNCTION_CALL], payment.map(p => Payment(p._2, p._1)), fee, timestamp, proofs)
- } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId ${tx.chainId.toInt}"))
+ .flatMap(_ => Either.cond(tx.fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: ${tx.fee}")))
+ .flatMap(_ =>
+ tx.payment match {
+ case Some(Payment(amt, token)) => Either.cond(amt > 0, (), ValidationError.NegativeAmount(0, token.toString))
+ case _ => Right(())
+ })
+ .flatMap(_ =>
+ Either.cond(tx.fc.args.forall(x => x.isInstanceOf[EVALUATED] || x == REF("unit")),
+ (),
+ GenericError("all arguments of contractInvocation must be EVALUATED")))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount,
@@ -131,6 +125,11 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca
proofs: Proofs): Either[ValidationError, TransactionT] = {
for {
_ <- Either.cond(fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: $fee"))
+ _ <- Either.cond(
+ fc.args.size <= ContractLimits.MaxContractInvocationArgs,
+ (),
+ ValidationError.GenericError(s"ContractInvocation can't have more than ${ContractLimits.MaxContractInvocationArgs} arguments")
+ )
_ <- p match {
case Some(Payment(amt, token)) => Either.cond(amt > 0, (), ValidationError.NegativeAmount(0, token.toString))
case _ => Right(())
@@ -139,7 +138,10 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca
_ <- Either.cond(fc.args.forall(x => x.isInstanceOf[EVALUATED] || x == REF("unit")),
(),
GenericError("all arguments of contractInvocation must be EVALUATED"))
- } yield new ContractInvocationTransaction(currentChainId, sender, contractAddress, fc, p, fee, timestamp, proofs)
+ tx = new ContractInvocationTransaction(currentChainId, sender, contractAddress, fc, p, fee, timestamp, proofs)
+ size = tx.bytes().length
+ _ <- Either.cond(size <= ContractLimits.MaxContractInvocationSizeInBytes, (), ValidationError.TooBigArray)
+ } yield tx
}
def signed(sender: PublicKeyAccount,
@@ -162,4 +164,17 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca
timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, contractAddress, fc, p, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[ContractInvocationTransaction] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ AddressBytes(tailIndex(3), "Contract address"),
+ FunctionCallBytes(tailIndex(4), "Function call"),
+ OptionBytes(tailIndex(5), "Payment", PaymentBytes(tailIndex(5), "Payment")),
+ LongBytes(tailIndex(6), "Fee"),
+ LongBytes(tailIndex(7), "Timestamp"),
+ ProofsBytes(tailIndex(8))
+ ) mapN ContractInvocationTransaction.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala b/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala
index a9ad09e..e127adc 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala
@@ -48,7 +48,8 @@ object RealTransactionWrapper {
expiration = o.expiration,
matcherFee = o.matcherFee,
bodyBytes = ByteStr(o.bodyBytes()),
- proofs = o.proofs.proofs.map(a => ByteStr(a.arr)).toIndexedSeq
+ proofs = o.proofs.proofs.map(a => ByteStr(a.arr)).toIndexedSeq,
+ matcherFeeAssetId = o.matcherFeeAssetId
)
implicit def aoaToRecipient(aoa: AddressOrAlias): Recipient = aoa match {
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala
index 04c79aa..b9adbab 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala
@@ -1,19 +1,20 @@
package com.zbsnetwork.transaction.smart
+import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
import com.zbsnetwork.account._
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto.KeyLength
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.ValidationError.GenericError
import com.zbsnetwork.transaction._
-import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader}
+import com.zbsnetwork.transaction.description._
+import com.zbsnetwork.transaction.smart.script.Script
import monix.eval.Coeval
import play.api.libs.json.Json
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class SetScriptTransaction private (chainId: Byte, sender: PublicKeyAccount, script: Option[Script], fee: Long, timestamp: Long, proofs: Proofs)
extends ProvenTransaction
@@ -45,29 +46,16 @@ object SetScriptTransaction extends TransactionParserFor[SetScriptTransaction] w
override val typeId: Byte = 13
override val supportedVersions: Set[Byte] = Set(1)
- private def chainId = AddressScheme.current.chainId
+ private def chainId: Byte = AddressScheme.current.chainId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val chainId = bytes(0)
- val sender = PublicKeyAccount(bytes.slice(1, KeyLength + 1))
- val (scriptOptEi: Option[Either[ValidationError.ScriptParseError, Script]], scriptEnd) =
- Deser.parseOption(bytes, KeyLength + 1)(ScriptReader.fromBytes)
- val scriptEiOpt = scriptOptEi match {
- case None => Right(None)
- case Some(Right(sc)) => Right(Some(sc))
- case Some(Left(err)) => Left(err)
- }
-
- lazy val fee = Longs.fromByteArray(bytes.slice(scriptEnd, scriptEnd + 8))
- lazy val timestamp = Longs.fromByteArray(bytes.slice(scriptEnd + 8, scriptEnd + 16))
- (for {
- scriptOpt <- scriptEiOpt
- _ <- Either.cond(chainId == chainId, (), GenericError(s"Wrong chainId ${chainId.toInt}"))
- proofs <- Proofs.fromBytes(bytes.drop(scriptEnd + 16))
- tx <- create(sender, scriptOpt, fee, timestamp, proofs)
- } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Either
+ .cond(tx.chainId == chainId, (), GenericError(s"Wrong chainId ${tx.chainId.toInt}"))
+ .flatMap(_ => Either.cond(tx.fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: ${tx.fee}")))
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(sender: PublicKeyAccount, script: Option[Script], fee: Long, timestamp: Long, proofs: Proofs): Either[ValidationError, TransactionT] = {
@@ -89,4 +77,15 @@ object SetScriptTransaction extends TransactionParserFor[SetScriptTransaction] w
def selfSigned(sender: PrivateKeyAccount, script: Option[Script], fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = {
signed(sender, script, fee, timestamp, sender)
}
+
+ val byteTailDescription: ByteEntity[SetScriptTransaction] = {
+ (
+ OneByte(tailIndex(1), "Chain ID"),
+ PublicKeyAccountBytes(tailIndex(2), "Sender's public key"),
+ OptionBytes(index = tailIndex(3), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(3), "Script")),
+ LongBytes(tailIndex(4), "Fee"),
+ LongBytes(tailIndex(5), "Timestamp"),
+ ProofsBytes(tailIndex(6))
+ ) mapN SetScriptTransaction.apply
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala
index ed7a2f5..5560d93 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala
@@ -1,29 +1,32 @@
package com.zbsnetwork.transaction.smart.script
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.crypto
-import com.zbsnetwork.lang.ScriptType
-import com.zbsnetwork.lang.StdLibVersion.{StdLibVersion, V1}
+import com.zbsnetwork.lang.ContentType
+import com.zbsnetwork.lang.StdLibVersion.StdLibVersion
import com.zbsnetwork.lang.contract.{Contract, ContractSerDe}
import com.zbsnetwork.lang.v1.compiler.Terms._
-import com.zbsnetwork.lang.v1.evaluator.FunctionIds.SIGVERIFY
import com.zbsnetwork.lang.v1.{FunctionHeader, ScriptEstimator}
+import com.zbsnetwork.lang.v1.ContractLimits._
import com.zbsnetwork.transaction.smart.script.v1.ExprScript.checksumLength
import com.zbsnetwork.utils.{functionCosts, varNames}
import monix.eval.Coeval
object ContractScript {
- private val maxComplexity = 20 * functionCosts(V1)(FunctionHeader.Native(SIGVERIFY))()
+ def validateBytes(bs: Array[Byte]): Either[String, Unit] =
+ Either.cond(bs.length <= MaxContractSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $MaxContractSizeInBytes bytes")
def apply(version: StdLibVersion, contract: Contract): Either[String, Script] = {
for {
funcMaxComplexity <- estimateComplexity(version, contract)
_ <- Either.cond(
- funcMaxComplexity._2 <= maxComplexity,
+ funcMaxComplexity._2 <= MaxContractComplexity,
(),
- s"Contract function (${funcMaxComplexity._1}) is too complex: ${funcMaxComplexity._2} > $maxComplexity"
+ s"Contract function (${funcMaxComplexity._1}) is too complex: ${funcMaxComplexity._2} > $MaxContractComplexity"
)
- s = new ContractScriptImpl(version, contract, funcMaxComplexity._2)
+ s = ContractScriptImpl(version, contract, funcMaxComplexity._2)
+ _ <- validateBytes(s.bytes().arr)
+
} yield s
}
@@ -32,7 +35,7 @@ object ContractScript {
override type Expr = Contract
override val bytes: Coeval[ByteStr] =
Coeval.evalOnce {
- val s = Array(0: Byte, ScriptType.Contract.toByte, stdLibVersion.toByte) ++ ContractSerDe.serialize(expr)
+ val s = Array(0: Byte, ContentType.Contract.toByte, stdLibVersion.toByte) ++ ContractSerDe.serialize(expr)
ByteStr(s ++ crypto.secureHash(s).take(checksumLength))
}
override val containsBlockV2: Coeval[Boolean] = Coeval.evalOnce(true)
@@ -45,15 +48,15 @@ object ContractScript {
(contract.cfs.map(func => (func.annotation.invocationArgName, func.u)) ++ contract.vf.map(func => (func.annotation.invocationArgName, func.u)))
.map {
case (annotationArgName, funcExpr) =>
- ScriptEstimator(varNames(version), functionCosts(version), constructExprFromFuncAndContex(contract.dec, annotationArgName, funcExpr))
+ ScriptEstimator(varNames(version), functionCosts(version), constructExprFromFuncAndContext(contract.dec, annotationArgName, funcExpr))
.map(complexity => (funcExpr.name, complexity))
}
val funcsWithComplexityEi: E[Vector[(String, Long)]] = funcsWithComplexity.toVector.sequence
- funcsWithComplexityEi.map(namesAndComp => namesAndComp.maxBy(_._2))
+ funcsWithComplexityEi.map(namesAndComp => (("", 0L) +: namesAndComp).maxBy(_._2))
}
- private def constructExprFromFuncAndContex(dec: List[DECLARATION], annotationArgName: String, funcExpr: FUNC): EXPR = {
+ private def constructExprFromFuncAndContext(dec: List[DECLARATION], annotationArgName: String, funcExpr: FUNC): EXPR = {
val funcWithAnnotationContext =
BLOCK(
LET(annotationArgName, TRUE),
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala
index 996b04f..0cb7891 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala
@@ -5,8 +5,8 @@ import com.zbsnetwork.common.utils.Base64
import com.zbsnetwork.lang.StdLibVersion._
import com.zbsnetwork.lang.v1.compiler.Decompiler
import com.zbsnetwork.transaction.ValidationError.ScriptParseError
-import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl
import monix.eval.Coeval
+import com.zbsnetwork.transaction.smart.script.v1.ExprScript
trait Script {
type Expr
@@ -39,7 +39,7 @@ object Script {
} yield script
def decompile(s: Script): String = s match {
- case ExprScriprImpl(_, expr, _) => Decompiler(expr, com.zbsnetwork.utils.defaultDecompilerContext)
+ case e: ExprScript => Decompiler(e.expr, com.zbsnetwork.utils.defaultDecompilerContext)
case com.zbsnetwork.transaction.smart.script.ContractScript.ContractScriptImpl(_, contract, _) =>
Decompiler(contract, com.zbsnetwork.utils.defaultDecompilerContext)
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala
index 2bcab3f..80f31c5 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala
@@ -1,47 +1,41 @@
package com.zbsnetwork.transaction.smart.script
-import com.zbsnetwork.lang.ScriptType.ScriptType
+import com.zbsnetwork.lang.ContentType.ContentType
import com.zbsnetwork.lang.StdLibVersion.StdLibVersion
import com.zbsnetwork.lang.directives.DirectiveParser
import com.zbsnetwork.lang.utils._
import com.zbsnetwork.lang.v1.ScriptEstimator
import com.zbsnetwork.lang.v1.compiler.{ContractCompiler, ExpressionCompiler}
-import com.zbsnetwork.lang.v1.parser.Parser
-import com.zbsnetwork.lang.{ScriptType, StdLibVersion}
+import com.zbsnetwork.lang.{ContentType, ScriptType}
import com.zbsnetwork.transaction.smart.script.ContractScript._
import com.zbsnetwork.transaction.smart.script.v1.ExprScript
-import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl
import com.zbsnetwork.utils._
object ScriptCompiler extends ScorexLogging {
- def contract(scriptText: String): Either[String, Script] = {
- val ctx = compilerContext(StdLibVersion.V3, isAssetScript = false)
- ContractCompiler(ctx, Parser.parseContract(scriptText).get.value)
- .flatMap(s => ContractScript(StdLibVersion.V3, s))
- }
-
+ @Deprecated
def apply(scriptText: String, isAssetScript: Boolean): Either[String, (Script, Long)] = {
val directives = DirectiveParser(scriptText)
-
- val scriptWithoutDirectives =
- scriptText.linesIterator
- .filter(str => !str.contains("{-#"))
- .mkString("\n")
-
for {
ver <- extractStdLibVersion(directives)
- tpe <- extractScriptType(directives)
- script <- tryCompile(scriptWithoutDirectives, tpe, ver, isAssetScript)
+ tpe <- extractContentType(directives)
+ script <- tryCompile(scriptText, tpe, ver, isAssetScript)
} yield (script, script.complexity)
}
- def tryCompile(src: String, tpe: ScriptType, version: StdLibVersion, isAssetScript: Boolean): Either[String, Script] = {
+ def compile(scriptText: String): Either[String, (Script, Long)] = {
+ for {
+ scriptType <- extractScriptType(DirectiveParser(scriptText))
+ result <- apply(scriptText, scriptType == ScriptType.Asset)
+ } yield result
+ }
+
+ private def tryCompile(src: String, tpe: ContentType, version: StdLibVersion, isAssetScript: Boolean): Either[String, Script] = {
val ctx = compilerContext(version, isAssetScript)
try {
tpe match {
- case ScriptType.Expression => ExpressionCompiler.compile(src, ctx).flatMap(expr => ExprScript.apply(version, expr))
- case ScriptType.Contract => ContractCompiler.compile(src, ctx).flatMap(expr => ContractScript.apply(version, expr))
+ case ContentType.Expression => ExpressionCompiler.compile(src, ctx).flatMap(expr => ExprScript.apply(version, expr))
+ case ContentType.Contract => ContractCompiler.compile(src, ctx).flatMap(expr => ContractScript.apply(version, expr))
}
} catch {
case ex: Throwable =>
@@ -53,7 +47,7 @@ object ScriptCompiler extends ScorexLogging {
}
def estimate(script: Script, version: StdLibVersion): Either[String, Long] = script match {
- case s: ExprScriprImpl => ScriptEstimator(varNames(version), functionCosts(version), s.expr)
+ case s: ExprScript => ScriptEstimator(varNames(version), functionCosts(version), s.expr)
case s: ContractScriptImpl => ContractScript.estimateComplexity(version, s.expr).map(_._2)
case _ => ???
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala
index 0744521..9378293 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala
@@ -3,7 +3,7 @@ package com.zbsnetwork.transaction.smart.script
import com.zbsnetwork.crypto
import com.zbsnetwork.lang.contract.ContractSerDe
import com.zbsnetwork.lang.v1.Serde
-import com.zbsnetwork.lang.{ScriptType, StdLibVersion}
+import com.zbsnetwork.lang.{ContentType, StdLibVersion}
import com.zbsnetwork.transaction.ValidationError.ScriptParseError
import com.zbsnetwork.transaction.smart.script.v1._
@@ -15,24 +15,26 @@ object ScriptReader {
val checkSum = bytes.takeRight(checksumLength)
val computedCheckSum = crypto.secureHash(bytes.dropRight(checksumLength)).take(checksumLength)
val versionByte: Byte = bytes.head
- val (scriptType, stdLibVersion, offset) =
- if (versionByte == 0)
- (ScriptType.parseVersion(bytes(1)), StdLibVersion.parseVersion(bytes(2)), 3)
- else if (versionByte == StdLibVersion.V1.toByte || versionByte == StdLibVersion.V2.toByte)
- (ScriptType.Expression, StdLibVersion(versionByte.toInt), 1)
- else ???
- val scriptBytes = bytes.drop(offset).dropRight(checksumLength)
-
(for {
+ a <- {
+ if (versionByte == 0)
+ Right((ContentType.parseId(bytes(1)), StdLibVersion.parseVersion(bytes(2)), 3))
+ else if (versionByte == StdLibVersion.V1.toByte || versionByte == StdLibVersion.V2.toByte)
+ Right((ContentType.Expression, StdLibVersion(versionByte.toInt), 1))
+ else Left(ScriptParseError(s"Can't parse script bytes starting with [${bytes(0).toInt},${bytes(1).toInt},${bytes(2).toInt}]"))
+ }
+ (scriptType, stdLibVersion, offset) = a
+ scriptBytes = bytes.drop(offset).dropRight(checksumLength)
+
_ <- Either.cond(checkSum.sameElements(computedCheckSum), (), ScriptParseError("Invalid checksum"))
s <- scriptType match {
- case ScriptType.Expression =>
+ case ContentType.Expression =>
for {
_ <- ExprScript.validateBytes(scriptBytes)
bytes <- Serde.deserialize(scriptBytes).map(_._1)
s <- ExprScript(stdLibVersion, bytes, checkSize = false)
} yield s
- case ScriptType.Contract =>
+ case ContentType.Contract =>
for {
bytes <- ContractSerDe.deserialize(scriptBytes)
s <- ContractScript(stdLibVersion, bytes)
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala
index 4e7353e..cb0e9fe 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala
@@ -2,16 +2,14 @@ package com.zbsnetwork.transaction.smart.script
import cats.implicits._
import com.zbsnetwork.account.AddressScheme
-import com.zbsnetwork.lang.v1.compiler.Terms.EVALUATED
-import com.zbsnetwork.lang.v1.evaluator.EvaluatorV1
import com.zbsnetwork.lang._
import com.zbsnetwork.lang.contract.Contract
-import com.zbsnetwork.lang.v1.evaluator._
-import com.zbsnetwork.lang.v1.compiler.Terms.{FALSE, TRUE}
+import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FALSE, TRUE}
+import com.zbsnetwork.lang.v1.evaluator.{EvaluatorV1, _}
import com.zbsnetwork.state._
-import com.zbsnetwork.transaction.{Authorized, Proven}
+import com.zbsnetwork.transaction.smart.script.v1.ExprScript
import com.zbsnetwork.transaction.smart.{BlockchainContext, RealTransactionWrapper, Verifier}
-import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl
+import com.zbsnetwork.transaction.{Authorized, Proven}
import monix.eval.Coeval
object ScriptRunner {
@@ -19,7 +17,7 @@ object ScriptRunner {
def apply(height: Int, in: TxOrd, blockchain: Blockchain, script: Script, isTokenScript: Boolean): (Log, Either[ExecutionError, EVALUATED]) = {
script match {
- case s: ExprScriprImpl =>
+ case s: ExprScript =>
val ctx = BlockchainContext.build(
script.stdLibVersion,
AddressScheme.current.chainId,
diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala
index fa9e911..992e4be 100644
--- a/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala
@@ -3,9 +3,9 @@ package com.zbsnetwork.transaction.smart.script.v1
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.crypto
import com.zbsnetwork.lang.StdLibVersion._
+import com.zbsnetwork.lang.v1.ContractLimits._
import com.zbsnetwork.lang.v1.compiler.Terms._
-import com.zbsnetwork.lang.v1.evaluator.FunctionIds._
-import com.zbsnetwork.lang.v1.{FunctionHeader, ScriptEstimator, Serde}
+import com.zbsnetwork.lang.v1.{ScriptEstimator, Serde}
import com.zbsnetwork.transaction.smart.script.Script
import com.zbsnetwork.utils.{functionCosts, varNames}
import monix.eval.Coeval
@@ -14,24 +14,22 @@ import scala.annotation.tailrec
import scala.collection.mutable._
object ExprScript {
- val checksumLength = 4
- private val maxComplexity = 20 * functionCosts(V1)(FunctionHeader.Native(SIGVERIFY))()
- private val maxSizeInBytes = 8 * 1024
+ val checksumLength = 4
def validateBytes(bs: Array[Byte]): Either[String, Unit] =
- Either.cond(bs.length <= maxSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $maxSizeInBytes bytes")
+ Either.cond(bs.length <= MaxExprSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $MaxExprSizeInBytes bytes")
def apply(x: EXPR): Either[String, Script] = apply(V1, x)
def apply(version: StdLibVersion, x: EXPR, checkSize: Boolean = true): Either[String, Script] =
for {
scriptComplexity <- ScriptEstimator(varNames(version), functionCosts(version), x)
- _ <- Either.cond(scriptComplexity <= maxComplexity, (), s"Script is too complex: $scriptComplexity > $maxComplexity")
- s = new ExprScriprImpl(version, x, scriptComplexity)
+ _ <- Either.cond(scriptComplexity <= MaxExprComplexity, (), s"Script is too complex: $scriptComplexity > $MaxExprComplexity")
+ s = new ExprScriptImpl(version, x, scriptComplexity)
_ <- if (checkSize) validateBytes(s.bytes().arr) else Right(())
} yield s
- case class ExprScriprImpl(stdLibVersion: StdLibVersion, expr: EXPR, complexity: Long) extends Script {
+ private case class ExprScriptImpl(stdLibVersion: StdLibVersion, expr: EXPR, complexity: Long) extends ExprScript {
override type Expr = EXPR
override val bytes: Coeval[ByteStr] =
@@ -61,3 +59,10 @@ object ExprScript {
horTraversal(Queue(e))
}
}
+
+trait ExprScript extends Script {
+ override type Expr = EXPR
+ val stdLibVersion: StdLibVersion
+ val expr: EXPR
+ val complexity: Long
+}
diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala
index dd852ca..1e131f7 100644
--- a/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala
@@ -6,17 +6,17 @@ import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccou
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.{Base58, EitherExt2}
import com.zbsnetwork.crypto
-import com.zbsnetwork.crypto._
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction.ValidationError.Validation
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import com.zbsnetwork.transaction.transfer.MassTransferTransaction.{ParsedTransfer, toJson}
import io.swagger.annotations.{ApiModel, ApiModelProperty}
import monix.eval.Coeval
import play.api.libs.json.{Format, JsObject, JsValue, Json}
import scala.annotation.meta.field
-import scala.util.{Either, Failure, Success, Try}
+import scala.util.{Either, Try}
case class MassTransferTransaction private (assetId: Option[AssetId],
sender: PublicKeyAccount,
@@ -49,6 +49,7 @@ case class MassTransferTransaction private (assetId: Option[AssetId],
Deser.serializeArray(attachment)
)
}
+
override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(bodyBytes(), proofs.bytes()))
override val assetFee: (Option[AssetId], Long) = (None, fee)
@@ -90,34 +91,25 @@ object MassTransferTransaction extends TransactionParserFor[MassTransferTransact
implicit val transferFormat: Format[Transfer] = Json.format
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val sender = PublicKeyAccount(bytes.slice(0, KeyLength))
- val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, KeyLength, AssetIdLength)
- val transferCount = Shorts.fromByteArray(bytes.slice(s0, s0 + 2))
-
- def readTransfer(offset: Int): (Validation[ParsedTransfer], Int) = {
- AddressOrAlias.fromBytes(bytes, offset) match {
- case Right((addr, ofs)) =>
- val amount = Longs.fromByteArray(bytes.slice(ofs, ofs + 8))
- (Right[ValidationError, ParsedTransfer](ParsedTransfer(addr, amount)), ofs + 8)
- case Left(e) => (Left(e), offset)
- }
- }
-
- val transfersList: List[(Validation[ParsedTransfer], Int)] =
- List.iterate(readTransfer(s0 + 2), transferCount) { case (_, offset) => readTransfer(offset) }
-
- val s1 = transfersList.lastOption.map(_._2).getOrElse(s0 + 2)
- val tx: Validation[MassTransferTransaction] = for {
- transfers <- transfersList.map { case (ei, _) => ei }.sequence
- timestamp = Longs.fromByteArray(bytes.slice(s1, s1 + 8))
- feeAmount = Longs.fromByteArray(bytes.slice(s1 + 8, s1 + 16))
- (attachment, attachEnd) = Deser.parseArraySize(bytes, s1 + 16)
- proofs <- Proofs.fromBytes(bytes.drop(attachEnd))
- mtt <- MassTransferTransaction.create(assetIdOpt.map(ByteStr(_)), sender, transfers, timestamp, feeAmount, attachment, proofs)
- } yield mtt
- tx.fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ Try { tx.transfers.map(_.amount).fold(tx.fee)(Math.addExact) }
+ .fold(
+ ex => Left(ValidationError.OverflowError),
+ totalAmount =>
+ if (tx.transfers.lengthCompare(MaxTransferCount) > 0) {
+ Left(ValidationError.GenericError(s"Number of transfers ${tx.transfers.length} is greater than $MaxTransferCount"))
+ } else if (tx.transfers.exists(_.amount < 0)) {
+ Left(ValidationError.GenericError("One of the transfers has negative amount"))
+ } else if (tx.attachment.length > TransferTransaction.MaxAttachmentSize) {
+ Left(ValidationError.TooBigArray)
+ } else if (tx.fee <= 0) {
+ Left(ValidationError.InsufficientFee())
+ } else {
+ Right(tx)
+ }
+ )
+ .foldToTry
+ }
}
def create(assetId: Option[AssetId],
@@ -177,4 +169,27 @@ object MassTransferTransaction extends TransactionParserFor[MassTransferTransact
private def toJson(transfers: List[ParsedTransfer]): JsValue = {
Json.toJson(transfers.map { case ParsedTransfer(address, amount) => Transfer(address.stringRepr, amount) })
}
+
+ val byteTailDescription: ByteEntity[MassTransferTransaction] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ OptionBytes(index = tailIndex(2), name = "Asset ID", nestedByteEntity = AssetIdBytes(tailIndex(2), "Asset ID")),
+ TransfersBytes(tailIndex(3)),
+ LongBytes(tailIndex(4), "Timestamp"),
+ LongBytes(tailIndex(5), "Fee"),
+ BytesArrayUndefinedLength(tailIndex(6), "Attachments"),
+ ProofsBytes(tailIndex(7))
+ ) mapN {
+ case (sender, assetId, transfer, timestamp, fee, attachment, proofs) =>
+ MassTransferTransaction(
+ assetId = assetId,
+ sender = sender,
+ transfers = transfer,
+ timestamp = timestamp,
+ fee = fee,
+ attachment = attachment,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala
index bd5b11a..bbb0032 100644
--- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala
@@ -2,7 +2,7 @@ package com.zbsnetwork.transaction.transfer
import cats.implicits._
import com.google.common.primitives.{Bytes, Longs}
-import com.zbsnetwork.account.{AddressOrAlias, PublicKeyAccount}
+import com.zbsnetwork.account.AddressOrAlias
import com.zbsnetwork.common.utils.Base58
import com.zbsnetwork.serialization.Deser
import com.zbsnetwork.transaction._
@@ -10,7 +10,6 @@ import com.zbsnetwork.transaction.validation._
import com.zbsnetwork.utils.base58Length
import monix.eval.Coeval
import play.api.libs.json.{JsObject, Json}
-import com.zbsnetwork.crypto._
trait TransferTransaction extends ProvenTransaction with VersionedTransaction {
def assetId: Option[AssetId]
@@ -62,30 +61,21 @@ object TransferTransaction {
val MaxAttachmentSize = 140
val MaxAttachmentStringSize: Int = base58Length(MaxAttachmentSize)
- def validate(amount: Long, feeAmount: Long, attachment: Array[Byte]): Either[ValidationError, Unit] = {
+ def validate(tx: TransferTransaction): Either[ValidationError, Unit] = {
+ validate(tx.amount, tx.assetId, tx.fee, tx.feeAssetId, tx.attachment)
+ }
+
+ def validate(amt: Long,
+ maybeAmtAsset: Option[AssetId],
+ feeAmt: Long,
+ maybeFeeAsset: Option[AssetId],
+ attachment: Array[Byte]): Either[ValidationError, Unit] = {
(
- validateAmount(amount, "zbs"),
- validateFee(feeAmount),
- validateAttachment(attachment),
- validateSum(Seq(amount, feeAmount))
+ validateAmount(amt, maybeAmtAsset.map(_.base58).getOrElse("zbs")),
+ validateFee(feeAmt),
+ validateAttachment(attachment)
).mapN { case _ => () }
.toEither
.leftMap(_.head)
}
-
- def parseBase(bytes: Array[Byte], start: Int) = {
- val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength))
- val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, start + KeyLength, AssetIdLength)
- val (feeAssetIdOpt, s1) = Deser.parseByteArrayOption(bytes, s0, AssetIdLength)
- val timestamp = Longs.fromByteArray(bytes.slice(s1, s1 + 8))
- val amount = Longs.fromByteArray(bytes.slice(s1 + 8, s1 + 16))
- val feeAmount = Longs.fromByteArray(bytes.slice(s1 + 16, s1 + 24))
- for {
- recRes <- AddressOrAlias.fromBytes(bytes, s1 + 24)
- (recipient, recipientEnd) = recRes
- (attachment, end) = Deser.parseArraySize(bytes, recipientEnd)
- } yield (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, end)
-
- }
-
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala
index 0dfe547..a64957d 100644
--- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.transfer
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
-import com.zbsnetwork.crypto._
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class TransferTransactionV1 private (assetId: Option[AssetId],
sender: PublicKeyAccount,
@@ -34,25 +36,12 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1]
override val typeId: Byte = TransferTransaction.typeId
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- val signature = ByteStr(bytes.slice(0, SignatureLength))
- val txId = bytes(SignatureLength)
- require(txId == typeId, s"Signed tx id is not match")
-
- (for {
- parsed <- TransferTransaction.parseBase(bytes, SignatureLength + 1)
- (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, _) = parsed
- tt <- TransferTransactionV1.create(assetIdOpt.map(ByteStr(_)),
- sender,
- recipient,
- amount,
- timestamp,
- feeAssetIdOpt.map(ByteStr(_)),
- feeAmount,
- attachment,
- signature)
- } yield tt).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ TransferTransaction
+ .validate(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(assetId: Option[AssetId],
@@ -65,7 +54,7 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1]
attachment: Array[Byte],
signature: ByteStr): Either[ValidationError, TransactionT] = {
TransferTransaction
- .validate(amount, feeAmount, attachment)
+ .validate(amount, assetId, feeAmount, feeAssetId, attachment)
.map(_ => TransferTransactionV1(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, signature))
}
@@ -93,4 +82,33 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1]
attachment: Array[Byte]): Either[ValidationError, TransactionT] = {
signed(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, sender)
}
+
+ val byteTailDescription: ByteEntity[TransferTransactionV1] = {
+ (
+ SignatureBytes(tailIndex(1), "Signature"),
+ ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"),
+ PublicKeyAccountBytes(tailIndex(3), "Sender's public key"),
+ OptionBytes[AssetId](tailIndex(4), "Asset ID", AssetIdBytes(tailIndex(4), "Asset ID"), "flag (1 - asset, 0 - Zbs)"),
+ OptionBytes[AssetId](tailIndex(5), "Fee's asset ID", AssetIdBytes(tailIndex(5), "Fee's asset ID"), "flag (1 - asset, 0 - Zbs)"),
+ LongBytes(tailIndex(6), "Timestamp"),
+ LongBytes(tailIndex(7), "Amount"),
+ LongBytes(tailIndex(8), "Fee"),
+ AddressOrAliasBytes(tailIndex(9), "Recipient"),
+ BytesArrayUndefinedLength(tailIndex(10), "Attachment")
+ ) mapN {
+ case (signature, txId, senderPublicKey, assetId, feeAssetId, timestamp, amount, fee, recipient, attachments) =>
+ require(txId == typeId, s"Signed tx id is not match")
+ TransferTransactionV1(
+ assetId = assetId,
+ sender = senderPublicKey,
+ recipient = recipient,
+ amount = amount,
+ timestamp = timestamp,
+ feeAssetId = feeAssetId,
+ fee = fee,
+ attachment = attachments,
+ signature = signature
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala
index fde0103..558a3ae 100644
--- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala
@@ -1,14 +1,16 @@
package com.zbsnetwork.transaction.transfer
+import cats.implicits._
import com.google.common.primitives.Bytes
import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.crypto
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.description._
import monix.eval.Coeval
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
case class TransferTransactionV2 private (sender: PublicKeyAccount,
recipient: AddressOrAlias,
@@ -26,7 +28,8 @@ case class TransferTransactionV2 private (sender: PublicKeyAccount,
override val builder: TransactionParser = TransferTransactionV2
override val bodyBytes: Coeval[Array[Byte]] = Coeval.evalOnce(Array(builder.typeId, version) ++ bytesBase())
override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(Array(0: Byte), bodyBytes(), proofs.bytes()))
- override def version: Byte = 2
+
+ override def version: Byte = 2
}
object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2] with TransactionParser.MultipleVersions {
@@ -35,22 +38,12 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2]
override val supportedVersions: Set[Byte] = Set(2)
override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = {
- Try {
- (for {
- parsed <- TransferTransaction.parseBase(bytes, 0)
- (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, end) = parsed
- proofs <- Proofs.fromBytes(bytes.drop(end))
- tt <- TransferTransactionV2.create(assetIdOpt.map(ByteStr(_)),
- sender,
- recipient,
- amount,
- timestamp,
- feeAssetIdOpt.map(ByteStr(_)),
- feeAmount,
- attachment,
- proofs)
- } yield tt).fold(left => Failure(new Exception(left.toString)), right => Success(right))
- }.flatten
+ byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx =>
+ TransferTransaction
+ .validate(tx)
+ .map(_ => tx)
+ .foldToTry
+ }
}
def create(assetId: Option[AssetId],
@@ -63,7 +56,7 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2]
attachment: Array[Byte],
proofs: Proofs): Either[ValidationError, TransactionT] = {
for {
- _ <- TransferTransaction.validate(amount, feeAmount, attachment)
+ _ <- TransferTransaction.validate(amount, assetId, feeAmount, feeAssetId, attachment)
} yield TransferTransactionV2(sender, recipient, assetId, amount, timestamp, feeAssetId, feeAmount, attachment, proofs)
}
@@ -91,4 +84,31 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2]
attachment: Array[Byte]): Either[ValidationError, TransactionT] = {
signed(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, sender)
}
+
+ val byteTailDescription: ByteEntity[TransferTransactionV2] = {
+ (
+ PublicKeyAccountBytes(tailIndex(1), "Sender's public key"),
+ OptionBytes(tailIndex(2), "Asset ID", AssetIdBytes(tailIndex(2), "Asset ID"), "flag (1 - asset, 0 - Zbs)"),
+ OptionBytes(tailIndex(3), "Fee's asset ID", AssetIdBytes(tailIndex(3), "Fee's asset ID"), "flag (1 - asset, 0 - Zbs)"),
+ LongBytes(tailIndex(4), "Timestamp"),
+ LongBytes(tailIndex(5), "Amount"),
+ LongBytes(tailIndex(6), "Fee"),
+ AddressOrAliasBytes(tailIndex(7), "Recipient"),
+ BytesArrayUndefinedLength(tailIndex(8), "Attachment"),
+ ProofsBytes(tailIndex(9))
+ ) mapN {
+ case (senderPublicKey, assetId, feeAssetId, timestamp, amount, fee, recipient, attachments, proofs) =>
+ TransferTransactionV2(
+ sender = senderPublicKey,
+ recipient = recipient,
+ assetId = assetId,
+ amount = amount,
+ timestamp = timestamp,
+ feeAssetId = feeAssetId,
+ fee = fee,
+ attachment = attachments,
+ proofs = proofs
+ )
+ }
+ }
}
diff --git a/src/main/scala/com/zbsnetwork/transaction/validation/package.scala b/src/main/scala/com/zbsnetwork/transaction/validation/package.scala
index 0c6e516..0869a8a 100644
--- a/src/main/scala/com/zbsnetwork/transaction/validation/package.scala
+++ b/src/main/scala/com/zbsnetwork/transaction/validation/package.scala
@@ -26,7 +26,7 @@ package object validation {
)
}
- def validateAmount(amount: Long, of: String): Validated[Long] = {
+ def validateAmount(amount: Long, of: => String): Validated[Long] = {
Validated
.condNel(
amount > 0,
diff --git a/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala b/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala
index fe43888..41aba63 100644
--- a/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala
+++ b/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala
@@ -88,7 +88,7 @@ object EmptyBlockchain extends Blockchain {
override def assetDistribution(assetId: ByteStr): AssetDistribution = Monoid.empty[AssetDistribution]
- override def zbsDistribution(height: Int): Map[Address, Long] = Map.empty
+ override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = Right(Map.empty)
override def allActiveLeases: Set[LeaseTransaction] = Set.empty
diff --git a/src/main/scala/com/zbsnetwork/utils/package.scala b/src/main/scala/com/zbsnetwork/utils/package.scala
index c0c808e..53c9874 100644
--- a/src/main/scala/com/zbsnetwork/utils/package.scala
+++ b/src/main/scala/com/zbsnetwork/utils/package.scala
@@ -7,7 +7,6 @@ import com.google.common.base.Throwables
import com.zbsnetwork.account.AddressScheme
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.state.ByteStr._
-import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.db.{Storage, VersionedStorage}
import com.zbsnetwork.lang.Global
import com.zbsnetwork.lang.StdLibVersion._
@@ -15,7 +14,7 @@ import com.zbsnetwork.lang.v1.compiler.{CompilerContext, DecompilerContext}
import com.zbsnetwork.lang.v1.evaluator.ctx._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
-import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, ScriptEstimator}
+import com.zbsnetwork.lang.v1.{CTX, FunctionHeader}
import com.zbsnetwork.transaction.smart.ZbsEnvironment
import monix.eval.Coeval
import monix.execution.UncaughtExceptionReporter
@@ -123,11 +122,11 @@ package object utils extends ScorexLogging {
def dummyEvalContext(version: StdLibVersion): EvaluationContext = lazyContexts(version)().evaluationContext
private val lazyFunctionCosts: Map[StdLibVersion, Coeval[Map[FunctionHeader, Coeval[Long]]]] =
- lazyContexts.mapValues(_.map(ctx => estimate(ctx.evaluationContext)))
+ lazyContexts.map(el => (el._1, el._2.map(ctx => estimate(el._1, ctx.evaluationContext))))
def functionCosts(version: StdLibVersion): Map[FunctionHeader, Coeval[Long]] = lazyFunctionCosts(version)()
- def estimate(ctx: EvaluationContext): Map[FunctionHeader, Coeval[Long]] = {
+ def estimate(version: StdLibVersion, ctx: EvaluationContext): Map[FunctionHeader, Coeval[Long]] = {
val costs: mutable.Map[FunctionHeader, Coeval[Long]] = ctx.typeDefs.collect {
case (typeName, CaseType(_, fields)) => FunctionHeader.User(typeName) -> Coeval.now(fields.size.toLong)
}(collection.breakOut)
@@ -135,11 +134,10 @@ package object utils extends ScorexLogging {
ctx.functions.values.foreach { func =>
val cost = func match {
case f: UserFunction =>
- import f.signature.args
- Coeval.evalOnce(ScriptEstimator(ctx.letDefs.keySet ++ args.map(_._1), costs, f.ev).explicitGet() + args.size * 5)
- case f: NativeFunction => Coeval.now(f.cost)
+ f.costByLibVersion(version)
+ case f: NativeFunction => f.cost
}
- costs += func.header -> cost
+ costs += func.header -> Coeval.now(cost)
}
costs.toMap
diff --git a/src/main/scala/com/zbsnetwork/utx/UtxPool.scala b/src/main/scala/com/zbsnetwork/utx/UtxPool.scala
index 1f14338..2d1fe76 100644
--- a/src/main/scala/com/zbsnetwork/utx/UtxPool.scala
+++ b/src/main/scala/com/zbsnetwork/utx/UtxPool.scala
@@ -13,9 +13,9 @@ trait UtxPool extends AutoCloseable {
def removeAll(txs: Traversable[Transaction]): Unit
- def accountPortfolio(addr: Address): Portfolio
+ def spendableBalance(addr: Address, assetId: Option[AssetId]): Long
- def portfolio(addr: Address): Portfolio
+ def pessimisticPortfolio(addr: Address): Portfolio
def all: Seq[Transaction]
diff --git a/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala b/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala
index 2f9aba6..25fab04 100644
--- a/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala
+++ b/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala
@@ -5,7 +5,6 @@ import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentHashMap
import cats._
-import cats.implicits._
import com.zbsnetwork.account.Address
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.consensus.TransactionsOrdering
@@ -24,14 +23,17 @@ import kamon.Kamon
import kamon.metric.MeasurementUnit
import monix.eval.Task
import monix.execution.schedulers.SchedulerService
-import monix.execution.{CancelableFuture, Scheduler}
-import monix.reactive.Observer
+import monix.execution.{Cancelable, Scheduler}
+import monix.reactive.{Observable, Observer}
import scala.collection.JavaConverters._
-import scala.concurrent.duration.DurationLong
import scala.util.{Left, Right}
-class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer[Address], fs: FunctionalitySettings, utxSettings: UtxSettings)
+class UtxPoolImpl(time: Time,
+ blockchain: Blockchain,
+ spendableBalanceChanged: Observer[(Address, Option[AssetId])],
+ fs: FunctionalitySettings,
+ utxSettings: UtxSettings)
extends ScorexLogging
with Instrumented
with AutoCloseable
@@ -40,18 +42,11 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer
import com.zbsnetwork.utx.UtxPoolImpl._
- private implicit val scheduler: SchedulerService = Scheduler.singleThread("utx-pool-cleanup")
-
- private val transactions = new ConcurrentHashMap[ByteStr, Transaction]()
- private val pessimisticPortfolios = new PessimisticPortfolios(portfolioChanges)
-
- private val removeInvalidTask: Task[Unit] =
- Task.eval(removeInvalid()) >>
- Task.sleep(utxSettings.cleanupInterval) >>
- removeInvalidTask
-
- private val cleanup: CancelableFuture[Unit] = removeInvalidTask.runAsyncLogErr
+ // State
+ private[this] val transactions = new ConcurrentHashMap[ByteStr, Transaction]()
+ private[this] val pessimisticPortfolios = new PessimisticPortfolios(spendableBalanceChanged)
+ // Metrics
private[this] object PoolMetrics {
private[this] val sizeStats = Kamon.rangeSampler("utx-pool-size", MeasurementUnit.none, Duration.of(500, ChronoUnit.MILLIS))
private[this] val bytesStats = Kamon.rangeSampler("utx-pool-bytes", MeasurementUnit.information.bytes, Duration.of(500, ChronoUnit.MILLIS))
@@ -60,79 +55,115 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer
def addTransaction(tx: Transaction): Unit = {
sizeStats.increment()
- bytesStats.increment(tx.bytes().size)
+ bytesStats.increment(tx.bytes().length)
}
def removeTransaction(tx: Transaction): Unit = {
sizeStats.decrement()
- bytesStats.decrement(tx.bytes().size)
+ bytesStats.decrement(tx.bytes().length)
}
}
- override def close(): Unit = {
- cleanup.cancel()
- scheduler.shutdown()
- }
+ override def putIfNew(tx: Transaction): Either[ValidationError, (Boolean, Diff)] = {
+ def canReissue(blockchain: Blockchain, tx: Transaction) = tx match {
+ case r: ReissueTransaction if blockchain.assetDescription(r.assetId).exists(!_.reissuable) => Left(GenericError(s"Asset is not reissuable"))
+ case _ => Right(())
+ }
- private def removeExpired(currentTs: Long): Unit = {
- def isExpired(tx: Transaction) = (currentTs - tx.timestamp).millis > fs.maxTransactionTimeBackOffset
+ def checkAlias(blockchain: Blockchain, tx: Transaction) = tx match {
+ case cat: CreateAliasTransaction if !blockchain.canCreateAlias(cat.alias) => Left(GenericError("Alias already claimed"))
+ case _ => Right(())
+ }
- transactions.values.asScala
- .collect {
- case tx if isExpired(tx) => tx.id()
- }
- .foreach(remove)
- }
+ def checkScripted(blockchain: Blockchain, tx: Transaction) = tx match {
+ case _ if utxSettings.allowTransactionsFromSmartAccounts => Right(())
+ case a: AuthorizedTransaction if blockchain.hasScript(a.sender.toAddress) =>
+ Left(GenericError("transactions from scripted accounts are denied from UTX pool"))
+ case _ => Right(())
+ }
- override def putIfNew(tx: Transaction): Either[ValidationError, (Boolean, Diff)] = putIfNew(blockchain, tx)
+ def checkNotBlacklisted(tx: Transaction) = {
+ if (utxSettings.blacklistSenderAddresses.isEmpty) {
+ Right(())
+ } else {
+ val sender: Option[String] = tx match {
+ case x: Authorized => Some(x.sender.address)
+ case _ => None
+ }
- private def checkNotBlacklisted(tx: Transaction): Either[ValidationError, Unit] = {
- if (utxSettings.blacklistSenderAddresses.isEmpty) {
- Right(())
- } else {
- val sender: Option[String] = tx match {
- case x: Authorized => Some(x.sender.address)
- case _ => None
+ sender match {
+ case Some(addr) if utxSettings.blacklistSenderAddresses.contains(addr) =>
+ val recipients = tx match {
+ case tt: TransferTransaction => Seq(tt.recipient)
+ case mtt: MassTransferTransaction => mtt.transfers.map(_.address)
+ case _ => Seq()
+ }
+ val allowed =
+ recipients.nonEmpty &&
+ recipients.forall(r => utxSettings.allowBlacklistedTransferTo.contains(r.stringRepr))
+ Either.cond(allowed, (), SenderIsBlacklisted(addr))
+ case _ => Right(())
+ }
}
+ }
- sender match {
- case Some(addr) if utxSettings.blacklistSenderAddresses.contains(addr) =>
- val recipients = tx match {
- case tt: TransferTransaction => Seq(tt.recipient)
- case mtt: MassTransferTransaction => mtt.transfers.map(_.address)
- case _ => Seq()
- }
- val allowed =
- recipients.nonEmpty &&
- recipients.forall(r => utxSettings.allowBlacklistedTransferTo.contains(r.stringRepr))
- Either.cond(allowed, (), SenderIsBlacklisted(addr))
- case _ => Right(())
+ PoolMetrics.putRequestStats.increment()
+ val result = measureSuccessful(
+ PoolMetrics.processingTimeStats, {
+ for {
+ _ <- Either.cond(transactions.size < utxSettings.maxSize, (), GenericError("Transaction pool size limit is reached"))
+
+ transactionsBytes = transactions.values.asScala // Bytes size of all transactions in pool
+ .map(_.bytes().length)
+ .sum
+ _ <- Either.cond((transactionsBytes + tx.bytes().length) <= utxSettings.maxBytesSize,
+ (),
+ GenericError("Transaction pool bytes size limit is reached"))
+
+ _ <- checkNotBlacklisted(tx)
+ _ <- checkScripted(blockchain, tx)
+ _ <- checkAlias(blockchain, tx)
+ _ <- canReissue(blockchain, tx)
+ diff <- TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height)(blockchain, tx)
+ } yield {
+ pessimisticPortfolios.add(tx.id(), diff)
+ val isNew = Option(transactions.put(tx.id(), tx)).isEmpty
+ if (isNew) PoolMetrics.addTransaction(tx)
+ (isNew, diff)
+ }
}
- }
+ )
+
+ result.fold(
+ err => log.trace(s"UTX putIfNew(${tx.id()}) failed with $err"),
+ r => log.trace(s"UTX putIfNew(${tx.id()}) succeeded, isNew = ${r._1}")
+ )
+
+ result
}
override def removeAll(txs: Traversable[Transaction]): Unit = {
txs.view.map(_.id()).foreach(remove)
- removeExpired(time.correctedTime())
+ cleanup.doExpiredCleanup()
}
- private def remove(txId: ByteStr): Unit = {
- Option(transactions.remove(txId)).foreach(PoolMetrics.removeTransaction)
- pessimisticPortfolios.remove(txId)
+ private[this] def afterRemove(tx: Transaction): Unit = {
+ PoolMetrics.removeTransaction(tx)
+ pessimisticPortfolios.remove(tx.id())
}
- private def removeInvalid(): Unit = {
- val b = blockchain
- val transactionsToRemove = transactions.values.asScala.filter { t =>
- TransactionDiffer(fs, b.lastBlockTimestamp, time.correctedTime(), b.height)(b, t).isLeft
- }
- removeAll(transactionsToRemove)
- }
+ private[this] def remove(txId: ByteStr): Unit =
+ Option(transactions.remove(txId))
+ .foreach(afterRemove)
- override def accountPortfolio(addr: Address): Portfolio = blockchain.portfolio(addr)
+ override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long =
+ blockchain.balance(addr, assetId) -
+ assetId.fold(blockchain.leaseBalance(addr).out)(_ => 0L) +
+ pessimisticPortfolios
+ .getAggregated(addr)
+ .spendableBalanceOf(assetId)
- override def portfolio(addr: Address): Portfolio =
- Monoid.combine(blockchain.portfolio(addr), pessimisticPortfolios.getAggregated(addr))
+ override def pessimisticPortfolio(addr: Address): Portfolio = pessimisticPortfolios.getAggregated(addr)
override def all: Seq[Transaction] = transactions.values.asScala.toSeq.sorted(TransactionsOrdering.InUTXPool)
@@ -141,16 +172,15 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer
override def transactionById(transactionId: ByteStr): Option[Transaction] = Option(transactions.get(transactionId))
override def packUnconfirmed(rest: MultiDimensionalMiningConstraint): (Seq[Transaction], MultiDimensionalMiningConstraint) = {
- val currentTs = time.correctedTime()
- removeExpired(currentTs)
- val b = blockchain
- val differ = TransactionDiffer(fs, blockchain.lastBlockTimestamp, currentTs, b.height) _
+ cleanup.doExpiredCleanup()
+
+ val differ = TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height) _
val (invalidTxs, reversedValidTxs, _, finalConstraint, _) = transactions.values.asScala.toSeq
.sorted(TransactionsOrdering.InUTXPool)
.iterator
.scanLeft((Seq.empty[ByteStr], Seq.empty[Transaction], Monoid[Diff].empty, rest, false)) {
case ((invalid, valid, diff, currRest, isEmpty), tx) =>
- val updatedBlockchain = composite(b, diff)
+ val updatedBlockchain = composite(blockchain, diff)
val updatedRest = currRest.put(updatedBlockchain, tx)
if (updatedRest.isOverfilled) {
(invalid, valid, diff, currRest, isEmpty)
@@ -171,61 +201,65 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer
(txs, finalConstraint)
}
- private def canReissue(b: Blockchain, tx: Transaction) = tx match {
- case r: ReissueTransaction if b.assetDescription(r.assetId).exists(!_.reissuable) => Left(GenericError(s"Asset is not reissuable"))
- case _ => Right(())
- }
+ //noinspection ScalaStyle
+ private[this] object TxCheck {
+ private[this] val ExpirationTime = fs.maxTransactionTimeBackOffset.toMillis
- private def checkAlias(b: Blockchain, tx: Transaction) = tx match {
- case cat: CreateAliasTransaction if !blockchain.canCreateAlias(cat.alias) => Left(GenericError("Alias already claimed"))
- case _ => Right(())
- }
+ def transactionIsExpired(transaction: Transaction, currentTime: Long = time.correctedTime()) = {
+ (currentTime - transaction.timestamp) > ExpirationTime
+ }
- private def checkScripted(b: Blockchain, tx: Transaction) =
- tx match {
- case a: AuthorizedTransaction if blockchain.hasScript(a.sender.toAddress) && (!utxSettings.allowTransactionsFromSmartAccounts) =>
- Left(GenericError("transactions from scripted accounts are denied from UTX pool"))
- case _ => Right(())
+ def transactionIsValid(transaction: Transaction,
+ lastBlockTimestamp: Option[Long] = blockchain.lastBlockTimestamp,
+ currentTime: Long = time.correctedTime(),
+ height: Int = blockchain.height) = {
+ !transactionIsExpired(transaction) && TransactionDiffer(fs, lastBlockTimestamp, currentTime, height)(blockchain, transaction).isRight
}
+ }
- private def putIfNew(b: Blockchain, tx: Transaction): Either[ValidationError, (Boolean, Diff)] = {
- PoolMetrics.putRequestStats.increment()
- val result = measureSuccessful(
- PoolMetrics.processingTimeStats, {
- for {
- _ <- Either.cond(transactions.size < utxSettings.maxSize, (), GenericError("Transaction pool size limit is reached"))
+ //noinspection ScalaStyle
+ object cleanup {
+ private[UtxPoolImpl] implicit val scheduler: SchedulerService = Scheduler.singleThread("utx-pool-cleanup")
+
+ val runCleanupTask: Task[Unit] = Task
+ .eval(doCleanup())
+ .executeOn(scheduler)
+
+ def runCleanupOn(observable: Observable[_]): Cancelable = {
+ observable
+ .whileBusyDropEventsAndSignal(dropped => log.warn(s"UTX pool cleanup is too slow, $dropped cleanups skipped"))
+ .mapTask(_ => runCleanupTask)
+ .doOnComplete(() => log.debug("UTX pool cleanup stopped"))
+ .doOnError(err => log.error("UTX pool cleanup error", err))
+ .subscribe()
+ }
- transactionsBytes = transactions.values.asScala // Bytes size of all transactions in pool
- .map(_.bytes().size)
- .sum
- _ <- Either.cond((transactionsBytes + tx.bytes().size) <= utxSettings.maxBytesSize,
- (),
- GenericError("Transaction pool bytes size limit is reached"))
+ private[UtxPoolImpl] def doExpiredCleanup(): Unit = {
+ transactions.entrySet().removeIf { entry =>
+ val tx = entry.getValue
+ val remove = TxCheck.transactionIsExpired(tx)
+ if (remove) UtxPoolImpl.this.afterRemove(tx)
+ remove
+ }
+ }
- _ <- checkNotBlacklisted(tx)
- _ <- checkScripted(b, tx)
- _ <- checkAlias(b, tx)
- _ <- canReissue(b, tx)
- diff <- TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height)(b, tx)
- } yield {
- pessimisticPortfolios.add(tx.id(), diff)
- val isNew = Option(transactions.put(tx.id(), tx)).isEmpty
- if (isNew) PoolMetrics.addTransaction(tx)
- (isNew, diff)
- }
+ private[UtxPoolImpl] def doCleanup(): Unit = {
+ transactions.entrySet().removeIf { entry =>
+ val tx = entry.getValue
+ val remove = !TxCheck.transactionIsValid(tx)
+ if (remove) UtxPoolImpl.this.afterRemove(tx)
+ remove
}
- )
- result.fold(
- err => log.trace(s"UTX putIfNew(${tx.id()}) failed with $err"),
- r => log.trace(s"UTX putIfNew(${tx.id()}) succeeded, isNew = ${r._1}")
- )
- result
+ }
+ }
+
+ override def close(): Unit = {
+ cleanup.scheduler.shutdown()
}
}
object UtxPoolImpl {
-
- private class PessimisticPortfolios(portfolioChanges: Observer[Address]) {
+ private class PessimisticPortfolios(spendableBalanceChanged: Observer[(Address, Option[AssetId])]) {
private type Portfolios = Map[Address, Portfolio]
private val transactionPortfolios = new ConcurrentHashMap[ByteStr, Portfolios]()
private val transactions = new ConcurrentHashMap[Address, Set[ByteStr]]()
@@ -238,12 +272,13 @@ object UtxPoolImpl {
Option(transactionPortfolios.put(txId, nonEmptyPessimisticPortfolios)).isEmpty) {
nonEmptyPessimisticPortfolios.keys.foreach { address =>
transactions.put(address, transactions.getOrDefault(address, Set.empty) + txId)
- portfolioChanges.onNext(address)
}
}
// Because we need to notify about balance changes when they are applied
- pessimisticPortfolios.iterator.collect { case (addr, p) if p.isEmpty => addr }.foreach(portfolioChanges.onNext)
+ pessimisticPortfolios.foreach {
+ case (addr, p) => p.assetIds.foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId))
+ }
}
def getAggregated(accountAddr: Address): Portfolio = {
@@ -257,13 +292,15 @@ object UtxPoolImpl {
}
def remove(txId: ByteStr): Unit = {
- if (Option(transactionPortfolios.remove(txId)).isDefined) {
- transactions.keySet().asScala.foreach { addr =>
- transactions.put(addr, transactions.getOrDefault(addr, Set.empty) - txId)
- portfolioChanges.onNext(addr)
- }
+ Option(transactionPortfolios.remove(txId)) match {
+ case Some(txPortfolios) =>
+ txPortfolios.foreach {
+ case (addr, p) =>
+ transactions.computeIfPresent(addr, (_, prevTxs) => prevTxs - txId)
+ p.assetIds.foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId))
+ }
+ case None =>
}
}
}
-
}
diff --git a/src/test/scala/com/zbsnetwork/TransactionGen.scala b/src/test/scala/com/zbsnetwork/TransactionGen.scala
index 4d30f4b..15daf9a 100644
--- a/src/test/scala/com/zbsnetwork/TransactionGen.scala
+++ b/src/test/scala/com/zbsnetwork/TransactionGen.scala
@@ -1,19 +1,13 @@
package com.zbsnetwork
-import cats.syntax.semigroup._
import com.zbsnetwork.account.PublicKeyAccount._
import com.zbsnetwork.account._
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
-import com.zbsnetwork.lang.Global
import com.zbsnetwork.lang.StdLibVersion._
-import com.zbsnetwork.lang.contract.Contract
-import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction}
-import com.zbsnetwork.lang.v1.FunctionHeader
+import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader}
import com.zbsnetwork.lang.v1.compiler.Terms._
-import com.zbsnetwork.lang.v1.compiler.{ExpressionCompiler, Terms}
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
-import com.zbsnetwork.lang.v1.testing.ScriptGen
+import com.zbsnetwork.lang.v1.testing.{ScriptGen, TypedScriptGen}
import com.zbsnetwork.settings.Constants
import com.zbsnetwork.state._
import com.zbsnetwork.state.diffs.ENOUGH_AMT
@@ -21,8 +15,8 @@ import com.zbsnetwork.transaction._
import com.zbsnetwork.transaction.assets._
import com.zbsnetwork.transaction.assets.exchange._
import com.zbsnetwork.transaction.lease._
-import com.zbsnetwork.transaction.smart.script.{ContractScript, Script}
import com.zbsnetwork.transaction.smart.script.v1.ExprScript
+import com.zbsnetwork.transaction.smart.script.{ContractScript, Script}
import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction}
import com.zbsnetwork.transaction.transfer.MassTransferTransaction.{MaxTransferCount, ParsedTransfer}
import com.zbsnetwork.transaction.transfer._
@@ -36,7 +30,7 @@ trait TransactionGen extends TransactionGenBase { _: Suite =>
}
-trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
+trait TransactionGenBase extends ScriptGen with TypedScriptGen with NTPTime { _: Suite =>
val ScriptExtraFee = 400000L
protected def zbs(n: Float): Long = (n * 100000000L).toLong
@@ -90,7 +84,7 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
val positiveLongGen: Gen[Long] = Gen.choose(1, 100000000L * 100000000L / 100)
val positiveIntGen: Gen[Int] = Gen.choose(1, Int.MaxValue / 100)
- val smallFeeGen: Gen[Long] = Gen.choose(400000, 100000000)
+ val smallFeeGen: Gen[Long] = Gen.choose(50000000000L, 100000000000L)
val maxOrderTimeGen: Gen[Long] = Gen.choose(10000L, Order.MaxLiveTime).map(_ + ntpTime.correctedTime())
val timestampGen: Gen[Long] = Gen.choose(1, Long.MaxValue - 100)
@@ -110,25 +104,16 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
proofs <- Gen.listOfN(proofsAmount, genBoundedBytes(0, 50))
} yield Proofs.create(proofs.map(ByteStr(_))).explicitGet()
- val scriptGen = BOOLgen(100).map {
- case (expr, _) =>
- val typed =
- ExpressionCompiler(PureContext.build(V1).compilerContext |+| CryptoContext.compilerContext(Global), expr).explicitGet()
- ExprScript(typed._1).explicitGet()
- }
-
- val contractGen = Gen.const(
- ContractScript(V3, Contract(List.empty, List(CallableFunction(CallableAnnotation("sender"), Terms.FUNC("foo", List("a"), Terms.REF("a")))), None))
- .explicitGet()
- )
-
+ val scriptGen: Gen[Script] = exprGen.map(e => ExprScript(e).explicitGet())
+ val contractScriptGen: Gen[Script] = contractGen.map(e => ContractScript(V3, e).explicitGet())
+ val contractOrExpr = Gen.oneOf(scriptGen, contractScriptGen)
val setAssetScriptTransactionGen: Gen[(Seq[Transaction], SetAssetScriptTransaction)] = for {
version <- Gen.oneOf(SetScriptTransaction.supportedVersions.toSeq)
(sender, assetName, description, quantity, decimals, _, iFee, timestamp) <- issueParamGen
fee <- smallFeeGen
timestamp <- timestampGen
proofs <- proofsGen
- script <- Gen.option(Gen.oneOf(scriptGen, contractGen))
+ script <- Gen.option(scriptGen)
issue = IssueTransactionV2
.selfSigned(AddressScheme.current.chainId, sender, assetName, description, quantity, decimals, reissuable = true, script, iFee, timestamp)
.explicitGet()
@@ -143,7 +128,7 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
fee <- smallFeeGen
timestamp <- timestampGen
proofs <- proofsGen
- script <- Gen.option(scriptGen)
+ script <- Gen.option(contractOrExpr)
} yield SetScriptTransaction.create(sender, script, fee, timestamp, proofs).explicitGet()
def selfSignedSetScriptTransactionGenP(sender: PrivateKeyAccount, s: Script): Gen[SetScriptTransaction] =
@@ -336,9 +321,8 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
val massTransferGen: Gen[MassTransferTransaction] = massTransferGen(MaxTransferCount)
- def massTransferGen(maxTransfersCount: Int) =
+ def massTransferGen(maxTransfersCount: Int): Gen[MassTransferTransaction] = {
for {
- version <- Gen.oneOf(MassTransferTransaction.supportedVersions.toSeq)
(assetId, sender, _, _, timestamp, _, feeAmount, attachment) <- transferParamGen
transferCount <- Gen.choose(0, maxTransfersCount)
transferGen = for {
@@ -347,8 +331,9 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
} yield ParsedTransfer(recipient, amount)
recipients <- Gen.listOfN(transferCount, transferGen)
} yield MassTransferTransaction.selfSigned(assetId, sender, recipients, timestamp, feeAmount, attachment).explicitGet()
+ }
- val MinIssueFee = 100000000
+ val MinIssueFee = 50000000000L
val createAliasGen: Gen[CreateAliasTransaction] = for {
timestamp: Long <- positiveLongGen
@@ -416,7 +401,6 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
fee: Long,
timestamp: Long): Gen[ReissueTransaction] = {
for {
- version <- versionGen(ReissueTransactionV2)
tx <- Gen.oneOf(
ReissueTransactionV1
.selfSigned(reissuer, assetId, quantity, reissuable, fee, timestamp)
@@ -496,9 +480,9 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
assetId = issue.assetId()
} yield
(issue,
- SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee), 1 * Constants.UnitsInZbs, timestamp).explicitGet(),
- SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee1), 1 * Constants.UnitsInZbs, timestamp).explicitGet(),
- SponsorFeeTransaction.selfSigned(sender, assetId, None, 1 * Constants.UnitsInZbs, timestamp).explicitGet(),
+ SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee), 50 * Constants.UnitsInZbs, timestamp).explicitGet(),
+ SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee1), 50 * Constants.UnitsInZbs, timestamp).explicitGet(),
+ SponsorFeeTransaction.selfSigned(sender, assetId, None, 50 * Constants.UnitsInZbs, timestamp).explicitGet(),
)
val sponsorFeeGen = for {
@@ -518,12 +502,12 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
val funcCallGen = for {
functionName <- genBoundedString(1, 32).map(_.toString)
- amt <- Gen.choose(0, 10)
+ amt <- Gen.choose(0, ContractLimits.MaxContractInvocationArgs)
args <- Gen.listOfN(amt, argGen)
} yield FUNCTION_CALL(FunctionHeader.User(functionName), args)
- val contractInvokationGen = for {
+ val contractInvocationGen = for {
sender <- accountGen
contractAddress <- accountGen
fc <- funcCallGen
@@ -562,7 +546,12 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee) <- orderParamGen
} yield Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, 2: Byte)
- val orderGen: Gen[Order] = Gen.oneOf(orderV1Gen, orderV2Gen)
+ val orderV3Gen: Gen[Order] = for {
+ (sender, matcher, pair, orderType, price, amount, timestamp, expiration, matcherFee) <- orderParamGen
+ matcherFeeAssetId <- assetIdGen
+ } yield Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, 3: Byte, matcherFeeAssetId)
+
+ val orderGen: Gen[Order] = Gen.oneOf(orderV1Gen, orderV2Gen, orderV3Gen)
val arbitraryOrderGen: Gen[Order] = for {
(sender, matcher, pair, orderType, _, _, _, _, _) <- orderParamGen
@@ -577,17 +566,27 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
sender1: PrivateKeyAccount <- accountGen
sender2: PrivateKeyAccount <- accountGen
assetPair <- assetPairGen
+ buyerAnotherAsset <- assetIdGen
+ sellerAnotherAsset <- assetIdGen
+ buyerMatcherFeeAssetId <- Gen.oneOf(assetPair.amountAsset, assetPair.priceAsset, buyerAnotherAsset, None)
+ sellerMatcherFeeAssetId <- Gen.oneOf(assetPair.amountAsset, assetPair.priceAsset, sellerAnotherAsset, None)
r <- Gen.oneOf(
exchangeV1GeneratorP(sender1, sender2, assetPair.amountAsset, assetPair.priceAsset),
- exchangeV2GeneratorP(sender1, sender2, assetPair.amountAsset, assetPair.priceAsset)
+ exchangeV2GeneratorP(
+ buyer = sender1,
+ seller = sender2,
+ amountAssetId = assetPair.amountAsset,
+ priceAssetId = assetPair.priceAsset,
+ buyMatcherFeeAssetId = buyerMatcherFeeAssetId,
+ sellMatcherFeeAssetId = sellerMatcherFeeAssetId
+ )
)
} yield r
def exchangeGeneratorP(buyer: PrivateKeyAccount,
seller: PrivateKeyAccount,
amountAssetId: Option[ByteStr],
- priceAssetId: Option[ByteStr],
- fixedMatcherFee: Option[Long] = None): Gen[ExchangeTransaction] = {
+ priceAssetId: Option[ByteStr]): Gen[ExchangeTransaction] = {
Gen.oneOf(
exchangeV1GeneratorP(buyer, seller, amountAssetId, priceAssetId),
exchangeV2GeneratorP(buyer, seller, amountAssetId, priceAssetId)
@@ -633,23 +632,38 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite =>
amountAssetId: Option[ByteStr],
priceAssetId: Option[ByteStr],
fixedMatcherFee: Option[Long] = None,
- orderVersions: Set[Byte] = Set(1, 2)): Gen[ExchangeTransactionV2] = {
- def mkBuyOrder(version: Byte): OrderConstructor = if (version == 1) OrderV1.buy else OrderV2.buy
- def mkSellOrder(version: Byte): OrderConstructor = if (version == 1) OrderV1.sell else OrderV2.sell
+ orderVersions: Set[Byte] = Set(1, 2, 3),
+ buyMatcherFeeAssetId: Option[ByteStr] = None,
+ sellMatcherFeeAssetId: Option[ByteStr] = None,
+ fixedMatcher: Option[PrivateKeyAccount] = None): Gen[ExchangeTransactionV2] = {
+
+ def mkBuyOrder(version: Byte): OrderConstructor = version match {
+ case 1 => OrderV1.buy
+ case 2 => OrderV2.buy
+ case 3 => OrderV3.buy(_, _, _, _, _, _, _, _, buyMatcherFeeAssetId)
+ }
+
+ def mkSellOrder(version: Byte): OrderConstructor = version match {
+ case 1 => OrderV1.sell
+ case 2 => OrderV2.sell
+ case 3 => OrderV3.sell(_, _, _, _, _, _, _, _, sellMatcherFeeAssetId)
+ }
for {
- (_, matcher, _, _, price, amount1, timestamp, expiration, genMatcherFee) <- orderParamGen
- amount2: Long <- matcherAmountGen
- matcherFee = fixedMatcherFee.getOrElse(genMatcherFee)
+ (_, generatedMatcher, _, _, amount1, price, timestamp, expiration, generatedMatcherFee) <- orderParamGen
+ amount2: Long <- matcherAmountGen
+ matcher = fixedMatcher.getOrElse(generatedMatcher)
+ matcherFee = fixedMatcherFee.getOrElse(generatedMatcherFee)
matchedAmount: Long <- Gen.choose(Math.min(amount1, amount2) / 2000, Math.min(amount1, amount2) / 1000)
assetPair = AssetPair(amountAssetId, priceAssetId)
mkO1 <- Gen.oneOf(orderVersions.map(mkBuyOrder).toSeq)
mkO2 <- Gen.oneOf(orderVersions.map(mkSellOrder).toSeq)
} yield {
+
val buyFee = (BigInt(matcherFee) * BigInt(matchedAmount) / BigInt(amount1)).longValue()
val sellFee = (BigInt(matcherFee) * BigInt(matchedAmount) / BigInt(amount2)).longValue()
- val o1 = mkO1(seller, matcher, assetPair, amount1, price, timestamp, expiration, matcherFee)
+ val o1 = mkO1(buyer, matcher, assetPair, amount1, price, timestamp, expiration, matcherFee)
val o2 = mkO2(seller, matcher, assetPair, amount2, price, timestamp, expiration, matcherFee)
ExchangeTransactionV2
diff --git a/src/test/scala/com/zbsnetwork/WithDB.scala b/src/test/scala/com/zbsnetwork/WithDB.scala
index 79e900c..94d0030 100644
--- a/src/test/scala/com/zbsnetwork/WithDB.scala
+++ b/src/test/scala/com/zbsnetwork/WithDB.scala
@@ -4,6 +4,7 @@ import java.nio.file.Files
import com.zbsnetwork.account.Address
import com.zbsnetwork.db.LevelDBFactory
+import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.utils.Implicits.SubjectOps
import monix.reactive.subjects.Subject
import org.iq80.leveldb.{DB, Options}
@@ -17,7 +18,7 @@ trait WithDB extends BeforeAndAfterEach {
def db: DB = currentDBInstance
- protected val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address]
+ protected val ignoreSpendableBalanceChanged: Subject[(Address, Option[AssetId]), (Address, Option[AssetId])] = Subject.empty
override def beforeEach(): Unit = {
currentDBInstance = LevelDBFactory.factory.open(path.toFile, new Options().createIfMissing(true))
diff --git a/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala b/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala
index a7376c6..ee43cc3 100644
--- a/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala
+++ b/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala
@@ -203,10 +203,10 @@ class FPPoSSelectorTest extends FreeSpec with Matchers with WithDB with Transact
}
def withEnv(gen: Time => Gen[(Seq[PrivateKeyAccount], Seq[Block])])(f: Env => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
val pos = new PoSSelector(bcu, settings.blockchainSettings, settings.synchronizationSettings)
try {
val (accounts, blocks) = gen(ntpTime).sample.get
diff --git a/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala b/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala
index 15830ec..b119c82 100644
--- a/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala
+++ b/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala
@@ -39,7 +39,7 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
import TestFunctionalitySettings.Enabled
"correctly joins height ranges" in {
val fs = Enabled.copy(preActivatedFeatures = Map(BlockchainFeatures.SmartAccountTrading.id -> 0))
- val writer = new LevelDBWriter(db, ignorePortfolioChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000)
+ val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000)
writer.merge(Seq(15, 12, 3), Seq(12, 5)) shouldEqual Seq((15, 12), (12, 12), (3, 5))
writer.merge(Seq(12, 5), Seq(15, 12, 3)) shouldEqual Seq((12, 15), (12, 12), (5, 3))
writer.merge(Seq(8, 4), Seq(8, 4)) shouldEqual Seq((8, 8), (4, 4))
@@ -47,14 +47,14 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
}
"hasScript" - {
"returns false if a script was not set" in {
- val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
writer.hasScript(accountGen.sample.get.toAddress) shouldBe false
}
"returns false if a script was set and then unset" in {
assume(BlockchainFeatures.implemented.contains(BlockchainFeatures.SmartAccounts.id))
resetTest { (_, account) =>
- val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
writer.hasScript(account) shouldBe false
}
}
@@ -63,7 +63,7 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
"if there is a script in db" in {
assume(BlockchainFeatures.implemented.contains(BlockchainFeatures.SmartAccounts.id))
test { (_, account) =>
- val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
writer.hasScript(account) shouldBe true
}
}
@@ -110,10 +110,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
}
def baseTest(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (LevelDBWriter, PrivateKeyAccount) => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
try {
val (account, blocks) = gen(ntpTime).sample.get
@@ -130,10 +130,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
}
def testWithBlocks(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (LevelDBWriter, Seq[Block], PrivateKeyAccount) => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
try {
val (account, blocks) = gen(ntpTime).sample.get
@@ -175,10 +175,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
}
"correctly reassemble block from header and transactions" in {
- val rw = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
+ val rw = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(rw, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(rw, ignoreSpendableBalanceChanged, settings, ntpTime)
try {
val master = PrivateKeyAccount("master".getBytes())
val recipient = PrivateKeyAccount("recipient".getBytes())
@@ -266,10 +266,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with
} yield (leaser, leaseTxs, blocks.reverse)
}
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
try {
val (leaser, leases, blocks) = precs.sample.get
diff --git a/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala b/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala
index 7b4bd94..7acfa11 100644
--- a/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala
+++ b/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala
@@ -129,10 +129,10 @@ class ScriptCacheTest extends FreeSpec with Matchers with WithDB with Transactio
}
def withBlockchain(gen: Time => Gen[(Seq[PrivateKeyAccount], Seq[Block])])(f: (Seq[PrivateKeyAccount], BlockchainUpdater with NG) => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, CACHE_SIZE, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, CACHE_SIZE, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
try {
val (accounts, blocks) = gen(ntpTime).sample.get
diff --git a/src/test/scala/com/zbsnetwork/db/WithState.scala b/src/test/scala/com/zbsnetwork/db/WithState.scala
index 15fc579..0db4cdd 100644
--- a/src/test/scala/com/zbsnetwork/db/WithState.scala
+++ b/src/test/scala/com/zbsnetwork/db/WithState.scala
@@ -8,17 +8,18 @@ import com.zbsnetwork.database.LevelDBWriter
import com.zbsnetwork.history.Domain
import com.zbsnetwork.settings.{FunctionalitySettings, ZbsSettings, loadConfig}
import com.zbsnetwork.state.{Blockchain, BlockchainUpdaterImpl}
+import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.utils.Implicits.SubjectOps
import com.zbsnetwork.{NTPTime, TestHelpers}
import monix.reactive.subjects.Subject
import org.scalatest.Suite
trait WithState extends DBCacheSettings {
- protected val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address]
+ protected val ignoreSpendableBalanceChanged: Subject[(Address, Option[AssetId]), (Address, Option[AssetId])] = Subject.empty
protected def withState[A](fs: FunctionalitySettings)(f: Blockchain => A): A = {
val path = Files.createTempDirectory("leveldb-test")
val db = openDB(path.toAbsolutePath.toString)
- try f(new LevelDBWriter(db, ignorePortfolioChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000))
+ try f(new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000))
finally {
db.close()
TestHelpers.deleteRecursively(path)
@@ -33,7 +34,7 @@ trait WithDomain extends WithState with NTPTime {
def withDomain[A](settings: ZbsSettings = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())))(test: Domain => A): A = {
try withState(settings.blockchainSettings.functionalitySettings) { blockchain =>
- val bcu = new BlockchainUpdaterImpl(blockchain, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(blockchain, ignoreSpendableBalanceChanged, settings, ntpTime)
try test(Domain(bcu))
finally bcu.shutdown()
} finally {}
diff --git a/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala b/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala
index 412bb73..33e3443 100644
--- a/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala
+++ b/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala
@@ -45,7 +45,7 @@ class BlockchainUpdaterInMemoryDiffTest
domain.blockchainUpdater.height shouldBe MaxTransactionsPerBlockDiff * 2 + 2
- val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.portfolio(genesis.recipient).balance
+ val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.balance(genesis.recipient)
mastersBalanceAfterPayment1AndPayment2 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee - payment2.amount - payment2.fee)
}
}
@@ -61,7 +61,7 @@ class BlockchainUpdaterInMemoryDiffTest
firstBlocks.foreach(b => domain.blockchainUpdater.processBlock(b).explicitGet())
domain.blockchainUpdater.processBlock(payment1Block).explicitGet()
domain.blockchainUpdater.processBlock(emptyBlock).explicitGet()
- val mastersBalanceAfterPayment1 = domain.blockchainUpdater.portfolio(genesis.recipient).balance
+ val mastersBalanceAfterPayment1 = domain.blockchainUpdater.balance(genesis.recipient)
mastersBalanceAfterPayment1 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee)
// discard liquid block
@@ -70,7 +70,7 @@ class BlockchainUpdaterInMemoryDiffTest
domain.blockchainUpdater.height shouldBe MaxTransactionsPerBlockDiff * 2 + 1
- val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.portfolio(genesis.recipient).balance
+ val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.balance(genesis.recipient)
mastersBalanceAfterPayment1AndPayment2 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee - payment2.amount - payment2.fee)
}
}
diff --git a/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala
index f430968..95920a7 100644
--- a/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala
+++ b/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala
@@ -14,8 +14,7 @@ import com.zbsnetwork.transaction.transfer._
import com.zbsnetwork.transaction.{Proofs, Transaction}
import com.zbsnetwork.utx.UtxPool
import com.zbsnetwork.wallet.Wallet
-import io.netty.channel.group.ChannelGroup
-import org.scalacheck.Gen._
+import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher}
import org.scalacheck.{Gen => G}
import org.scalamock.scalatest.PathMockFactory
import org.scalatest.prop.PropertyChecks
@@ -127,7 +126,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ
def posting[A: Writes](v: A): RouteTestResult = Post(routePath("transfer"), v) ~> route
forAll(nonPositiveLong) { q =>
- posting(tr.copy(amount = q)) should produce(NegativeAmount(s"$q of zbs"))
+ posting(tr.copy(amount = q)) should produce(NegativeAmount(s"$q of ${tr.assetId.getOrElse("zbs")}"))
}
forAll(invalidBase58) { pk =>
posting(tr.copy(senderPublicKey = pk)) should produce(InvalidAddress)
@@ -144,9 +143,6 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ
forAll(longAttachment) { a =>
posting(tr.copy(attachment = Some(a))) should produce(CustomValidationError("invalid.attachment"))
}
- forAll(posNum[Long]) { quantity =>
- posting(tr.copy(amount = quantity, fee = Long.MaxValue)) should produce(OverflowError)
- }
forAll(nonPositiveLong) { fee =>
posting(tr.copy(fee = fee)) should produce(InsufficientFee())
}
@@ -159,7 +155,11 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ
(alwaysApproveUtx.putIfNew _).when(*).onCall((_: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes()
val alwaysSendAllChannels = stub[ChannelGroup]
- (alwaysSendAllChannels.writeAndFlush(_: Any)).when(*).onCall((_: Any) => null).anyNumberOfTimes()
+ (alwaysSendAllChannels
+ .writeAndFlush(_: Any, _: ChannelMatcher))
+ .when(*, *)
+ .onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture])
+ .anyNumberOfTimes()
val route = AssetsBroadcastApiRoute(settings, alwaysApproveUtx, alwaysSendAllChannels).route
@@ -213,7 +213,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ
}
"returns a error if it is not a transfer request" in posting(issueReq.sample.get) ~> check {
- status shouldNot be(StatusCodes.OK)
+ status shouldBe StatusCodes.BadRequest
}
}
@@ -250,7 +250,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ
}
"returns a error if it is not a transfer request" in posting(List(issueReq.sample.get)) ~> check {
- status shouldNot be(StatusCodes.OK)
+ status shouldBe StatusCodes.BadRequest
}
}
diff --git a/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala
index bcd5a89..0a22ffd 100644
--- a/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala
+++ b/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala
@@ -7,7 +7,7 @@ import com.zbsnetwork.settings.RestAPISettings
import com.zbsnetwork.state.{Blockchain, Diff}
import com.zbsnetwork.utx.UtxPool
import com.zbsnetwork.{RequestGen, TestTime}
-import io.netty.channel.group.ChannelGroup
+import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher}
import org.scalamock.scalatest.PathMockFactory
import org.scalatest.concurrent.Eventually
import play.api.libs.json.Writes
@@ -31,7 +31,7 @@ class AssetsRouteSpec extends RouteSpec("/assets") with RequestGen with PathMock
(wallet.privateKeyAccount _).when(senderPrivateKey.toAddress).onCall((_: Address) => Right(senderPrivateKey)).anyNumberOfTimes()
(utx.putIfNew _).when(*).onCall((_: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes()
- (allChannels.writeAndFlush(_: Any)).when(*).onCall((_: Any) => null).anyNumberOfTimes()
+ (allChannels.writeAndFlush(_: Any, _: ChannelMatcher)).when(*, *).onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture]).anyNumberOfTimes()
"/transfer" - {
val route = AssetsApiRoute(settings, wallet, utx, allChannels, state, new TestTime()).route
diff --git a/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala
index fedf69e..f4126bd 100644
--- a/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala
+++ b/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala
@@ -25,7 +25,7 @@ class LeaseBroadcastRouteSpec extends RouteSpec("/leasing/broadcast/") with Requ
(utx.putIfNew _).when(*).onCall((t: Transaction) => Left(TransactionValidationError(GenericError("foo"), t))).anyNumberOfTimes()
- "returns StateCheckFiled" - {
+ "returns StateCheckFailed" - {
val route = LeaseBroadcastApiRoute(settings, utx, allChannels).route
val vt = Table[String, G[_ <: Transaction], (JsValue) => JsValue](
diff --git a/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala
index 7ae544e..0939680 100644
--- a/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala
+++ b/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala
@@ -9,7 +9,7 @@ import com.zbsnetwork.transaction.transfer._
import com.zbsnetwork.utils.Time
import com.zbsnetwork.utx.UtxPool
import com.zbsnetwork.{NoShrink, TestWallet, TransactionGen}
-import io.netty.channel.group.ChannelGroup
+import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher}
import org.scalamock.scalatest.MockFactory
import org.scalatest.prop.PropertyChecks
import play.api.libs.json.{JsObject, Json}
@@ -23,10 +23,12 @@ class PaymentRouteSpec
with TransactionGen
with NoShrink {
- private val utx = stub[UtxPool]
- (utx.putIfNew _).when(*).onCall((t: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes()
+ private val utx = stub[UtxPool]
private val allChannels = stub[ChannelGroup]
+ (utx.putIfNew _).when(*).onCall((t: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes()
+ (allChannels.writeAndFlush(_: Any, _: ChannelMatcher)).when(*, *).onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture]).anyNumberOfTimes()
+
"accepts payments" in {
forAll(accountOrAliasGen.label("recipient"), positiveLongGen.label("amount"), smallFeeGen.label("fee")) {
case (recipient, amount, fee) =>
diff --git a/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala
index ab6f4d6..7e0ad72 100644
--- a/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala
@@ -184,20 +184,24 @@ class AddressActorSpecification
Props(
new AddressActor(
address,
- currentPortfolio.get(),
- 1.day,
+ x => currentPortfolio.get().spendableBalanceOf(x),
1.day,
ntpTime,
EmptyOrderDB,
+ _ => false,
event => {
eventsProbe.ref ! event
Future.successful(QueueEventWithMeta(0, 0, event))
}
)))
- f(addressActor, eventsProbe, (updatedPortfolio, notify) => {
- currentPortfolio.set(updatedPortfolio)
- if (notify) addressActor ! BalanceUpdated
- })
+ f(
+ addressActor,
+ eventsProbe,
+ (updatedPortfolio, notify) => {
+ val prevPortfolio = currentPortfolio.getAndSet(updatedPortfolio)
+ if (notify) addressActor ! BalanceUpdated(prevPortfolio.changedAssetIds(updatedPortfolio))
+ }
+ )
addressActor ! PoisonPill
}
diff --git a/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala b/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala
index 98e1d4b..f3eb919 100644
--- a/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala
@@ -6,7 +6,7 @@ import com.zbsnetwork.matcher.model.{OrderInfo, OrderStatus}
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order}
object EmptyOrderDB extends OrderDB {
- override def contains(id: ByteStr): Boolean = false
+ override def containsInfo(id: ByteStr): Boolean = false
override def status(id: ByteStr): OrderStatus.Final = OrderStatus.NotFound
override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = {}
override def saveOrder(o: Order): Unit = {}
diff --git a/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala b/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala
index 4aed4bb..044da10 100644
--- a/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala
@@ -11,11 +11,11 @@ class TestOrderDB(maxOrdersPerRequest: Int) extends OrderDB {
private var idsForPair = Map.empty[(Address, AssetPair), Seq[ByteStr]].withDefaultValue(Seq.empty)
private var idsForAddress = Map.empty[Address, Seq[ByteStr]].withDefaultValue(Seq.empty)
- override def contains(id: ByteStr): Boolean = knownOrders.contains(id)
+ override def containsInfo(id: ByteStr): Boolean = orderInfo.contains(id)
override def status(id: ByteStr): OrderStatus.Final = orderInfo.get(id).fold[OrderStatus.Final](OrderStatus.NotFound)(_.status)
- override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = if (!orderInfo.contains(id)) {
+ override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = if (!containsInfo(id)) {
orderInfo += id -> oi
idsForAddress += sender -> (id +: idsForAddress(sender))
idsForPair += (sender, oi.assetPair) -> (id +: idsForPair(sender -> oi.assetPair))
diff --git a/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala
index 7a7338b..88049ad 100644
--- a/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala
@@ -99,8 +99,10 @@ class MatcherActorSpecification
actor ! wrap(order1)
actor ! wrap(order2)
- ob.get()(pair1) shouldBe 'right
- ob.get()(pair2) shouldBe 'right
+ eventually {
+ ob.get()(pair1) shouldBe 'right
+ ob.get()(pair2) shouldBe 'right
+ }
val toKill = actor.getChild(List(OrderBookActor.name(pair1)).iterator)
diff --git a/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala
index cf291d1..e04456a 100644
--- a/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala
@@ -5,7 +5,6 @@ import java.util.concurrent.ConcurrentHashMap
import akka.actor.{ActorRef, Props}
import akka.testkit.{ImplicitSender, TestProbe}
import com.zbsnetwork.NTPTime
-import com.zbsnetwork.OrderOps._
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.matcher.MatcherTestData
import com.zbsnetwork.matcher.api.AlreadyProcessed
@@ -15,6 +14,7 @@ import com.zbsnetwork.matcher.market.MatcherActor.SaveSnapshot
import com.zbsnetwork.matcher.market.OrderBookActor._
import com.zbsnetwork.matcher.model.Events.OrderAdded
import com.zbsnetwork.matcher.model._
+import com.zbsnetwork.transaction.assets.exchange.OrderOps._
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order}
import com.zbsnetwork.utils.EmptyBlockchain
import org.scalamock.scalatest.PathMockFactory
diff --git a/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala
index a3a5104..41d31c0 100644
--- a/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala
@@ -1,7 +1,6 @@
package com.zbsnetwork.matcher.market
import com.google.common.base.Charsets
-import com.zbsnetwork.OrderOps._
import com.zbsnetwork.account.{Address, PrivateKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
@@ -13,6 +12,7 @@ import com.zbsnetwork.matcher.model._
import com.zbsnetwork.settings.Constants
import com.zbsnetwork.state.diffs.produce
import com.zbsnetwork.state.{AssetDescription, Blockchain, LeaseBalance, Portfolio}
+import com.zbsnetwork.transaction.assets.exchange.OrderOps._
import com.zbsnetwork.transaction.assets.exchange._
import com.zbsnetwork.transaction.smart.script.ScriptCompiler
import com.zbsnetwork.transaction.smart.script.v1.ExprScript
@@ -36,7 +36,6 @@ class OrderValidatorSpecification
private val wbtc = mkAssetId("WBTC").get
private val pairZbsBtc = AssetPair(None, Some(wbtc))
- private val defaultTs = 1000
private val defaultPortfolio = Portfolio(0, LeaseBalance.empty, Map(wbtc -> 10 * Constants.UnitsInZbs))
@@ -116,28 +115,10 @@ class OrderValidatorSpecification
ov(order) should produce("Script doesn't exist and proof doesn't validate as signature")
}
- "default ts - drift > its for new users" in {
+ "order exists" in {
val pk = PrivateKeyAccount(randomBytes())
- val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, 0L, _ => false)(_)
- ov(newBuyOrder(pk, defaultTs - matcherSettings.orderTimestampDrift - 1)) should produce("Order should have a timestamp")
- }
-
- "default ts - drift = its ts for new users" in {
- val pk = PrivateKeyAccount(randomBytes())
- val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, 0L, _ => false)(_)
- ov(newBuyOrder(pk, defaultTs - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp")
- }
-
- "ts1 - drift > ts2" in {
- val pk = PrivateKeyAccount(randomBytes())
- val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, defaultTs + 1000, _ => false)(_)
- ov(newBuyOrder(pk, defaultTs + 999 - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp")
- }
-
- "ts1 - drift = ts2" in {
- val pk = PrivateKeyAccount(randomBytes())
- val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, defaultTs + 1000, _ => false)(_)
- ov(newBuyOrder(pk, defaultTs + 1000 - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp")
+ val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 1, _ => true)(_)
+ ov(newBuyOrder(pk, 1000)) should produce("Order has already been placed")
}
"order price has invalid non-zero trailing decimals" in forAll(assetIdGen(1), accountGen, Gen.choose(1, 7)) {
@@ -332,7 +313,7 @@ class OrderValidatorSpecification
orderStatus: ByteStr => Boolean = _ => false,
o: Order = newBuyOrder
)(f: Either[String, Order] => A): A =
- f(OrderValidator.accountStateAware(o.sender, tradableBalance(p), 0, 0, orderStatus)(o))
+ f(OrderValidator.accountStateAware(o.sender, tradableBalance(p), 0, orderStatus)(o))
private def msa(ba: Set[Address], o: Order) = OrderValidator.matcherSettingsAware(o.matcherPublicKey, ba, Set.empty) _
}
diff --git a/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala
index 2640bbb..cf3b274 100644
--- a/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala
@@ -8,7 +8,6 @@ import com.zbsnetwork.matcher.market.MatcherSpecLike
import com.zbsnetwork.matcher.model.Events.{OrderAdded, OrderExecuted}
import com.zbsnetwork.matcher.model.{LimitOrder, OrderHistoryStub}
import com.zbsnetwork.matcher.{AssetPairDecimals, MatcherTestData, _}
-import com.zbsnetwork.state.Portfolio
import com.zbsnetwork.transaction.AssetId
import com.zbsnetwork.transaction.assets.exchange.OrderType.{BUY, SELL}
import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order, OrderType}
@@ -83,12 +82,19 @@ class ReservedBalanceSpecification
private val addressDir = system.actorOf(
Props(
new AddressDirectory(
- ignorePortfolioChanged,
- _ => Portfolio.empty,
- _ => Future.failed(new IllegalStateException("Should not be used in the test")),
+ ignoreSpendableBalanceChanged,
matcherSettings,
- ntpTime,
- new TestOrderDB(100)
+ address =>
+ Props(
+ new AddressActor(
+ address,
+ _ => 0L,
+ 5.seconds,
+ ntpTime,
+ new TestOrderDB(100),
+ _ => false,
+ _ => Future.failed(new IllegalStateException("Should not be used in the test"))
+ ))
)
))
diff --git a/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala b/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala
index 8d13896..4893c50 100644
--- a/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala
@@ -35,9 +35,8 @@ class OrderDBSpec extends FreeSpec with Matchers with WithDB with MatcherTestDat
"stores" - {
"order" in test { odb =>
forAll(orderGenerator) {
- case (order, _) =>
- odb.saveOrder(order)
- odb.contains(order.id()) shouldBe true
+ case (o, _) =>
+ odb.saveOrder(o)
}
}
@@ -45,6 +44,7 @@ class OrderDBSpec extends FreeSpec with Matchers with WithDB with MatcherTestDat
forAll(finalizedOrderInfoGen) {
case (o, oi) =>
odb.saveOrderInfo(o.id(), o.sender, oi)
+ odb.containsInfo(o.id()) shouldBe true
odb.status(o.id()) shouldBe oi.status
}
}
diff --git a/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala b/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala
index f445daa..eef04c1 100644
--- a/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala
+++ b/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala
@@ -5,7 +5,6 @@ import com.zbsnetwork.account.Address
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.matcher.queue.QueueEventWithMeta
import com.zbsnetwork.matcher.{AddressActor, TestOrderDB}
-import com.zbsnetwork.state.Portfolio
import com.zbsnetwork.utils.Time
import scala.collection.mutable
@@ -23,11 +22,11 @@ class OrderHistoryStub(system: ActorSystem, time: Time) {
Props(
new AddressActor(
lo.order.sender,
- Portfolio.empty,
- 5.seconds,
+ _ => 0L,
5.seconds,
time,
new TestOrderDB(100),
+ _ => false,
e => Future.successful(QueueEventWithMeta(0, 0, e)),
)))
)
diff --git a/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala b/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala
index f4350ba..0fa0680 100644
--- a/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala
+++ b/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala
@@ -18,7 +18,7 @@ import com.zbsnetwork.settings.{ZbsSettings, _}
import com.zbsnetwork.state._
import com.zbsnetwork.state.appender.BlockAppender
import com.zbsnetwork.state.diffs.ENOUGH_AMT
-import com.zbsnetwork.transaction.{BlockchainUpdater, GenesisTransaction, Transaction}
+import com.zbsnetwork.transaction.{AssetId, BlockchainUpdater, GenesisTransaction, Transaction}
import com.zbsnetwork.utils.BaseTargetReachedMaximum
import com.zbsnetwork.utx.UtxPool
import com.zbsnetwork.wallet.Wallet
@@ -109,7 +109,7 @@ class BlockWithMaxBaseTargetTest extends FreeSpec with Matchers with WithDB with
}
def withEnv(f: Env => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
val minerSettings = settings0.minerSettings.copy(quorum = 0)
@@ -126,19 +126,19 @@ class BlockWithMaxBaseTargetTest extends FreeSpec with Matchers with WithDB with
featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)
)
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
val pos = new PoSSelector(bcu, settings.blockchainSettings, settings.synchronizationSettings)
val utxPoolStub = new UtxPool {
- override def putIfNew(tx: Transaction) = ???
- override def removeAll(txs: Traversable[Transaction]): Unit = {}
- override def accountPortfolio(addr: Address) = ???
- override def portfolio(addr: Address) = ???
- override def all = ???
- override def size = ???
- override def transactionById(transactionId: ByteStr) = ???
- override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ???
- override def close(): Unit = {}
+ override def putIfNew(tx: Transaction) = ???
+ override def removeAll(txs: Traversable[Transaction]): Unit = {}
+ override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long = ???
+ override def pessimisticPortfolio(addr: Address): Portfolio = ???
+ override def all = ???
+ override def size = ???
+ override def transactionById(transactionId: ByteStr) = ???
+ override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ???
+ override def close(): Unit = {}
}
val schedulerService: SchedulerService = Scheduler.singleThread("appender")
diff --git a/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala
index 73924b1..2a01872 100644
--- a/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala
@@ -25,13 +25,11 @@ class MatcherSettingsSpecification extends FlatSpec with Matchers {
| snapshots-loading-timeout = 423s
| start-events-processing-timeout = 543s
| rest-order-limit = 100
- | order-timestamp-drift = 10m
| price-assets = [
| ZBS
| 8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS
| DHgwrRvVyqJsepd32YbBqUeDH4GJ1N984X8QoekjgH8J
| ]
- | max-timestamp-diff = 30d
| blacklisted-assets = ["a"]
| blacklisted-names = ["b"]
| blacklisted-addresses = [
@@ -80,7 +78,6 @@ class MatcherSettingsSpecification extends FlatSpec with Matchers {
settings.snapshotsLoadingTimeout should be(423.seconds)
settings.startEventsProcessingTimeout should be(543.seconds)
settings.maxOrdersPerRequest should be(100)
- settings.orderTimestampDrift should be(10.minutes.toMillis)
settings.priceAssets should be(Seq("ZBS", "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", "DHgwrRvVyqJsepd32YbBqUeDH4GJ1N984X8QoekjgH8J"))
settings.blacklistedAssets shouldBe Set("a")
settings.blacklistedNames.map(_.pattern.pattern()) shouldBe Seq("b")
diff --git a/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala
index a66ebb9..ef4a653 100644
--- a/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala
@@ -32,6 +32,9 @@ class SynchronizationSettingsSpecification extends FlatSpec with Matchers {
| network-tx-cache-time = 70s
| max-buffer-size = 777
| max-buffer-time = 999ms
+ | max-queue-size = 7777
+ | parallelism = 4
+ | max-threads = 2
| }
|
| micro-block-synchronizer {
@@ -62,7 +65,6 @@ class SynchronizationSettingsSpecification extends FlatSpec with Matchers {
maxBlockCacheSize = 2
)
- settings.utxSynchronizerSettings shouldBe UtxSynchronizerSettings(7000000, 70.seconds, 777, 999.millis)
-
+ settings.utxSynchronizerSettings shouldBe UtxSynchronizerSettings(7000000, 70.seconds, 777, 999.millis, 4, 2, 7777)
}
}
diff --git a/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala b/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala
index 2e5aec6..1fd220a 100644
--- a/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala
+++ b/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala
@@ -16,10 +16,12 @@ object TestFunctionalitySettings {
allowMultipleLeaseCancelTransactionUntilTimestamp = 0L,
resetEffectiveBalancesAtHeight = 0,
blockVersion3AfterHeight = 0,
- preActivatedFeatures = Map(BlockchainFeatures.SmartAccounts.id -> 0,
- BlockchainFeatures.SmartAssets.id -> 0,
- BlockchainFeatures.FairPoS.id -> 0,
- BlockchainFeatures.Ride4DApps.id -> 0),
+ preActivatedFeatures = Map(
+ BlockchainFeatures.SmartAccounts.id -> 0,
+ BlockchainFeatures.SmartAssets.id -> 0,
+ BlockchainFeatures.FairPoS.id -> 0,
+ BlockchainFeatures.Ride4DApps.id -> 0
+ ),
doubleFeaturesPeriodsAfterHeight = Int.MaxValue,
maxTransactionTimeBackOffset = 120.minutes,
maxTransactionTimeForwardOffset = 90.minutes
diff --git a/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala
index c04403e..9a360ea 100644
--- a/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala
@@ -5,15 +5,12 @@ import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import org.scalatest.{FlatSpec, Matchers}
-import scala.concurrent.duration._
-
class UTXSettingsSpecification extends FlatSpec with Matchers {
"UTXSettings" should "read values" in {
val config = ConfigFactory.parseString("""zbs {
| utx {
| max-size = 100
| max-bytes-size = 100
- | cleanup-interval = 10m
| blacklist-sender-addresses = ["a"]
| allow-blacklisted-transfer-to = ["b"]
| allow-transactions-from-smart-accounts = false
@@ -23,7 +20,6 @@ class UTXSettingsSpecification extends FlatSpec with Matchers {
val settings = config.as[UtxSettings]("zbs.utx")
settings.maxSize shouldBe 100
settings.maxBytesSize shouldBe 100L
- settings.cleanupInterval shouldBe 10.minutes
settings.blacklistSenderAddresses shouldBe Set("a")
settings.allowBlacklistedTransferTo shouldBe Set("b")
settings.allowTransactionsFromSmartAccounts shouldBe false
diff --git a/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala b/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala
index 1310ac8..34df4c4 100644
--- a/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala
+++ b/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala
@@ -19,9 +19,10 @@ import org.scalatest.{FreeSpec, Matchers}
class BlockchainUpdaterImplSpec extends FreeSpec with Matchers with WithDB with RequestGen with NTPTime with DBCacheSettings {
def baseTest(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (BlockchainUpdaterImpl, PrivateKeyAccount) => Unit): Unit = {
- val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
+ val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000)
val settings = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load()))
- val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime)
+ val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime)
+
try {
val (account, blocks) = gen(ntpTime).sample.get
diff --git a/src/test/scala/com/zbsnetwork/state/NgStateTest.scala b/src/test/scala/com/zbsnetwork/state/NgStateTest.scala
new file mode 100644
index 0000000..e444dbd
--- /dev/null
+++ b/src/test/scala/com/zbsnetwork/state/NgStateTest.scala
@@ -0,0 +1,96 @@
+package com.zbsnetwork.state
+
+import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.history._
+import com.zbsnetwork.state.diffs._
+import com.zbsnetwork.transaction.GenesisTransaction
+import com.zbsnetwork.transaction.transfer._
+import com.zbsnetwork.{NoShrink, TransactionGen}
+import org.scalacheck.Gen
+import org.scalatest.prop.PropertyChecks
+import org.scalatest.{Matchers, PropSpec}
+
+class NgStateTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with NoShrink {
+
+ def preconditionsAndPayments(amt: Int): Gen[(GenesisTransaction, Seq[TransferTransactionV1])] =
+ for {
+ master <- accountGen
+ recipient <- accountGen
+ ts <- positiveIntGen
+ genesis: GenesisTransaction = GenesisTransaction.create(master, ENOUGH_AMT, ts).explicitGet()
+ payments: Seq[TransferTransactionV1] <- Gen.listOfN(amt, zbsTransferGeneratorP(master, recipient))
+ } yield (genesis, payments)
+
+ property("can forge correctly signed blocks") {
+ forAll(preconditionsAndPayments(10)) {
+ case (genesis, payments) =>
+ val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t)))
+
+ val ng = new NgState(block, Diff.empty, 0L, Set.empty)
+ microBlocks.foreach(m => ng.append(m, Diff.empty, 0L, 0L))
+
+ ng.totalDiffOf(microBlocks.last.totalResBlockSig)
+ microBlocks.foreach { m =>
+ ng.totalDiffOf(m.totalResBlockSig).get match {
+ case (forged, _, _, _) => forged.signaturesValid() shouldBe 'right
+ case _ => ???
+ }
+ }
+ Seq(microBlocks(4)).map(x => ng.totalDiffOf(x.totalResBlockSig))
+ }
+ }
+
+ property("can resolve best liquid block") {
+ forAll(preconditionsAndPayments(5)) {
+ case (genesis, payments) =>
+ val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t)))
+
+ val ng = new NgState(block, Diff.empty, 0L, Set.empty)
+ microBlocks.foreach(m => ng.append(m, Diff.empty, 0L, 0L))
+
+ ng.bestLiquidBlock.uniqueId shouldBe microBlocks.last.totalResBlockSig
+
+ new NgState(block, Diff.empty, 0L, Set.empty).bestLiquidBlock.uniqueId shouldBe block.uniqueId
+ }
+ }
+
+ property("can resolve best last block") {
+ forAll(preconditionsAndPayments(5)) {
+ case (genesis, payments) =>
+ val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t)))
+
+ val ng = new NgState(block, Diff.empty, 0L, Set.empty)
+
+ microBlocks.foldLeft(1000) {
+ case (thisTime, m) =>
+ ng.append(m, Diff.empty, 0L, thisTime)
+ thisTime + 50
+ }
+
+ ng.bestLastBlockInfo(0).blockId shouldBe block.uniqueId
+ ng.bestLastBlockInfo(1001).blockId shouldBe microBlocks.head.totalResBlockSig
+ ng.bestLastBlockInfo(1051).blockId shouldBe microBlocks.tail.head.totalResBlockSig
+ ng.bestLastBlockInfo(2000).blockId shouldBe microBlocks.last.totalResBlockSig
+
+ new NgState(block, Diff.empty, 0L, Set.empty).bestLiquidBlock.uniqueId shouldBe block.uniqueId
+ }
+ }
+
+ property("calculates carry fee correctly") {
+ forAll(preconditionsAndPayments(5)) {
+ case (genesis, payments) =>
+ val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t)))
+
+ val ng = new NgState(block, Diff.empty, 0L, Set.empty)
+ microBlocks.foreach(m => ng.append(m, Diff.empty, 1L, 0L))
+
+ ng.totalDiffOf(block.uniqueId).map(_._3) shouldBe Some(0L)
+ microBlocks.zipWithIndex.foreach {
+ case (m, i) =>
+ val u = ng.totalDiffOf(m.totalResBlockSig).map(_._3)
+ u shouldBe Some(i + 1)
+ }
+ ng.carryFee shouldBe microBlocks.size
+ }
+ }
+}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala
index 87e24c9..8d27458 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala
@@ -286,7 +286,7 @@ class AssetTransactionsDiffTest extends PropSpec with PropertyChecks with Matche
case (blockDiff, newState) =>
val totalPortfolioDiff = Monoid.combineAll(blockDiff.portfolios.values)
totalPortfolioDiff.assets(issue.id()) shouldEqual issue.quantity
- newState.portfolio(newState.resolveAlias(transfer.recipient).explicitGet()).assets(issue.id()) shouldEqual transfer.amount
+ newState.balance(newState.resolveAlias(transfer.recipient).explicitGet(), Some(issue.id())) shouldEqual transfer.amount
}
}
}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala
index d5abb68..2f8870f 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala
@@ -52,12 +52,12 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With
"height < enableMicroblocksAfterHeight - a miner should receive 100% of the current block's fee" in {
assertDiff(testChain.init, 1000) {
case (_, s) =>
- s.portfolio(signerA).balance shouldBe 40
+ s.balance(signerA) shouldBe 40
}
assertDiff(testChain, 1000) {
case (_, s) =>
- s.portfolio(signerB).balance shouldBe 50
+ s.balance(signerB) shouldBe 50
}
}
@@ -79,7 +79,7 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With
"height = enableMicroblocksAfterHeight - a miner should receive 40% of the current block's fee only" in {
assertDiff(testChain, 9) {
case (_, s) =>
- s.portfolio(signerB).balance shouldBe 44
+ s.balance(signerB) shouldBe 44
}
}
@@ -101,12 +101,12 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With
"height > enableMicroblocksAfterHeight - a miner should receive 60% of previous block's fee and 40% of the current one" in {
assertDiff(testChain.init, 4) {
case (_, s) =>
- s.portfolio(signerA).balance shouldBe 34
+ s.balance(signerA) shouldBe 34
}
assertDiff(testChain, 4) {
case (_, s) =>
- s.portfolio(signerB).balance shouldBe 50
+ s.balance(signerB) shouldBe 50
}
}
}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala
index fcf3bcb..aab56ba 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala
@@ -15,13 +15,13 @@ import com.zbsnetwork.lang.v1.compiler.Terms._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.FieldNames
import com.zbsnetwork.settings.TestFunctionalitySettings
import com.zbsnetwork.state._
-import com.zbsnetwork.transaction.{AssetId, GenesisTransaction, ValidationError}
-import com.zbsnetwork.transaction.smart.script.ContractScript
-import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction}
-import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment
import com.zbsnetwork.transaction.assets.IssueTransactionV2
+import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment
+import com.zbsnetwork.transaction.smart.script.ContractScript
import com.zbsnetwork.transaction.smart.script.v1.ExprScript
+import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction}
import com.zbsnetwork.transaction.transfer.TransferTransactionV2
+import com.zbsnetwork.transaction.{AssetId, GenesisTransaction}
import com.zbsnetwork.{NoShrink, TransactionGen, WithDB}
import org.scalacheck.Gen
import org.scalatest.prop.PropertyChecks
@@ -29,6 +29,12 @@ import org.scalatest.{Matchers, PropSpec}
class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with NoShrink with WithDB {
+ def ciFee(sc: Int = 0): Gen[Long] =
+ Gen.choose(
+ CommonValidation.FeeUnit * CommonValidation.FeeConstants(ContractInvocationTransaction.typeId) + sc * CommonValidation.ScriptExtraFee,
+ CommonValidation.FeeUnit * CommonValidation.FeeConstants(ContractInvocationTransaction.typeId) + (sc + 1) * CommonValidation.ScriptExtraFee - 1
+ )
+
private val fs = TestFunctionalitySettings.Enabled.copy(
preActivatedFeatures =
Map(BlockchainFeatures.SmartAccounts.id -> 0, BlockchainFeatures.SmartAssets.id -> 0, BlockchainFeatures.Ride4DApps.id -> 0))
@@ -36,87 +42,109 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
val assetAllowed = ExprScript(TRUE).explicitGet()
val assetBanned = ExprScript(FALSE).explicitGet()
- def dataContract(senderBinding: String, argName: String, funcName: String) = Contract(
- List.empty,
- List(
- CallableFunction(
- CallableAnnotation(senderBinding),
- Terms.FUNC(
- funcName,
- List(argName),
+ def dataContract(senderBinding: String, argName: String, funcName: String, bigData: Boolean) = {
+ val datas =
+ if (bigData) List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), CONST_STRING("abcde" * 1024))), REF("nil"))
+ else
+ List(
+ FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), REF(argName))),
FUNCTION_CALL(
- User(FieldNames.WriteSet),
- List(FUNCTION_CALL(
- Native(1102),
+ Native(1100),
+ List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), GETTER(GETTER(REF(senderBinding), "caller"), "bytes"))), REF("nil")))
+ )
+
+ Contract(
+ List.empty,
+ List(
+ CallableFunction(
+ CallableAnnotation(senderBinding),
+ Terms.FUNC(
+ funcName,
+ List(argName),
+ FUNCTION_CALL(
+ User(FieldNames.WriteSet),
List(
- FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), REF(argName))),
- FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), GETTER(GETTER(REF(senderBinding), "caller"), "bytes")))
- )
- ))
+ FUNCTION_CALL(
+ Native(1100),
+ datas
+ ))
+ )
)
- )
- )),
- None
- )
+ )),
+ None
+ )
+ }
def paymentContract(senderBinding: String,
argName: String,
funcName: String,
recipientAddress: Address,
recipientAmount: Long,
- assetId: Option[AssetId] = None) = Contract(
- List.empty,
- List(
- CallableFunction(
- CallableAnnotation(senderBinding),
- Terms.FUNC(
- funcName,
- List(argName),
- FUNCTION_CALL(
- User(FieldNames.TransferSet),
- List(FUNCTION_CALL(
- Native(1102),
- List(
- FUNCTION_CALL(
- User(FieldNames.ContractTransfer),
- List(
- FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(recipientAddress.bytes))),
- CONST_LONG(recipientAmount),
- assetId.fold(REF("unit"): EXPR)(id => CONST_BYTESTR(id))
- )
- )
- )
- ))
+ masspayment: Boolean,
+ paymentCount: Int = 11,
+ assetId: Option[AssetId] = None) = {
+ val oneTransfer = FUNCTION_CALL(
+ User(FieldNames.ContractTransfer),
+ List(
+ FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(recipientAddress.bytes))),
+ CONST_LONG(recipientAmount),
+ assetId.fold(REF("unit"): EXPR)(id => CONST_BYTESTR(id))
+ )
+ )
+
+ val payments =
+ if (masspayment)
+ List(Range(0, paymentCount).foldRight(REF("nil"): EXPR) {
+ case (_, in) =>
+ FUNCTION_CALL(Native(1100), List(oneTransfer, in))
+ })
+ else
+ List(FUNCTION_CALL(Native(1100), List(oneTransfer, REF("nil"))))
+
+ Contract(
+ List.empty,
+ List(
+ CallableFunction(
+ CallableAnnotation(senderBinding),
+ Terms.FUNC(
+ funcName,
+ List(argName),
+ FUNCTION_CALL(
+ User(FieldNames.TransferSet),
+ payments
+ )
)
- )
- )),
- None
- )
+ )),
+ None
+ )
+ }
- def dataContractGen(func: String) =
+ def dataContractGen(func: String, bigData: Boolean) =
for {
senderBinging <- validAliasStringGen
argBinding <- validAliasStringGen
- } yield dataContract(senderBinging, argBinding, func)
+ } yield dataContract(senderBinging, argBinding, func, bigData)
- def paymentContractGen(address: Address, amount: Long, assetId: Option[AssetId] = None)(func: String) =
+ def paymentContractGen(address: Address, amount: Long, masspayment: Boolean, assetId: Option[AssetId] = None, paymentCount: Int = 11)(
+ func: String) =
for {
senderBinging <- validAliasStringGen
argBinding <- validAliasStringGen
- } yield paymentContract(senderBinging, argBinding, func, address, amount, assetId)
+ } yield paymentContract(senderBinging, argBinding, func, address, amount, masspayment, paymentCount, assetId)
def preconditionsAndSetContract(
senderBindingToContract: String => Gen[Contract],
invokerGen: Gen[PrivateKeyAccount] = accountGen,
masterGen: Gen[PrivateKeyAccount] = accountGen,
- payment: Option[Payment] = None): Gen[(List[GenesisTransaction], SetScriptTransaction, ContractInvocationTransaction)] =
+ payment: Option[Payment] = None,
+ feeGen: Gen[Long] = ciFee(0)): Gen[(List[GenesisTransaction], SetScriptTransaction, ContractInvocationTransaction)] =
for {
master <- masterGen
invoker <- invokerGen
ts <- timestampGen
genesis: GenesisTransaction = GenesisTransaction.create(master, ENOUGH_AMT, ts).explicitGet()
genesis2: GenesisTransaction = GenesisTransaction.create(invoker, ENOUGH_AMT, ts).explicitGet()
- fee <- smallFeeGen
+ fee <- feeGen
arg <- genBoundedString(1, 32)
funcBinding <- validAliasStringGen
contract <- senderBindingToContract(funcBinding)
@@ -128,7 +156,7 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
property("invoking contract results contract's state") {
forAll(for {
- r <- preconditionsAndSetContract(dataContractGen)
+ r <- preconditionsAndSetContract(s => dataContractGen(s, false))
} yield (r._1, r._2, r._3)) {
case (genesis, setScript, ci) =>
assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) {
@@ -142,11 +170,22 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
}
}
+ property("can't more than 5kb of data") {
+ forAll(for {
+ r <- preconditionsAndSetContract(s => dataContractGen(s, true))
+ } yield (r._1, r._2, r._3)) {
+ case (genesis, setScript, ci) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) {
+ _ should produce("WriteSet size can't exceed")
+ }
+ }
+ }
+
property("invoking payment contract results in accounts state") {
forAll(for {
a <- accountGen
am <- smallFeeGen
- contractGen = (paymentContractGen(a, am) _)
+ contractGen = (paymentContractGen(a, am, false) _)
r <- preconditionsAndSetContract(contractGen)
} yield (a, am, r._1, r._2, r._3)) {
case (acc, amount, genesis, setScript, ci) =>
@@ -157,20 +196,37 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
}
}
+ property("can't make more than 10 payments") {
+ forAll(for {
+ a <- accountGen
+ am <- smallFeeGen
+ contractGen = (paymentContractGen(a, am, true) _)
+ r <- preconditionsAndSetContract(contractGen)
+ } yield (a, am, r._1, r._2, r._3)) {
+ case (acc, amount, genesis, setScript, ci) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) {
+ _ should produce("many ContractTransfers")
+ }
+ }
+ }
+
val chainId = AddressScheme.current.chainId
val enoughFee = CommonValidation.ScriptExtraFee + CommonValidation.FeeConstants(IssueTransactionV2.typeId) * CommonValidation.FeeUnit
- property("invoking contract recive payment") {
+ property("invoking contract receive payment") {
forAll(for {
a <- accountGen
am <- smallFeeGen
- contractGen = (paymentContractGen(a, am) _)
+ contractGen = (paymentContractGen(a, am, false) _)
invoker <- accountGen
ts <- timestampGen
asset = IssueTransactionV2
.selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetAllowed), enoughFee, ts)
.explicitGet()
- r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, Some(asset.id()))))
+ r <- preconditionsAndSetContract(contractGen,
+ invokerGen = Gen.oneOf(Seq(invoker)),
+ payment = Some(Payment(1, Some(asset.id()))),
+ feeGen = ciFee(1))
} yield (a, am, r._1, r._2, r._3, asset, invoker)) {
case (acc, amount, genesis, setScript, ci, asset, invoker) =>
assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) {
@@ -186,13 +242,16 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
forAll(for {
a <- accountGen
am <- smallFeeGen
- contractGen = (paymentContractGen(a, am) _)
+ contractGen = (paymentContractGen(a, am, false) _)
invoker <- accountGen
ts <- timestampGen
asset = IssueTransactionV2
.selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetBanned), enoughFee, ts)
.explicitGet()
- r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, Some(asset.id()))))
+ r <- preconditionsAndSetContract(contractGen,
+ invokerGen = Gen.oneOf(Seq(invoker)),
+ payment = Some(Payment(1, Some(asset.id()))),
+ feeGen = ciFee(1))
} yield (a, am, r._1, r._2, r._3, asset, invoker)) {
case (acc, amount, genesis, setScript, ci, asset, invoker) =>
assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi =>
@@ -211,8 +270,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
asset = IssueTransactionV2
.selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetAllowed), enoughFee, ts)
.explicitGet()
- contractGen = (paymentContractGen(a, am, Some(asset.id())) _)
- r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)))
+ contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _)
+ r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1))
} yield (a, am, r._1, r._2, r._3, asset, master)) {
case (acc, amount, genesis, setScript, ci, asset, master) =>
assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) {
@@ -233,8 +292,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
asset = IssueTransactionV2
.selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetBanned), enoughFee, ts)
.explicitGet()
- contractGen = (paymentContractGen(a, am, Some(asset.id())) _)
- r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)))
+ contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _)
+ r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1))
} yield (a, am, r._1, r._2, r._3, asset, master)) {
case (acc, amount, genesis, setScript, ci, asset, master) =>
assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi =>
@@ -253,8 +312,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
asset = IssueTransactionV2
.selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetAllowed), enoughFee, ts)
.explicitGet()
- contractGen = (paymentContractGen(a, -1, Some(asset.id())) _)
- r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)))
+ contractGen = (paymentContractGen(a, -1, false, Some(asset.id())) _)
+ r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1))
} yield (a, am, r._1, r._2, r._3, asset, master, ts)) {
case (acc, amount, genesis, setScript, ci, asset, master, ts) =>
val t = TransferTransactionV2.selfSigned(Some(asset.id()), master, acc, asset.quantity / 10, ts, None, enoughFee, Array[Byte]()).explicitGet()
@@ -271,13 +330,86 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks
ts <- timestampGen
arg <- genBoundedString(1, 32)
funcBinding <- validAliasStringGen
+ fee <- ciFee(1)
fc = Terms.FUNCTION_CALL(FunctionHeader.User(funcBinding), List(CONST_BYTESTR(ByteStr(arg))))
- ci = ContractInvocationTransaction.selfSigned(invoker, master, fc, Some(Payment(-1, None)), enoughFee, ts)
- } yield (ci)) { ci =>
- ci shouldBe 'left
- ci.left.get.isInstanceOf[ValidationError.NegativeAmount] should be(true)
+ ci = ContractInvocationTransaction.selfSigned(invoker, master, fc, Some(Payment(-1, None)), fee, ts)
+ } yield (ci)) { _ should produce("NegativeAmount") }
+ }
+
+ property("smart asset payment require extra fee") {
+ forAll(for {
+ a <- accountGen
+ quantity = 1000000
+ am <- Gen.choose[Long](1L, quantity)
+ master <- accountGen
+ ts <- timestampGen
+ asset = IssueTransactionV2
+ .selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetBanned), enoughFee, ts)
+ .explicitGet()
+ contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _)
+ r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(0))
+ } yield (a, am, r._1, r._2, r._3, asset, master)) {
+ case (acc, amount, genesis, setScript, ci, asset, master) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi =>
+ blockDiffEi should produce("does not exceed minimal value")
+ }
+ }
+ }
+ property("contract with payment of smart asset require extra fee") {
+ forAll(for {
+ a <- accountGen
+ am <- smallFeeGen
+ contractGen = (paymentContractGen(a, am, false) _)
+ invoker <- accountGen
+ ts <- timestampGen
+ asset = IssueTransactionV2
+ .selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetAllowed), enoughFee, ts)
+ .explicitGet()
+ r <- preconditionsAndSetContract(contractGen,
+ invokerGen = Gen.oneOf(Seq(invoker)),
+ payment = Some(Payment(1, Some(asset.id()))),
+ feeGen = ciFee(0))
+ } yield (a, am, r._1, r._2, r._3, asset, invoker)) {
+ case (acc, amount, genesis, setScript, ci, asset, invoker) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi =>
+ blockDiffEi should produce("does not exceed minimal value")
+ }
}
}
+ property("can't overflow payment + fee") {
+ forAll(for {
+ a <- accountGen
+ am <- smallFeeGen
+ contractGen = (paymentContractGen(a, am, false) _)
+ invoker <- accountGen
+ ts <- timestampGen
+ r <- preconditionsAndSetContract(contractGen,
+ invokerGen = Gen.oneOf(Seq(invoker)),
+ payment = Some(Payment(Long.MaxValue, None)),
+ feeGen = ciFee(1))
+ } yield (r._1, r._2, r._3)) {
+ case (genesis, setScript, ci) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) {
+ _ should produce("Attempt to transfer unavailable funds")
+ }
+ }
+ }
+
+ property("can't overflow sum of payment in contract") {
+ forAll(for {
+ a <- accountGen
+ am <- smallFeeGen
+ contractGen = (paymentContractGen(a, Long.MaxValue / 2 + 2, true, None, 4) _)
+ invoker <- accountGen
+ ts <- timestampGen
+ r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, None)), feeGen = ciFee(1))
+ } yield (r._1, r._2, r._3)) {
+ case (genesis, setScript, ci) =>
+ assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) {
+ _ should produce("Attempt to transfer unavailable funds")
+ }
+ }
+ }
}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala
index 620390d..e3054e9 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala
@@ -58,7 +58,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers
assertDiffAndState(Seq(genesis), blocks(0), fs) {
case (totalDiff, state) =>
assertBalanceInvariant(totalDiff)
- state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs(0).fee)
+ state.balance(sender) shouldBe (ENOUGH_AMT - txs(0).fee)
state.accountData(sender, item1.key) shouldBe Some(item1)
state.accountData(sender).data.get(item1.key) shouldBe Some(item1)
}
@@ -67,7 +67,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers
assertDiffAndState(Seq(genesis, blocks(0)), blocks(1), fs) {
case (totalDiff, state) =>
assertBalanceInvariant(totalDiff)
- state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs.take(2).map(_.fee).sum)
+ state.balance(sender) shouldBe (ENOUGH_AMT - txs.take(2).map(_.fee).sum)
state.accountData(sender, item1.key) shouldBe Some(item1)
state.accountData(sender).data.get(item1.key) shouldBe Some(item1)
state.accountData(sender, item2.key) shouldBe Some(item2)
@@ -78,7 +78,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers
assertDiffAndState(Seq(genesis, blocks(0), blocks(1)), blocks(2), fs) {
case (totalDiff, state) =>
assertBalanceInvariant(totalDiff)
- state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs.map(_.fee).sum)
+ state.balance(sender) shouldBe (ENOUGH_AMT - txs.map(_.fee).sum)
state.accountData(sender, item1.key) shouldBe Some(item3)
state.accountData(sender).data.get(item1.key) shouldBe Some(item3)
state.accountData(sender, item2.key) shouldBe Some(item2)
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala
index 821758e..b7c5ead 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala
@@ -1,27 +1,31 @@
package com.zbsnetwork.state.diffs
-import cats.{Order => _, _}
-import com.zbsnetwork.OrderOps._
-import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount}
+import cats.{Order ⇒ _, _}
+import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
import com.zbsnetwork.features.{BlockchainFeature, BlockchainFeatures}
import com.zbsnetwork.lagonaki.mocks.TestBlock
import com.zbsnetwork.settings.{Constants, FunctionalitySettings, TestFunctionalitySettings}
import com.zbsnetwork.state._
+import com.zbsnetwork.state.diffs.ExchangeTransactionDiff.getOrderFeePortfolio
import com.zbsnetwork.state.diffs.TransactionDiffer.TransactionValidationError
import com.zbsnetwork.transaction.ValidationError.AccountBalanceError
import com.zbsnetwork.transaction._
+import com.zbsnetwork.transaction.assets.exchange.OrderOps._
import com.zbsnetwork.transaction.assets.exchange.{Order, _}
import com.zbsnetwork.transaction.assets.{IssueTransaction, IssueTransactionV1, IssueTransactionV2}
import com.zbsnetwork.transaction.smart.SetScriptTransaction
import com.zbsnetwork.transaction.smart.script.ScriptCompiler
-import com.zbsnetwork.transaction.transfer.TransferTransaction
+import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer
+import com.zbsnetwork.transaction.transfer.{MassTransferTransaction, TransferTransaction}
import com.zbsnetwork.{NoShrink, TransactionGen, crypto}
import org.scalacheck.Gen
import org.scalatest.prop.PropertyChecks
import org.scalatest.{Inside, Matchers, PropSpec}
+import scala.util.Random
+
class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with Inside with NoShrink {
val MATCHER: PrivateKeyAccount = PrivateKeyAccount.fromSeed("matcher").explicitGet()
@@ -35,7 +39,44 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
)
)
- property("preserves zbs invariant, stores match info, rewards matcher") {
+ val fsWithOrderV3Feature: FunctionalitySettings = fs.copy(preActivatedFeatures = fs.preActivatedFeatures + (BlockchainFeatures.OrderV3.id -> 0))
+
+ val fsOV3MT =
+ fsWithOrderV3Feature.copy(preActivatedFeatures = fsWithOrderV3Feature.preActivatedFeatures + (BlockchainFeatures.MassTransfer.id -> 0))
+
+ property("Validation fails when OrderV3 feature is not activation yet") {
+
+ val preconditionsAndExchange
+ : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ maybeAsset1 <- Gen.option(issue1.id())
+ maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1)
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = maybeAsset2,
+ priceAssetId = maybeAsset1,
+ orderVersions = Set(3)
+ )
+ } yield (gen1, gen2, gen3, issue1, issue2, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, exchange) =>
+ assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fs) { blockDiffEi =>
+ blockDiffEi should produce("Order Version 3 has not been activated yet")
+ }
+ }
+ }
+
+ property("Preserves zbs invariant, stores match info, rewards matcher") {
val preconditionsAndExchange: Gen[(GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for {
buyer <- accountGen
@@ -52,7 +93,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
forAll(preconditionsAndExchange) {
case (gen1, gen2, issue1, issue2, exchange) =>
- assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1, issue2))), TestBlock.create(Seq(exchange)), fs) {
+ assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) {
case (blockDiff, state) =>
val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values)
totalPortfolioDiff.balance shouldBe 0
@@ -64,6 +105,309 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
}
}
+ property("Preserves assets invariant (matcher's fee in one of the assets of the pair or in Zbs), stores match info, rewards matcher") {
+
+ val preconditionsAndExchange
+ : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ maybeAsset1 <- Gen.option(issue1.id())
+ maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1)
+ buyMatcherFeeAssetId <- Gen.oneOf(maybeAsset1, maybeAsset2)
+ sellMatcherFeeAssetId <- Gen.oneOf(maybeAsset1, maybeAsset2)
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = maybeAsset2,
+ priceAssetId = maybeAsset1,
+ buyMatcherFeeAssetId = buyMatcherFeeAssetId,
+ sellMatcherFeeAssetId = sellMatcherFeeAssetId,
+ fixedMatcher = Some(matcher)
+ ) retryUntil transactionWithOrdersV3IsValid
+ } yield (gen1, gen2, gen3, issue1, issue2, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, exchange) =>
+ assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) {
+ case (blockDiff, state) =>
+ val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values)
+ totalPortfolioDiff.balance shouldBe 0
+ totalPortfolioDiff.effectiveBalance shouldBe 0
+ totalPortfolioDiff.assets.values.toSet shouldBe Set(0L)
+
+ val matcherPortfolio = Monoid.combineAll(blockDiff.portfolios.filterKeys(_.address == exchange.sender.address).values)
+
+ val restoredMatcherPortfolio =
+ Monoid.combineAll(
+ Seq(
+ ExchangeTransactionDiff.getOrderFeePortfolio(exchange.buyOrder, exchange.buyMatcherFee),
+ ExchangeTransactionDiff.getOrderFeePortfolio(exchange.sellOrder, exchange.sellMatcherFee),
+ ExchangeTransactionDiff.zbsPortfolio(-exchange.fee)
+ )
+ )
+
+ matcherPortfolio shouldBe restoredMatcherPortfolio
+ }
+ }
+ }
+
+ property("Validation fails when received amount of asset is less than fee in that asset (Orders V3 are used)") {
+ val preconditionsAndExchange
+ : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ buyerIssuedAsset = Some(issue1.id())
+ sellerIssuedAsset = Some(issue2.id())
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = sellerIssuedAsset, // buyer buys sellerIssuedAsset (received asset)
+ priceAssetId = buyerIssuedAsset, // buyer sells buyerIssuedAsset
+ buyMatcherFeeAssetId = sellerIssuedAsset, // buyer pays fee in sellerIssuedAsset (received asset)
+ sellMatcherFeeAssetId = buyerIssuedAsset,
+ fixedMatcher = Some(matcher),
+ orderVersions = Set(3)
+ ).retryUntil(ex => !transactionWithOrdersV3IsValid(ex)) // fee in sellerIssuedAsset (received asset) is greater than amount of received sellerIssuedAsset
+ } yield (gen1, gen2, gen3, issue1, issue2, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, exchange) =>
+ assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) {
+ blockDiffEi =>
+ blockDiffEi should produce("negative asset balance")
+ }
+ }
+ }
+
+ property("Preserves assets invariant (matcher's fee in separately issued asset), stores match info, rewards matcher (Orders V3 are used)") {
+
+ val preconditionsAndExchange: Gen[(GenesisTransaction,
+ GenesisTransaction,
+ GenesisTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ issue3: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue4: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ maybeAsset1 <- Gen.option(issue1.id())
+ maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1)
+ buyMatcherFeeAssetId = Some(issue3.id())
+ sellMatcherFeeAssetId = Some(issue4.id())
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = maybeAsset2,
+ priceAssetId = maybeAsset1,
+ buyMatcherFeeAssetId = buyMatcherFeeAssetId,
+ sellMatcherFeeAssetId = sellMatcherFeeAssetId,
+ fixedMatcher = Some(matcher),
+ orderVersions = Set(3)
+ )
+ } yield (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) =>
+ assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2, issue3, issue4))),
+ TestBlock.create(Seq(exchange)),
+ fsWithOrderV3Feature) {
+ case (blockDiff, state) =>
+ val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values)
+ totalPortfolioDiff.balance shouldBe 0
+ totalPortfolioDiff.effectiveBalance shouldBe 0
+ totalPortfolioDiff.assets.values.toSet shouldBe Set(0L)
+
+ val matcherPortfolio = Monoid.combineAll(blockDiff.portfolios.filterKeys(_.address == exchange.sender.address).values)
+
+ val restoredMatcherPortfolio =
+ Monoid.combineAll(
+ Seq(
+ ExchangeTransactionDiff.getOrderFeePortfolio(exchange.buyOrder, exchange.buyMatcherFee),
+ ExchangeTransactionDiff.getOrderFeePortfolio(exchange.sellOrder, exchange.sellMatcherFee),
+ ExchangeTransactionDiff.zbsPortfolio(-exchange.fee)
+ )
+ )
+
+ matcherPortfolio shouldBe restoredMatcherPortfolio
+ }
+ }
+ }
+
+ property("Validation fails in case of attempt to pay fee in unissued asset (Orders V3 are used)") {
+
+ val preconditionsAndExchange
+ : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ maybeAsset1 <- Gen.option(issue1.id())
+ maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1)
+ matcherFeeAssetId <- assetIdGen retryUntil (_.nonEmpty)
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = maybeAsset2,
+ priceAssetId = maybeAsset1,
+ buyMatcherFeeAssetId = matcherFeeAssetId,
+ sellMatcherFeeAssetId = matcherFeeAssetId,
+ fixedMatcher = Some(matcher),
+ orderVersions = Set(3)
+ )
+ } yield (gen1, gen2, gen3, issue1, issue2, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, exchange) =>
+ assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) {
+ blockDiffEi =>
+ blockDiffEi should produce("negative asset balance")
+ }
+ }
+ }
+
+ property("Validation fails when balance of asset issued separately (asset is not in the pair) is less than fee in that asset (Orders V3 are used)") {
+
+ val preconditionsAndExchange: Gen[(GenesisTransaction,
+ GenesisTransaction,
+ GenesisTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ IssueTransaction,
+ ExchangeTransaction)] = for {
+ buyer <- accountGen
+ seller <- accountGen
+ matcher <- accountGen
+ ts <- timestampGen
+ gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet()
+ gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet()
+ gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet()
+ issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty)
+ issue3: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT / 1000000, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issue4: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT / 1000000, seller).map(_._1).retryUntil(_.script.isEmpty)
+ buyerIssuedAsset = Some(issue1.id())
+ sellerIssuedAsset = Some(issue2.id())
+ buyMatcherFeeAssetId = Some(issue3.id())
+ sellMatcherFeeAssetId = Some(issue4.id())
+ exchange <- exchangeV2GeneratorP(
+ buyer = buyer,
+ seller = seller,
+ amountAssetId = sellerIssuedAsset,
+ priceAssetId = buyerIssuedAsset,
+ fixedMatcherFee = Some(ENOUGH_AMT / 10),
+ buyMatcherFeeAssetId = buyMatcherFeeAssetId,
+ sellMatcherFeeAssetId = sellMatcherFeeAssetId,
+ fixedMatcher = Some(matcher),
+ orderVersions = Set(3)
+ )
+ } yield (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange)
+
+ forAll(preconditionsAndExchange) {
+ case (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) =>
+ assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2, issue3, issue4))),
+ TestBlock.create(Seq(exchange)),
+ fsWithOrderV3Feature) { blockDiffEi =>
+ blockDiffEi should produce("negative asset balance")
+ }
+ }
+ }
+
+ property("Total matcher's fee (sum of matcher's fees in exchange transactions) is less than or equal to order's matcher fee") {
+
+ val preconditions =
+ oneBuyFewSellsPreconditions(
+ totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee - 1000L, bigBuyOrderMatcherFee), // sum of buyMatcherFee in ex trs <= specified in bigBuyOrder
+ sellersTotalAmount = identity
+ )
+
+ forAll(preconditions) {
+ case (genesises, issueTx1, issueTx2, massTransfer, exchanges, bigBuyOrder) =>
+ assertDiffAndState(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))),
+ TestBlock.create(exchanges),
+ fsOV3MT) {
+ case (blockDiff, _) =>
+ val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values)
+
+ totalPortfolioDiff.balance shouldBe 0
+ totalPortfolioDiff.effectiveBalance shouldBe 0
+ totalPortfolioDiff.assets.values.toSet shouldBe Set(0L)
+
+ val feeSumPaidByBuyer =
+ Monoid
+ .combineAll(
+ exchanges.map(ex => getOrderFeePortfolio(bigBuyOrder, ex.buyMatcherFee))
+ )
+ .assets(bigBuyOrder.matcherFeeAssetId.get)
+
+ (feeSumPaidByBuyer <= exchanges.head.buyOrder.matcherFee) shouldBe true
+ }
+ }
+ }
+
+ property("Validation fails when total matcher's fee (sum of matcher's fees in exchange transactions) is greater than order's matcher fee") {
+
+ val preconditions =
+ oneBuyFewSellsPreconditions(
+ totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee + 1, bigBuyOrderMatcherFee + 100000L), // sum of buyMatcherFee in ex trs > specified in bigBuyOrder
+ sellersTotalAmount = identity
+ )
+
+ forAll(preconditions) {
+ case (genesises, issueTx1, issueTx2, massTransfer, exchanges, _) =>
+ assertDiffEi(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))), TestBlock.create(exchanges), fsOV3MT) {
+ blockDiffEi =>
+ blockDiffEi should produce("Insufficient buy fee")
+ }
+ }
+ }
+
+ property("Validation fails when total sell amount overfills buy order amount") {
+
+ val preconditions =
+ oneBuyFewSellsPreconditions(
+ totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee - 10000L, bigBuyOrderMatcherFee), // correct total buyMatcherFee in ex trs
+ sellersTotalAmount = (bigBuyOrderAmount: Long) => bigBuyOrderAmount + 10000L // sell orders overfill buy order
+ )
+
+ forAll(preconditions) {
+ case (genesises, issueTx1, issueTx2, massTransfer, exchanges, _) =>
+ assertDiffEi(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))), TestBlock.create(exchanges), fsOV3MT) {
+ blockDiffEi =>
+ blockDiffEi should produce("Too much buy")
+ }
+ }
+ }
+
property("buy zbs without enough money for fee") {
val preconditions: Gen[(GenesisTransaction, GenesisTransaction, IssueTransactionV1, ExchangeTransaction)] = for {
buyer <- accountGen
@@ -83,7 +427,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
forAll(preconditions) {
case (gen1, gen2, issue1, exchange) =>
whenever(exchange.amount > 300000) {
- assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(exchange)), fs) {
+ assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) {
case (blockDiff, _) =>
val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values)
totalPortfolioDiff.balance shouldBe 0
@@ -136,7 +480,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fs) {
case (blockDiff, state) =>
blockDiff.portfolios(tx.sender).balance shouldBe tx.buyMatcherFee + tx.sellMatcherFee - tx.fee
- state.portfolio(tx.sender).balance shouldBe 0L
+ state.balance(tx.sender) shouldBe 0L
}
}
}
@@ -162,7 +506,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
val buy = Order.buy(buyer, matcher, assetPair, issue1.quantity + 1, price, Ts, Ts + 1, MatcherFee)
val sell = Order.sell(seller, matcher, assetPair, issue1.quantity + 1, price, Ts, Ts + 1, MatcherFee)
val tx = createExTx(buy, sell, price, matcher, Ts).explicitGet()
- assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fs) { totalDiffEi =>
+ assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fsWithOrderV3Feature) { totalDiffEi =>
inside(totalDiffEi) {
case Left(TransactionValidationError(AccountBalanceError(errs), _)) =>
errs should contain key seller.toAddress
@@ -461,7 +805,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
lazy val contract = s"""
|
|{-# STDLIB_VERSION 3 #-}
- |{-# SCRIPT_TYPE CONTRACT #-}
+ |{-# CONTENT_TYPE CONTRACT #-}
|
| @Verifier(tx)
| func verify() = {
@@ -556,4 +900,168 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc
exchange <- exchangeGeneratorP(buyer, seller, maybeAsset1, maybeAsset2)
} yield (gen1, gen2, issue1, issue2, exchange)
+ /**
+ * Checks whether generated ExchangeTransactionV2 is valid.
+ * In case of using orders of version 3 it is possible that matched amount of received asset is less than matcher's
+ * fee in that asset. It leads to negative asset balance error
+ */
+ def transactionWithOrdersV3IsValid(ex: ExchangeTransaction): Boolean = {
+ (ex.buyOrder, ex.sellOrder) match {
+ case (_: OrderV3, _: Order) | (_: Order, _: OrderV3) =>
+ val isBuyerReceiveAmountGreaterThanFee =
+ if (ex.buyOrder.assetPair.amountAsset == ex.buyOrder.matcherFeeAssetId) {
+ ex.buyOrder.getReceiveAmount(ex.amount, ex.price).right.get > ex.buyMatcherFee
+ } else true
+
+ val isSellerReceiveAmountGreaterThanFee =
+ if (ex.sellOrder.assetPair.amountAsset == ex.sellOrder.matcherFeeAssetId) {
+ ex.sellOrder.getReceiveAmount(ex.amount, ex.price).right.get > ex.sellMatcherFee
+ } else true
+
+ isBuyerReceiveAmountGreaterThanFee && isSellerReceiveAmountGreaterThanFee
+ case _ => true
+ }
+ }
+
+ /** Generates sequence of Longs with predefined sum and size */
+ def getSeqWithPredefinedSum(sum: Long, count: Int): Seq[Long] = {
+
+ val (rem, res) = (1 until count)
+ .foldLeft((sum, List.empty[Long])) {
+ case ((remainder, result), _) =>
+ val next = java.util.concurrent.ThreadLocalRandom.current.nextLong(1, remainder)
+ (remainder - next) -> (next :: result)
+ }
+
+ Random.shuffle(rem :: res)
+ }
+
+ /** Generates sequence of sell orders for one big buy order */
+ def sellOrdersForBigBuyOrderGenerator(matcher: PublicKeyAccount,
+ sellers: Seq[PrivateKeyAccount],
+ assetPair: AssetPair,
+ price: Long,
+ matcherFeeAssetId: Option[AssetId],
+ totalAmount: Long,
+ totalMatcherFee: Long): Gen[Seq[Order]] = {
+
+ val randomAmountsAndFees =
+ getSeqWithPredefinedSum(totalAmount, sellers.length) zip getSeqWithPredefinedSum(totalMatcherFee, sellers.length)
+
+ val sellers2AmountsAndFees = sellers zip randomAmountsAndFees
+
+ def timestampAndExpirationGenerator: Gen[(Long, Long)] = {
+ for {
+ timestamp <- timestampGen
+ expiration <- maxOrderTimeGen
+ } yield (timestamp, expiration)
+ }
+
+ for { timestampsAndExpiration <- Gen.listOfN(sellers.length, timestampAndExpirationGenerator) } yield {
+
+ (timestampsAndExpiration zip sellers2AmountsAndFees)
+ .map {
+ case ((timestamp, expiration), (seller, (amount, fee))) =>
+ OrderV3(
+ sender = seller,
+ matcher = matcher,
+ pair = assetPair,
+ orderType = OrderType.SELL,
+ amount = amount,
+ price = price,
+ timestamp = timestamp,
+ expiration = expiration,
+ matcherFee = fee,
+ matcherFeeAssetId = matcherFeeAssetId
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns preconditions for tests based on case when there is one big buy order and few small sell orders
+ *
+ * @param totalBuyMatcherFeeBoundaries function for manipulating of total matcher's fee paid by buyer in exchange transactions
+ * @param sellersTotalAmount function for manipulating of total sell orders amount
+ */
+ def oneBuyFewSellsPreconditions(totalBuyMatcherFeeBoundaries: Long => (Long, Long), sellersTotalAmount: Long => Long)
+ : Gen[(List[GenesisTransaction], IssueTransaction, IssueTransaction, MassTransferTransaction, Seq[ExchangeTransactionV2], Order)] = {
+ for {
+ matcher <- accountGen
+ sellOrdersCount <- Gen.choose(1, 5)
+ sellers <- Gen.listOfN(sellOrdersCount, accountGen)
+ (buyer, _, _, _, bigBuyOrderAmount, price, bigBuyOrderTimestamp, bigBuyOrderExpiration, bigBuyOrderMatcherFee) <- orderParamGen
+ genesisTimestamp <- timestampGen
+ issueTx1: IssueTransaction <- issueReissueBurnGeneratorP(Long.MaxValue - 1000L, buyer).map(_._1).retryUntil(_.script.isEmpty)
+ issueTx2: IssueTransaction <- issueReissueBurnGeneratorP(Long.MaxValue - 1000L, buyer).map(_._1).retryUntil(_.script.isEmpty)
+
+ pair = AssetPair(Some(issueTx2.id()), Some(issueTx1.id()))
+ (minTotalBuyMatcherFee, maxTotalBuyMatcherFee) = totalBuyMatcherFeeBoundaries(bigBuyOrderMatcherFee)
+
+ totalBuyMatcherFeeForExchangeTransactions <- Gen.choose(minTotalBuyMatcherFee, maxTotalBuyMatcherFee)
+
+ bigBuyOrder = Order(
+ sender = buyer,
+ matcher = matcher,
+ pair = pair,
+ orderType = OrderType.BUY,
+ amount = bigBuyOrderAmount,
+ price = price,
+ timestamp = bigBuyOrderTimestamp,
+ expiration = bigBuyOrderExpiration,
+ matcherFee = bigBuyOrderMatcherFee,
+ version = 3: Byte,
+ matcherFeeAssetId = Some(issueTx1.id())
+ )
+
+ sellOrders <- sellOrdersForBigBuyOrderGenerator(
+ matcher = matcher,
+ assetPair = pair,
+ price = price,
+ matcherFeeAssetId = Some(issueTx2.id()),
+ sellers = sellers,
+ totalAmount = sellersTotalAmount(bigBuyOrderAmount),
+ totalMatcherFee = bigBuyOrderMatcherFee
+ )
+ } yield {
+
+ val genesises = (matcher :: buyer :: sellers).map { recipient =>
+ GenesisTransaction.create(recipient, ENOUGH_AMT, genesisTimestamp).explicitGet()
+ }
+
+ val massTransfer =
+ MassTransferTransaction
+ .selfSigned(
+ assetId = Some(issueTx2.id()),
+ sender = buyer,
+ transfers = sellers.map(seller => ParsedTransfer(seller, issueTx2.quantity / sellOrdersCount)),
+ genesisTimestamp + 1000L,
+ feeAmount = 1000L,
+ Array.empty[Byte]
+ )
+ .explicitGet()
+
+ val buyMatcherFees = getSeqWithPredefinedSum(totalBuyMatcherFeeForExchangeTransactions, sellOrdersCount)
+
+ val exchanges = (sellOrders zip buyMatcherFees).map {
+ case (sellOrder, buyMatcherFee) =>
+ ExchangeTransactionV2
+ .create(
+ matcher = matcher,
+ buyOrder = bigBuyOrder,
+ sellOrder = sellOrder,
+ amount = sellOrder.amount,
+ price = bigBuyOrder.price,
+ buyMatcherFee = buyMatcherFee,
+ sellMatcherFee = sellOrder.matcherFee,
+ fee = (bigBuyOrder.matcherFee + sellOrder.matcherFee) / 2,
+ timestamp = Math.min(sellOrder.expiration, bigBuyOrder.expiration) - 10000
+ )
+ .explicitGet()
+ }
+
+ (genesises, issueTx1, issueTx2, massTransfer, exchanges, bigBuyOrder)
+ }
+ }
+
}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/package.scala b/src/test/scala/com/zbsnetwork/state/diffs/package.scala
index 2f95620..e53c94b 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/package.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/package.scala
@@ -13,7 +13,7 @@ import com.zbsnetwork.transaction.{Transaction, ValidationError}
import org.scalatest.Matchers
package object diffs extends WithState with Matchers {
- val ENOUGH_AMT: Long = Long.MaxValue / 3
+ val ENOUGH_AMT: Long = Long.MaxValue / 2
def assertDiffEi(preconditions: Seq[Block], block: Block, fs: FunctionalitySettings = TFS.Enabled)(
assertion: Either[ValidationError, Diff] => Unit): Unit = withStateAndHistory(fs) { state =>
@@ -29,7 +29,7 @@ package object diffs extends WithState with Matchers {
private def assertDiffAndState(preconditions: Seq[Block], block: Block, fs: FunctionalitySettings, withNg: Boolean)(
assertion: (Diff, Blockchain) => Unit): Unit = withStateAndHistory(fs) { state =>
- def differ(blockchain: Blockchain, prevBlock: Option[Block], b: Block) =
+ def differ(blockchain: Blockchain, prevBlock: Option[Block], b: Block): Either[ValidationError, (Diff, Long, MiningConstraint)] =
BlockDiffer.fromBlock(fs, blockchain, if (withNg) prevBlock else None, b, MiningConstraint.Unlimited)
preconditions.foldLeft[Option[Block]](None) { (prevBlock, curBlock) =>
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala
index 722cb2b..79d678c 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala
@@ -194,11 +194,9 @@ class CommonFunctionsTest extends PropSpec with PropertyChecks with Matchers wit
}
}
- property("shadowing of external variable") {
- //TODO: script can be simplified after NODE-837 fix
- try {
- runScript(
- s"""
+ property("shadowing of variable considered external") {
+ runScript(
+ s"""
|match {
| let aaa = 1
| tx
@@ -207,12 +205,7 @@ class CommonFunctionsTest extends PropSpec with PropertyChecks with Matchers wit
| case other => throw()
| }
|""".stripMargin
- )
-
- } catch {
- case ex: MatchError => Assertions.assert(ex.getMessage().contains("Compilation failed: Value 'tx' already defined in the scope"))
- case _: Throwable => Assertions.fail("Some unexpected error")
- }
+ ) should produce("already defined")
}
property("data constructors") {
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala
index e537f9d..1027a8c 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala
@@ -86,11 +86,21 @@ class ContextFunctionsTest extends PropSpec with PropertyChecks with Matchers wi
| let bin = extract(getBinary(d, "${bin.key}"))
| let str = extract(getString(d, "${str.key}"))
|
+ | let intV = getIntegerValue(d, "${int.key}")
+ | let boolV = getBooleanValue(d, "${bool.key}")
+ | let binV = getBinaryValue(d, "${bin.key}")
+ | let strV = getStringValue(d, "${str.key}")
+ |
| let okInt = int == ${int.value}
| let okBool = bool == ${bool.value}
| let okBin = bin == base58'${Base58.encode(bin.asInstanceOf[BinaryDataEntry].value.arr)}'
| let okStr = str == "${str.value}"
|
+ | let okIntV = int + 1 == ${int.value} + 1
+ | let okBoolV = bool || true == ${bool.value} || true
+ | let okBinV = bin == base58'${Base58.encode(bin.asInstanceOf[BinaryDataEntry].value.arr)}'
+ | let okStrV = str + "" == "${str.value}"
+ |
| let badInt = isDefined(getInteger(d, "${bool.key}"))
| let badBool = isDefined(getBoolean(d, "${bin.key}"))
| let badBin = isDefined(getBinary(d, "${str.key}"))
@@ -98,7 +108,7 @@ class ContextFunctionsTest extends PropSpec with PropertyChecks with Matchers wi
|
| let noSuchKey = isDefined(getInteger(d, "\u00a0"))
|
- | let positives = okInt && okBool && okBin && okStr
+ | let positives = okInt && okBool && okBin && okStr && okIntV && okBoolV && okBinV && okStrV
| let negatives = badInt || badBool || badBin || badStr || noSuchKey
| positives && ! negatives
| }
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala
index ab31961..1cfa2bd 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala
@@ -51,159 +51,157 @@ class SerContextFunctionsTest extends PropSpec with PropertyChecks with Matchers
val untypedScript = Parser.parseExpr(scriptWithAllFunctions(dtx, ttx)).get.value
val compiledScript = ExpressionCompiler(compilerContext(V1, isAssetScript = false), untypedScript).explicitGet()._1
- val bytes = Array[Byte](10, 0, 0, 0, 0, 3, 114, 110, 100, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0,
- 9, 116, 105, 109, 101, 115, 116, 97, 109, 112, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 7, 108, 111, 110, 103, 65,
- 108, 108, 3, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 104, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0,
- 0, 0, 7, -48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 105, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1,
- -12, 7, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7,
- 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 100, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -22, 7, 9, 0,
- 0, 0, 0, 0, 0, 2, 9, 0, 0, 101, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -26, 7, 10, 0, 0, 0,
- 0, 9, 115, 117, 109, 83, 116, 114, 105, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104,
- 97, 2, 0, 0, 0, 1, 45, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 10, 0, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86,
- 101, 99, 116, 111, 114, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36,
- 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 48, 5, 0,
- 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 10, 0, 0, 0, 0, 4, 98, 111, 100, 121, 8, 5, 0, 0, 0, 2, 100, 48, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121,
- 116, 101, 115, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, -53, 0, 0, 0, 2, 5, 0, 0, 0, 4, 98, 111, 100, 121, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66,
- -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3,
- 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115,
- 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 9, 0, 0, -53, 0, 0, 0, 2, 1, 0, 0, 0, 100, 12,
- 1, -43, 40, -86, -66, -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33,
- -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105,
- 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 1, 0, 0, 0, 100, 12, 1,
- -43, 40, -86, -66, -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17,
- 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99,
- 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5,
- 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111,
- 110, 6, 7, 10, 0, 0, 0, 0, 7, 101, 113, 85, 110, 105, 111, 110, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9,
+ val bytes = Array[Byte](4, 0, 0, 0, 3, 114, 110, 100, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9,
+ 116, 105, 109, 101, 115, 116, 97, 109, 112, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108,
+ 108, 3, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 104, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0,
+ 7, -48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 105, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, -12,
+ 7, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 9, 0,
+ 0, 0, 0, 0, 0, 2, 9, 0, 0, 100, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -22, 7, 9, 0, 0, 0,
+ 0, 0, 0, 2, 9, 0, 0, 101, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -26, 7, 4, 0, 0, 0, 9,
+ 115, 117, 109, 83, 116, 114, 105, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2,
+ 0, 0, 0, 1, 45, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 4, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86, 101, 99, 116,
+ 111, 114, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99,
+ 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 48, 5, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 4, 0, 0, 0, 4, 98, 111, 100, 121, 8, 5, 0, 0, 0, 2, 100, 48, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 9, 0, 0,
+ 0, 0, 0, 0, 2, 9, 0, 0, -53, 0, 0, 0, 2, 5, 0, 0, 0, 4, 98, 111, 100, 121, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92, -95, 0, -40, 124,
+ 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0,
+ 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115,
+ 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 9, 0, 0, -53, 0, 0, 0, 2, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92,
+ -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110,
+ 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3,
+ 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92, -95, 0,
+ -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0,
+ 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116,
+ 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104,
+ 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 7, 101, 113, 85,
+ 110, 105, 111, 110, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109,
+ 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0,
+ 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4,
+ 0, 0, 0, 2, 116, 48, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 48, 0, 0, 0, 9, 114, 101, 99,
+ 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128,
+ -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 7, 4, 0, 0, 0, 5, 98, 97, 115, 105, 99, 3, 3, 3,
+ 5, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108, 108, 5, 0, 0, 0, 9, 115, 117, 109, 83, 116, 114, 105, 110, 103, 7, 5, 0, 0, 0, 13, 115, 117, 109,
+ 66, 121, 116, 101, 86, 101, 99, 116, 111, 114, 7, 5, 0, 0, 0, 7, 101, 113, 85, 110, 105, 111, 110, 7, 4, 0, 0, 0, 6, 110, 101, 80, 114, 105,
+ 109, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2,
+ 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0,
+ 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 1, 0, 0, 0, 4, -123, -88, 90, -123, 7, 4, 0, 0, 0, 24,
+ 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101, 109, 101, 110, 116, 4, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116,
+ 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2,
+ 33, 61, 0, 0, 0, 2, 9, 0, 1, -111, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 49, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0,
+ 0, 9, 68, 97, 116, 97, 69, 110, 116, 114, 121, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 24,
+ 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 4, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116,
+ 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0,
+ 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0,
+ 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -23, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 7, 4, 0, 0, 0, 2, 110, 101, 3, 3, 5, 0, 0, 0, 6, 110, 101, 80, 114, 105, 109, 5, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116,
+ 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101, 109, 101, 110, 116, 7, 5, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100,
+ 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 7, 4, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103, 3, 9, 0, 0, 102, 0, 0, 0, 2, 0,
+ 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 9, 0, 0, 103, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 7, 4,
+ 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9,
0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105,
- 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84,
- 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 116, 48, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2,
- 8, 5, 0, 0, 0, 2, 116, 48, 0, 0, 0, 9, 114, 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0,
- 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100,
- -99, 7, 10, 0, 0, 0, 0, 5, 98, 97, 115, 105, 99, 3, 3, 3, 5, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108, 108, 5, 0, 0, 0, 9, 115, 117, 109, 83,
- 116, 114, 105, 110, 103, 7, 5, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86, 101, 99, 116, 111, 114, 7, 5, 0, 0, 0, 7, 101, 113, 85, 110,
- 105, 111, 110, 7, 10, 0, 0, 0, 0, 6, 110, 101, 80, 114, 105, 109, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0,
- 0, 0, 0, 0, 0, 3, -25, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0,
- 0, 5, 104, 97, 45, 104, 97, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116,
- 101, 115, 1, 0, 0, 0, 4, -123, -88, 90, -123, 7, 10, 0, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101,
- 116, 69, 108, 101, 109, 101, 110, 116, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5,
- 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0,
- 2, 100, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -111, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2,
- 100, 49, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 9, 68, 97, 116, 97, 69, 110, 116, 114, 121, 0, 0, 0, 2, 2, 0,
- 0, 0, 2, 104, 97, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101,
- 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 10, 0, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120,
- 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0,
- 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3,
- 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110,
- 115, 97, 99, 116, 105, 111, 110, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3,
- -23, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 2, 110, 101, 3, 3, 5, 0, 0, 0,
- 6, 110, 101, 80, 114, 105, 109, 5, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101,
- 109, 101, 110, 116, 7, 5, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103,
- 104, 116, 7, 10, 0, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103, 3, 9, 0, 0, 102, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0,
- 3, -25, 9, 0, 0, 103, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 7, 10, 0, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115,
- 116, 83, 105, 122, 101, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36,
- 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 50, 5, 0,
- 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -112, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 50, 0, 0, 0, 4,
- 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114,
- 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 10, 0, 0, 0, 0, 5, 117, 110, 97, 114, 121, 3, 9, 0, 0, 0,
- 0, 0, 0, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, 9, 0, 0, 0, 0, 0, 0, 2, 7, 9, 1, 0, 0, 0, 1, 33, 0, 0, 0, 1,
- 6, 7, 10, 0, 0, 0, 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 107, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0,
- 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 10, 0, 0,
- 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0,
- 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 51, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99,
- 104, 48, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121,
- 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -55, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0,
- 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9,
- 0, 0, -54, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
- 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 116, 97, 107, 101, 82, 105, 103, 104, 116, 66, 121, 116, 101, 115, 0, 0,
- 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9,
- 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 100, 114, 111, 112, 82, 105, 103, 104, 116, 66, 121, 116, 101, 115, 0, 0, 0, 2, 8, 5, 0,
- 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 3, 9, 0, 0, 1, 0,
- 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116,
- 105, 111, 110, 10, 0, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 105, 115, 68,
- 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 10, 102, 101, 101, 65, 115, 115, 101, 116, 73, 100, 7, 7, 10, 0,
- 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 49, 0, 0, 0, 1, 2, 0, 0, 0, 4, 104, 97, 104,
- 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 47, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0,
- 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 48, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 116, 97, 107, 101, 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0,
- 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 100, 114, 111, 112,
- 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 4, 112, 117,
- 114, 101, 3, 3, 3, 3, 3, 3, 3, 5, 0, 0, 0, 5, 98, 97, 115, 105, 99, 5, 0, 0, 0, 2, 110, 101, 7, 5, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103,
- 7, 5, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 7, 5, 0, 0, 0, 5, 117, 110, 97, 114, 121, 7, 5, 0, 0, 0, 8, 102, 114,
- 65, 99, 116, 105, 111, 110, 7, 5, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 7, 5, 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 7, 10, 0, 0,
- 0, 0, 6, 116, 120, 66, 121, 73, 100, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0,
- 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0,
- 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116,
- 105, 111, 110, 10, 0, 0, 0, 0, 1, 103, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -24, 0, 0, 0, 1, 1, 0, 0, 0, 32,
- -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11,
- 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 103, 0, 0, 0, 2, 105, 100, 1, 0, 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17,
- 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11, 7, 10, 0, 0, 0, 0, 7, 101, 110, 116, 114, 105, 101, 115,
- 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104,
- 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 100, 5, 0, 0, 0, 7, 36, 109, 97, 116,
- 99, 104, 48, 10, 0, 0, 0, 0, 3, 105, 110, 116, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 16, 0, 0, 0, 2, 8, 5, 0,
- 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 105, 110, 116, 10, 0, 0, 0, 0, 4, 98, 111, 111, 108, 9, 1, 0, 0, 0, 7, 101, 120, 116,
- 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 17, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 111, 111, 108,
- 10, 0, 0, 0, 0, 4, 98, 108, 111, 98, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 18, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1,
- 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 108, 111, 98, 10, 0, 0, 0, 0, 3, 115, 116, 114, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97,
- 99, 116, 0, 0, 0, 1, 9, 0, 4, 19, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 115, 116, 114, 10, 0, 0, 0, 0,
- 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -92, 0, 0, 0, 1, 5, 0, 0, 0, 3, 105, 110, 116, 2, 0, 0, 0,
- 2, 50, 52, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -91, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 111, 111, 108, 2, 0, 0, 0, 4, 116, 114, 117, 101, 6, 9, 0, 0,
- 102, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 108, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 9, 0, 0, 0, 0, 0, 0, 2, 5, 0, 0, 0, 3,
- 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 10, 0, 0, 0, 0, 2, 100, 48, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9,
- 1, 0, 0, 0, 10, 103, 101, 116, 73, 110, 116, 101, 103, 101, 114, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 10, 0, 0, 0, 0, 2, 100, 49, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0, 10, 103, 101, 116, 66,
- 111, 111, 108, 101, 97, 110, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 0, 0, 0, 0, 2, 100,
- 50, 9, 1, 0, 0, 0, 9, 103, 101, 116, 66, 105, 110, 97, 114, 121, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0,
- 0, 0, 0, 2, 10, 0, 0, 0, 0, 2, 100, 51, 9, 1, 0, 0, 0, 9, 103, 101, 116, 83, 116, 114, 105, 110, 103, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0,
- 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 3, 10, 0, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101, 120, 3, 3, 3, 9, 0, 0, 0,
- 0, 0, 0, 2, 9, 0, 1, -102, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 48, 1, 0, 0, 0, 4, 105, -73, 29, 121, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -100, 0, 0,
- 0, 1, 5, 0, 0, 0, 2, 100, 49, 1, 0, 0, 0, 4, -126, 24, -93, -110, 6, 9, 1, 0, 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1,
- 5, 0, 0, 0, 2, 100, 50, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -101, 0, 0, 0, 1, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 5,
- 0, 0, 0, 2, 100, 51, 1, 0, 0, 0, 4, -102, 122, 41, -86, 3, 5, 0, 0, 0, 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 5, 0, 0, 0, 11, 100, 97, 116,
- 97, 66, 121, 73, 110, 100, 101, 120, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97,
- 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 3, 97, 100, 100, 9, 1, 0, 0, 0, 7, 65, 100, 100,
- 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23,
- -25, 122, -18, -70, -100, -99, 10, 0, 0, 0, 0, 4, 108, 111, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116,
- 0, 0, 0, 1, 9, 0, 4, 26, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 10, 0, 0, 0, 0, 5,
- 98, 111, 111, 108, 49, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 27, 0, 0, 0, 2, 5, 0, 0,
- 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 111, 111, 108, 6, 10, 0, 0, 0, 0, 3, 98, 105, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120,
- 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 28, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 108, 111, 98, 1, 0, 0, 0, 5, 97,
- 108, 105, 99, 101, 10, 0, 0, 0, 0, 4, 115, 116, 114, 49, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1,
- 9, 0, 4, 29, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 3, 3, 3, 5, 0, 0, 0, 4,
- 108, 111, 110, 103, 5, 0, 0, 0, 5, 98, 111, 111, 108, 49, 7, 5, 0, 0, 0, 3, 98, 105, 110, 7, 5, 0, 0, 0, 4, 115, 116, 114, 49, 7, 3, 9, 0, 0, 1,
- 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 22, 67, 114, 101, 97, 116, 101, 65, 108, 105, 97, 115, 84, 114, 97, 110,
- 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 97, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 2, 0, 0, 0, 1, 2, 0, 0, 0, 5,
- 111, 104, 32, 110, 111, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 66, 117, 114, 110, 84, 114, 97,
- 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 98, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 5, 116, 104, 114, 111,
- 119, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 20, 97, 100, 100, 114, 101, 115,
- 115, 70, 114, 111, 109, 80, 117, 98, 108, 105, 99, 75, 101, 121, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 15, 115, 101, 110, 100, 101,
- 114, 80, 117, 98, 108, 105, 99, 75, 101, 121, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 10, 0, 0, 0, 0, 15, 97, 70,
- 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0,
- 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111,
- 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 17, 97, 100, 100, 114, 101, 115, 115, 70, 114, 111, 109, 83, 116, 114, 105, 110, 103, 0, 0, 0, 1, 2,
- 0, 0, 0, 35, 51, 78, 53, 71, 82, 113, 122, 68, 66, 104, 106, 86, 88, 110, 67, 110, 52, 52, 98, 97, 72, 99, 122, 50, 71, 111, 90, 121, 53, 113,
- 76, 120, 116, 84, 104, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, -88, 98, -11, -83, -97, -100, 82,
- 58, 6, 114, -79, -62, -117, -100, -95, 112, -63, 95, 104, -126, 95, -112, -18, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116,
- 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 116,
- 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 4, 36, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 9, 114,
- 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115,
- -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 7, 10, 0, 0, 0, 0, 8, 98, 97, 108, 97,
- 110, 99, 101, 115, 3, 9, 0, 0, 102, 0, 0, 0, 2, 9, 0, 3, -21, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114,
- 5, 0, 0, 0, 4, 117, 110, 105, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 12, 119, 97, 118, 101, 115,
- 66, 97, 108, 97, 110, 99, 101, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7,
- 10, 0, 0, 0, 0, 5, 119, 97, 118, 101, 115, 3, 3, 3, 3, 3, 5, 0, 0, 0, 6, 116, 120, 66, 121, 73, 100, 5, 0, 0, 0, 7, 101, 110, 116, 114, 105,
- 101, 115, 7, 5, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 7, 5, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 7, 5, 0, 0, 0, 15, 97, 70, 114,
- 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 7, 9, 0, 0, 102, 0, 0, 0, 2, 5, 0, 0, 0, 6, 104, 101, 105, 103, 104, 116, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 3, 98, 107, 115, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -10, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0,
- 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -11, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0,
- 1, -9, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 3, 115, 105, 103, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -12, 0, 0,
- 0, 3, 1, 0, 0, 0, 2, 26, -66, 1, 0, 0, 0, 2, 0, 60, 1, 0, 0, 0, 2, 53, -72, 6, 10, 0, 0, 0, 0, 5, 115, 116, 114, 53, 56, 9, 0, 0, 0, 0, 0, 0, 2,
- 9, 0, 2, 89, 0, 0, 0, 1, 9, 0, 2, 88, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105,
- 100, 10, 0, 0, 0, 0, 5, 115, 116, 114, 54, 52, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 91, 0, 0, 0, 1, 9, 0, 2, 90, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116,
- 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 10, 0, 0, 0, 0, 6, 99, 114, 121, 112, 116, 111, 3, 3, 3, 5, 0, 0,
- 0, 3, 98, 107, 115, 5, 0, 0, 0, 3, 115, 105, 103, 7, 5, 0, 0, 0, 5, 115, 116, 114, 53, 56, 7, 5, 0, 0, 0, 5, 115, 116, 114, 54, 52, 7, 3, 5, 0,
- 0, 0, 3, 114, 110, 100, 3, 5, 0, 0, 0, 4, 112, 117, 114, 101, 5, 0, 0, 0, 5, 119, 97, 118, 101, 115, 7, 5, 0, 0, 0, 6, 99, 114, 121, 112, 116,
- 111)
+ 111, 110, 4, 0, 0, 0, 2, 100, 50, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -112, 0, 0, 0, 1,
+ 8, 5, 0, 0, 0, 2, 100, 50, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116,
+ 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 5, 117,
+ 110, 97, 114, 121, 3, 9, 0, 0, 0, 0, 0, 0, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, 9, 0, 0, 0, 0, 0, 0, 2, 7,
+ 9, 1, 0, 0, 0, 1, 33, 0, 0, 0, 1, 6, 7, 4, 0, 0, 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 107, 0, 0, 0, 3,
+ 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 9, 4, 0, 0, 0, 8, 98, 121, 116, 101,
+ 115, 79, 112, 115, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 51, 5, 0, 0, 0, 7, 36,
+ 109, 97, 116, 99, 104, 48, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9,
+ 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -55, 0, 0, 0, 2, 8, 5, 0,
+ 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2,
+ 33, 61, 0, 0, 0, 2, 9, 0, 0, -54, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 116, 97, 107, 101, 82, 105, 103, 104, 116, 66,
+ 121, 116, 101, 115, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
+ 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 100, 114, 111, 112, 82, 105, 103, 104, 116, 66, 121, 116, 101,
+ 115, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9,
+ 49, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114,
+ 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0,
+ 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 10, 102, 101, 101, 65, 115, 115, 101, 116,
+ 73, 100, 7, 7, 4, 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 49, 0, 0, 0, 1, 2, 0, 0,
+ 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 47, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104,
+ 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 48, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 116, 97, 107, 101, 82, 105, 103, 104, 116,
+ 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0,
+ 9, 100, 114, 111, 112, 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 4, 0,
+ 0, 0, 4, 112, 117, 114, 101, 3, 3, 3, 3, 3, 3, 3, 5, 0, 0, 0, 5, 98, 97, 115, 105, 99, 5, 0, 0, 0, 2, 110, 101, 7, 5, 0, 0, 0, 7, 103, 116, 101,
+ 76, 111, 110, 103, 7, 5, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 7, 5, 0, 0, 0, 5, 117, 110, 97, 114, 121, 7, 5, 0, 0,
+ 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 7, 5, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 7, 5, 0, 0, 0, 6, 115, 116, 114, 79, 112,
+ 115, 7, 4, 0, 0, 0, 6, 116, 120, 66, 121, 73, 100, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0,
+ 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9,
+ 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115,
+ 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 103, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -24, 0, 0, 0, 1, 1, 0,
+ 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93,
+ -67, 20, 11, 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 103, 0, 0, 0, 2, 105, 100, 1, 0, 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34,
+ -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11, 7, 4, 0, 0, 0, 7, 101, 110, 116, 114, 105,
+ 101, 115, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99,
+ 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 100, 5, 0, 0, 0, 7, 36, 109, 97,
+ 116, 99, 104, 48, 4, 0, 0, 0, 3, 105, 110, 116, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 16, 0, 0, 0, 2, 8, 5, 0,
+ 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 105, 110, 116, 4, 0, 0, 0, 4, 98, 111, 111, 108, 9, 1, 0, 0, 0, 7, 101, 120, 116,
+ 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 17, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 111, 111, 108, 4,
+ 0, 0, 0, 4, 98, 108, 111, 98, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 18, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0,
+ 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 108, 111, 98, 4, 0, 0, 0, 3, 115, 116, 114, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0,
+ 0, 0, 1, 9, 0, 4, 19, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 115, 116, 114, 4, 0, 0, 0, 9, 100, 97,
+ 116, 97, 66, 121, 75, 101, 121, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -92, 0, 0, 0, 1, 5, 0, 0, 0, 3, 105, 110, 116, 2, 0, 0, 0, 2, 50, 52,
+ 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -91, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 111, 111, 108, 2, 0, 0, 0, 4, 116, 114, 117, 101, 6, 9, 0, 0, 102, 0, 0,
+ 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 108, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 9, 0, 0, 0, 0, 0, 0, 2, 5, 0, 0, 0, 3, 115, 116,
+ 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 4, 0, 0, 0, 2, 100, 48, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0,
+ 10, 103, 101, 116, 73, 110, 116, 101, 103, 101, 114, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 4, 0, 0, 0, 2, 100, 49, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0, 10, 103, 101, 116, 66, 111, 111, 108, 101,
+ 97, 110, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 2, 100, 50, 9, 1, 0, 0, 0, 9,
+ 103, 101, 116, 66, 105, 110, 97, 114, 121, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 0, 0,
+ 0, 2, 100, 51, 9, 1, 0, 0, 0, 9, 103, 101, 116, 83, 116, 114, 105, 110, 103, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0,
+ 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101, 120, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -102, 0, 0,
+ 0, 1, 5, 0, 0, 0, 2, 100, 48, 1, 0, 0, 0, 4, 105, -73, 29, 121, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -100, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 49, 1,
+ 0, 0, 0, 4, -126, 24, -93, -110, 6, 9, 1, 0, 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 50, 6, 9, 0,
+ 0, 0, 0, 0, 0, 2, 9, 0, 1, -101, 0, 0, 0, 1, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 51, 1, 0, 0, 0,
+ 4, -102, 122, 41, -86, 3, 5, 0, 0, 0, 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 5, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101,
+ 120, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114,
+ 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 3, 97, 100, 100, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0,
+ 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 4, 0, 0,
+ 0, 4, 108, 111, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 26, 0, 0, 0, 2, 5, 0,
+ 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 4, 0, 0, 0, 5, 98, 111, 111, 108, 49, 9, 0, 0, 0, 0, 0, 0, 2,
+ 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 27, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 111,
+ 111, 108, 6, 4, 0, 0, 0, 3, 98, 105, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 28, 0,
+ 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 108, 111, 98, 1, 0, 0, 0, 5, 97, 108, 105, 99, 101, 4, 0, 0, 0, 4, 115, 116, 114, 49,
+ 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 29, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2,
+ 0, 0, 0, 3, 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 3, 3, 3, 5, 0, 0, 0, 4, 108, 111, 110, 103, 5, 0, 0, 0, 5, 98, 111, 111, 108, 49,
+ 7, 5, 0, 0, 0, 3, 98, 105, 110, 7, 5, 0, 0, 0, 4, 115, 116, 114, 49, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48,
+ 2, 0, 0, 0, 22, 67, 114, 101, 97, 116, 101, 65, 108, 105, 97, 115, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 97, 5, 0,
+ 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 2, 0, 0, 0, 1, 2, 0, 0, 0, 5, 111, 104, 32, 110, 111, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7,
+ 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 66, 117, 114, 110, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 98, 5, 0, 0,
+ 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 5, 116, 104, 114, 111, 119, 0, 0, 0, 0, 7, 4, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 9,
+ 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 20, 97, 100, 100, 114, 101, 115, 115, 70, 114, 111, 109, 80, 117, 98, 108, 105, 99, 75, 101, 121, 0, 0, 0,
+ 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 15, 115, 101, 110, 100, 101, 114, 80, 117, 98, 108, 105, 99, 75, 101, 121, 8, 5, 0, 0, 0, 2, 116, 120,
+ 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 4, 0, 0, 0, 15, 97, 70, 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 4, 0, 0, 0, 7,
+ 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15,
+ 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 17, 97, 100, 100, 114, 101, 115, 115,
+ 70, 114, 111, 109, 83, 116, 114, 105, 110, 103, 0, 0, 0, 1, 2, 0, 0, 0, 35, 51, 78, 53, 71, 82, 113, 122, 68, 66, 104, 106, 86, 88, 110, 67,
+ 110, 52, 52, 98, 97, 72, 99, 122, 50, 71, 111, 90, 121, 53, 113, 76, 120, 116, 84, 104, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0,
+ 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, -88, 98, -11, -83, -97, -100, 82, 58, 6, 114, -79, -62, -117, -100, -95, 112, -63, 95, 104, -126, 95, -112, -18,
+ 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97,
+ 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 4, 36,
+ 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 9, 114, 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115,
+ 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18,
+ -70, -100, -99, 7, 4, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 3, 9, 0, 0, 102, 0, 0, 0, 2, 9, 0, 3, -21, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2,
+ 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 5, 0, 0, 0, 4, 117, 110, 105, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0,
+ 0, 0, 2, 9, 1, 0, 0, 0, 12, 119, 97, 118, 101, 115, 66, 97, 108, 97, 110, 99, 101, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101,
+ 110, 100, 101, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, 0, 0, 0, 5, 119, 97, 118, 101, 115, 3, 3, 3, 3, 3, 5, 0, 0, 0, 6, 116, 120, 66, 121, 73,
+ 100, 5, 0, 0, 0, 7, 101, 110, 116, 114, 105, 101, 115, 7, 5, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 7, 5, 0, 0, 0, 7, 97, 70, 114, 111,
+ 109, 80, 75, 7, 5, 0, 0, 0, 15, 97, 70, 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 7, 9, 0, 0, 102, 0, 0, 0, 2, 5, 0, 0, 0, 6,
+ 104, 101, 105, 103, 104, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, 0, 0, 0, 3, 98, 107, 115, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1,
+ -10, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -11, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7,
+ 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -9, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 4, 0, 0, 0, 3, 115, 105, 103, 9, 1, 0, 0, 0, 2,
+ 33, 61, 0, 0, 0, 2, 9, 0, 1, -12, 0, 0, 0, 3, 1, 0, 0, 0, 2, 26, -66, 1, 0, 0, 0, 2, 0, 60, 1, 0, 0, 0, 2, 53, -72, 6, 4, 0, 0, 0, 5, 115, 116,
+ 114, 53, 56, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 89, 0, 0, 0, 1, 9, 0, 2, 88, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5,
+ 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 4, 0, 0, 0, 5, 115, 116, 114, 54, 52, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 91, 0, 0, 0, 1, 9, 0, 2, 90,
+ 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 4, 0, 0, 0, 6, 99, 114, 121,
+ 112, 116, 111, 3, 3, 3, 5, 0, 0, 0, 3, 98, 107, 115, 5, 0, 0, 0, 3, 115, 105, 103, 7, 5, 0, 0, 0, 5, 115, 116, 114, 53, 56, 7, 5, 0, 0, 0, 5,
+ 115, 116, 114, 54, 52, 7, 3, 5, 0, 0, 0, 3, 114, 110, 100, 3, 5, 0, 0, 0, 4, 112, 117, 114, 101, 5, 0, 0, 0, 5, 119, 97, 118, 101, 115, 7, 5, 0,
+ 0, 0, 6, 99, 114, 121, 112, 116, 111)
Serde.serialize(compiledScript) shouldBe bytes
}
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala
index 245e18b..96c3245 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala
@@ -19,7 +19,7 @@ import com.zbsnetwork.transaction.smart.BlockchainContext.In
import com.zbsnetwork.transaction.smart.ZbsEnvironment
import com.zbsnetwork.transaction.{Proofs, ProvenTransaction, VersionedTransaction}
import com.zbsnetwork.utils.EmptyBlockchain
-import com.zbsnetwork.{NoShrink, TransactionGen}
+import com.zbsnetwork.{NoShrink, TransactionGen, crypto}
import fastparse.core.Parsed.Success
import monix.eval.Coeval
import org.scalacheck.Gen
@@ -43,7 +43,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
| let id = t.id == base58'${t.id().base58}'
| let fee = t.fee == ${t.assetFee._2}
| let timestamp = t.timestamp == ${t.timestamp}
- | let bodyBytes = t.bodyBytes == base64'${ByteStr(t.bodyBytes.apply()).base64}'
+ | let bodyBytes = blake2b256(t.bodyBytes) == base64'${ByteStr(crypto.fastHash(t.bodyBytes.apply().array)).base64}'
| let sender = t.sender == addressFromPublicKey(base58'${ByteStr(t.sender.publicKey).base58}')
| let senderPublicKey = t.senderPublicKey == base58'${ByteStr(t.sender.publicKey).base58}'
| let version = t.version == $version
@@ -247,8 +247,8 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
|match tx {
| case t : SetScriptTransaction =>
| ${provenPart(t)}
- | let script = if (${t.script.isDefined}) then extract(t.script) == base64'${t.script
- .map(_.bytes().base64)
+ | let script = if (${t.script.isDefined}) then blake2b256(extract(t.script)) == base64'${t.script
+ .map(s => ByteStr(crypto.fastHash(s.bytes().arr)).base64)
.getOrElse("")}' else isDefined(t.script) == false
| ${assertProvenPart("t")} && script
| case other => throw()
@@ -262,7 +262,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
}
property("SetAssetScriptTransaction binding") {
- forAll(setAssetScriptTransactionGen.sample.get._2) { t =>
+ forAll(setAssetScriptTransactionGen.map(_._2)) { t =>
val result = runScript(
s"""
|match tx {
@@ -390,6 +390,10 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
.getOrElse(ByteStr.empty)
.base58}'
| else isDefined(t.${oType}Order.assetPair.priceAsset) == false
+ | let ${oType}MatcherFeeAssetId = if (${ord.matcherFeeAssetId.isDefined}) then extract(t.${oType}Order.matcherFeeAssetId) == base58'${ord.matcherFeeAssetId
+ .getOrElse(ByteStr.empty)
+ .base58}'
+ | else isDefined(t.${oType}Order.matcherFeeAssetId) == false
""".stripMargin
val lets = List(
@@ -405,7 +409,8 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
"BodyBytes",
"AssetPairAmount",
"AssetPairPrice",
- "Proofs"
+ "Proofs",
+ "MatcherFeeAssetId"
).map(i => s"$oType$i")
.mkString(" && ")
@@ -451,7 +456,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
| let matcherFee = t.matcherFee == ${t.matcherFee}
| let bodyBytes = t.bodyBytes == base64'${ByteStr(t.bodyBytes.apply()).base64}'
| ${Range(0, 8).map(letProof(t.proofs, "t")).mkString("\n")}
- | let assetPairAmount = if (${t.assetPair.amountAsset.isDefined}) then extract(t.assetPair.amountAsset) == base58'${t.assetPair.amountAsset
+ | let assetPairAmount = if (${t.assetPair.amountAsset.isDefined}) then extract(t.assetPair.amountAsset) == base58'${t.assetPair.amountAsset
.getOrElse(ByteStr.empty)
.base58}'
| else isDefined(t.assetPair.amountAsset) == false
@@ -459,8 +464,12 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers
.getOrElse(ByteStr.empty)
.base58}'
| else isDefined(t.assetPair.priceAsset) == false
- | id && sender && senderPublicKey && matcherPublicKey && timestamp && price && amount && expiration && matcherFee && bodyBytes && ${assertProofs(
- "t")} && assetPairAmount && assetPairPrice
+ | let matcherFeeAssetId = if (${t.matcherFeeAssetId.isDefined}) then extract(t.matcherFeeAssetId) == base58'${t.matcherFeeAssetId
+ .getOrElse(ByteStr.empty)
+ .base58}'
+ | else isDefined(t.matcherFeeAssetId) == false
+ | id && sender && senderPublicKey && matcherPublicKey && timestamp && price && amount && expiration && matcherFee && bodyBytes && ${assertProofs(
+ "t")} && assetPairAmount && assetPairPrice && matcherFeeAssetId
| case other => throw()
| }
|""".stripMargin
diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala
index e497953..964229d 100644
--- a/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala
@@ -53,14 +53,11 @@ class ScriptedSponsorTest extends PropSpec with PropertyChecks with Matchers wit
val sponsor = setupTxs.flatten.collectFirst { case t: SponsorFeeTransaction => t.sender }.get
assertDiffAndState(setupBlocks :+ TestBlock.create(Nil), transferBlock, fs) { (diff, blck) =>
- val contractPortfolio = blck.portfolio(contract)
- val sponsorPortfolio = blck.portfolio(sponsor)
+ blck.balance(contract, Some(assetId)) shouldEqual ENOUGH_FEE * 2
+ blck.balance(contract) shouldEqual ENOUGH_AMT - contractSpent
- contractPortfolio.balanceOf(Some(assetId)) shouldEqual ENOUGH_FEE * 2
- contractPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - contractSpent
-
- sponsorPortfolio.balanceOf(Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2
- sponsorPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - sponsorSpent
+ blck.balance(sponsor, Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2
+ blck.balance(sponsor) shouldEqual ENOUGH_AMT - sponsorSpent
}
}
}
@@ -79,14 +76,11 @@ class ScriptedSponsorTest extends PropSpec with PropertyChecks with Matchers wit
val recipientSpent: Long = 1
assertDiffAndState(setupBlocks :+ TestBlock.create(Nil), transferBlock, fs) { (diff, blck) =>
- val contractPortfolio = blck.portfolio(contract)
- val recipientPortfolio = blck.portfolio(recipient)
-
- contractPortfolio.balanceOf(Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2
- contractPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - contractSpent
+ blck.balance(contract, Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2
+ blck.balance(contract) shouldEqual ENOUGH_AMT - contractSpent
- recipientPortfolio.balanceOf(Some(assetId)) shouldEqual ENOUGH_FEE * 2
- recipientPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - recipientSpent
+ blck.balance(recipient, Some(assetId)) shouldEqual ENOUGH_FEE * 2
+ blck.balance(recipient) shouldEqual ENOUGH_AMT - recipientSpent
}
}
}
diff --git a/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala b/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala
index fb5a687..dd60ce4 100644
--- a/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala
+++ b/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala
@@ -52,7 +52,7 @@ class StateReaderEffectiveBalancePropertyTest extends PropSpec with PropertyChec
forAll(setup) {
case (leaser, genesis, xfer1, lease1, xfer2, lease2) =>
assertDiffAndState(Seq(block(Seq(genesis)), block(Seq(xfer1, lease1))), block(Seq(xfer2, lease2)), fs) { (_, state) =>
- val portfolio = state.portfolio(lease1.sender)
+ val portfolio = state.zbsPortfolio(lease1.sender)
val expectedBalance = xfer1.amount + xfer2.amount - 2 * Fee
portfolio.balance shouldBe expectedBalance
GeneratingBalanceProvider.balance(state, fs, leaser, state.lastBlockId.get) shouldBe 0
diff --git a/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala
index 748c4fa..e88ccf3 100644
--- a/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala
@@ -1,13 +1,12 @@
package com.zbsnetwork.transaction
import com.zbsnetwork.TransactionGen
-import com.zbsnetwork.account.{AddressScheme, DefaultAddressScheme, PrivateKeyAccount}
+import com.zbsnetwork.account.{AddressScheme, DefaultAddressScheme, PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.api.http.{ContractInvocationRequest, SignedContractInvocationRequest}
import com.zbsnetwork.common.state.ByteStr
-import com.zbsnetwork.common.utils.Base64
-import com.zbsnetwork.lang.v1.FunctionHeader
+import com.zbsnetwork.common.utils.{Base64, _}
+import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader}
import com.zbsnetwork.lang.v1.compiler.Terms
-import com.zbsnetwork.state._
import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment
import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, Verifier}
import org.scalatest._
@@ -17,7 +16,7 @@ import play.api.libs.json.{JsObject, Json}
class ContractInvocationTransactionSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen {
property("ContractInvocationTransaction serialization roundtrip") {
- forAll(contractInvokationGen) { transaction: ContractInvocationTransaction =>
+ forAll(contractInvocationGen) { transaction: ContractInvocationTransaction =>
val bytes = transaction.bytes()
val deser = ContractInvocationTransaction.parseBytes(bytes).get
deser.sender shouldEqual transaction.sender
@@ -48,8 +47,7 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC
"call": {
"function" : "foo",
"args" : [
- { "key" : "",
- "type" : "binary",
+ { "type" : "binary",
"value" : "base64:YWxpY2U="
}
]
@@ -84,7 +82,7 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC
val req = SignedContractInvocationRequest(
senderPublicKey = "73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK",
fee = 1,
- call = ContractInvocationRequest.FunctionCallPart("bar", List(BinaryDataEntry("", ByteStr.decodeBase64("YWxpY2U=").get))),
+ call = ContractInvocationRequest.FunctionCallPart("bar", List(Terms.CONST_BYTESTR(ByteStr.decodeBase64("YWxpY2U=").get))),
payment = Some(Payment(1, None)),
contractAddress = "3Fb641A9hWy63K18KsBJwns64McmdEATgJd",
timestamp = 11,
@@ -94,4 +92,32 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC
AddressScheme.current = DefaultAddressScheme
}
+ property(s"can't have more than ${ContractLimits.MaxContractInvocationArgs} args") {
+ import com.zbsnetwork.common.state.diffs.ProduceError._
+ val pk = PublicKeyAccount.fromBase58String("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").explicitGet()
+ ContractInvocationTransaction.create(
+ pk,
+ pk.toAddress,
+ Terms.FUNCTION_CALL(FunctionHeader.User("foo"), Range(0, 23).map(_ => Terms.CONST_LONG(0)).toList),
+ None,
+ 1,
+ 1,
+ Proofs.empty
+ ) should produce("more than 22 arguments")
+ }
+
+ property("can't be more 5kb") {
+ val largeString = "abcde" * 1024
+ import com.zbsnetwork.common.state.diffs.ProduceError._
+ val pk = PublicKeyAccount.fromBase58String("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").explicitGet()
+ ContractInvocationTransaction.create(
+ pk,
+ pk.toAddress,
+ Terms.FUNCTION_CALL(FunctionHeader.User("foo"), List(Terms.CONST_STRING(largeString))),
+ None,
+ 1,
+ 1,
+ Proofs.empty
+ ) should produce("TooBigArray")
+ }
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala
index 9ab6e22..efddb53 100644
--- a/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala
@@ -1,10 +1,11 @@
package com.zbsnetwork.transaction
-import com.zbsnetwork.OrderOps._
import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.{Base58, EitherExt2}
import com.zbsnetwork.transaction.ValidationError.OrderValidationError
+import com.zbsnetwork.transaction.assets.exchange.AssetPair.extractAssetId
+import com.zbsnetwork.transaction.assets.exchange.OrderOps._
import com.zbsnetwork.transaction.assets.exchange.{Order, _}
import com.zbsnetwork.{NTPTime, TransactionGen}
import org.scalacheck.Gen
@@ -16,6 +17,28 @@ import scala.math.pow
class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen with NTPTime {
+ val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported
+ val transactionV2versions = for {
+ o1ver <- 1 to 3
+ o2ver <- 1 to 3
+ } yield (o1ver.toByte, o2ver.toByte, 2.toByte)
+
+ val versions = transactionV1versions +: transactionV2versions
+ val versionsGen: Gen[(Byte, Byte, Byte)] = Gen.oneOf(versions)
+
+ val preconditions: Gen[(PrivateKeyAccount, PrivateKeyAccount, PrivateKeyAccount, AssetPair, Option[AssetId], Option[AssetId], (Byte, Byte, Byte))] =
+ for {
+ sender1 <- accountGen
+ sender2 <- accountGen
+ matcher <- accountGen
+ pair <- assetPairGen
+ buyerAnotherAsset <- assetIdGen
+ sellerAnotherAsset <- assetIdGen
+ buyerMatcherFeeAssetId <- Gen.oneOf(pair.amountAsset, pair.priceAsset, buyerAnotherAsset)
+ sellerMatcherFeeAssetId <- Gen.oneOf(pair.amountAsset, pair.priceAsset, sellerAnotherAsset)
+ versions <- versionsGen
+ } yield (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions)
+
property("ExchangeTransaction transaction serialization roundtrip") {
forAll(exchangeTransactionGen) { om =>
val recovered = ExchangeTransaction.parse(om.bytes()).get
@@ -26,13 +49,9 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with
}
property("ExchangeTransaction balance changes") {
- val versionsGen: Gen[(Byte, Byte, Byte)] = Gen.oneOf((1: Byte, 1: Byte, 1: Byte),
- (1: Byte, 1: Byte, 2: Byte),
- (1: Byte, 2: Byte, 2: Byte),
- (2: Byte, 1: Byte, 2: Byte),
- (2: Byte, 2: Byte, 2: Byte))
- forAll(accountGen, accountGen, accountGen, assetPairGen, versionsGen) {
- case (sender1, sender2, matcher, pair, versions) =>
+
+ forAll(preconditions) {
+ case (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions) =>
val time = ntpTime.correctedTime()
val expirationTimestamp = time + Order.MaxLiveTime
val buyPrice = 60 * Order.PriceConstant
@@ -43,8 +62,8 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with
val mf2 = 2
val (o1ver, o2ver, tver) = versions
- val buy = Order.buy(sender1, matcher, pair, buyAmount, buyPrice, time, expirationTimestamp, mf1, o1ver)
- val sell = Order.sell(sender2, matcher, pair, sellAmount, sellPrice, time, expirationTimestamp, mf2, o2ver)
+ val buy = Order.buy(sender1, matcher, pair, buyAmount, buyPrice, time, expirationTimestamp, mf1, o1ver, buyerMatcherFeeAssetId)
+ val sell = Order.sell(sender2, matcher, pair, sellAmount, sellPrice, time, expirationTimestamp, mf2, o2ver, sellerMatcherFeeAssetId)
def create(matcher: PrivateKeyAccount = sender1,
buyOrder: Order = buy,
@@ -54,7 +73,7 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with
buyMatcherFee: Long = mf1,
sellMatcherFee: Long = 1,
fee: Long = 1,
- timestamp: Long = expirationTimestamp - Order.MaxLiveTime) = {
+ timestamp: Long = expirationTimestamp - Order.MaxLiveTime): Either[ValidationError, ExchangeTransaction] = {
if (tver == 1) {
ExchangeTransactionV1.create(
matcher = sender1,
@@ -159,31 +178,27 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with
}
property("Test transaction with small amount and expired order") {
- forAll(
- accountGen,
- accountGen,
- accountGen,
- assetPairGen,
- Gen.oneOf((1: Byte, 1: Byte, 1: Byte),
- (1: Byte, 1: Byte, 2: Byte),
- (1: Byte, 2: Byte, 2: Byte),
- (2: Byte, 1: Byte, 2: Byte),
- (2: Byte, 2: Byte, 2: Byte))
- ) { (sender1: PrivateKeyAccount, sender2: PrivateKeyAccount, matcher: PrivateKeyAccount, pair: AssetPair, versions) =>
- val time = ntpTime.correctedTime()
- val expirationTimestamp = time + Order.MaxLiveTime
- val buyPrice = 1 * Order.PriceConstant
- val sellPrice = (0.50 * Order.PriceConstant).toLong
- val mf = 300000L
- val (o1ver, o2ver, tver) = versions
-
- val sell = Order.sell(sender2, matcher, pair, 2, sellPrice, time, expirationTimestamp, mf, o1ver)
- val buy = Order.buy(sender1, matcher, pair, 1, buyPrice, time, expirationTimestamp, mf, o2ver)
-
- createExTx(buy, sell, sellPrice, matcher, tver) shouldBe an[Right[_, _]]
-
- val sell1 = Order.sell(sender1, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver)
- createExTx(buy, sell1, buyPrice, matcher, tver) shouldBe Left(OrderValidationError(sell1, "expiration should be > currentTime"))
+
+ forAll(preconditions) {
+ case (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions) =>
+ val time = ntpTime.correctedTime()
+ val expirationTimestamp = time + Order.MaxLiveTime
+ val buyPrice = 1 * Order.PriceConstant
+ val sellPrice = (0.50 * Order.PriceConstant).toLong
+ val mf = 300000L
+ val (o1ver, o2ver, tver) = versions
+
+ val sell = Order.sell(sender2, matcher, pair, 2, sellPrice, time, expirationTimestamp, mf, o1ver, sellerMatcherFeeAssetId)
+ val buy = Order.buy(sender1, matcher, pair, 1, buyPrice, time, expirationTimestamp, mf, o2ver, buyerMatcherFeeAssetId)
+
+ createExTx(buy, sell, sellPrice, matcher, tver) shouldBe an[Right[_, _]]
+
+ val sell1 =
+ if (o1ver == 3) {
+ Order.sell(sender2, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver, sellerMatcherFeeAssetId)
+ } else Order.sell(sender2, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver)
+
+ createExTx(buy, sell1, buyPrice, matcher, tver) shouldBe Left(OrderValidationError(sell1, "expiration should be > currentTime"))
}
}
@@ -372,4 +387,97 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with
js shouldEqual tx.json()
}
+ property("JSON format validation V2 OrderV3") {
+ val js = Json.parse("""{
+ "version": 2,
+ "type":7,
+ "id":"4f4ntXNge7QNLP2DhMPJJzgngygEboLQQdBmzLYGeRoT",
+ "sender":"3N22UCTvst8N1i1XDvGHzyqdgmZgwDKbp44",
+ "senderPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP",
+ "fee":1,
+ "timestamp":1526992336241,
+ "proofs":["5NxNhjMrrH5EWjSFnVnPbanpThic6fnNL48APVAkwq19y2FpQp4tNSqoAZgboC2ykUfqQs9suwBQj6wERmsWWNqa"],
+ "order1":{
+ "version": 3,
+ "id":"BRAgLHDZFEMP5zRXrAis4EcwuYj11ksD1vq3ZsmiUVSp",
+ "sender":"3MthkhReCHXeaPZcWXcT3fa6ey1XWptLtwj",
+ "senderPublicKey":"BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ",
+ "matcherPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP",
+ "assetPair":{"amountAsset":null,"priceAsset":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy"},
+ "orderType":"buy",
+ "price":6000000000,
+ "amount":2,
+ "timestamp":1526992336241,
+ "expiration":1529584336241,
+ "matcherFee":1,
+ "matcherFeeAssetId":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy",
+ "signature":"2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs",
+ "proofs":["2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs"]
+ },
+ "order2":{
+ "version": 1,
+ "id":"DS9HPBGRMJcquTb3sAGAJzi73jjMnFFSWWHfzzKK32Q7",
+ "sender":"3MswjKzUBKCD6i1w4vCosQSbC8XzzdBx1mG",
+ "senderPublicKey":"7E9Za8v8aT6EyU1sX91CVK7tWUeAetnNYDxzKZsyjyKV",
+ "matcherPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP",
+ "assetPair":{"amountAsset":null,"priceAsset":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy"},
+ "orderType":"sell",
+ "price":5000000000,
+ "amount":3,
+ "timestamp":1526992336241,
+ "expiration":1529584336241,
+ "matcherFee":2,
+ "signature":"2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq",
+ "proofs":["2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq"]
+ },
+ "price":5000000000,
+ "amount":2,
+ "buyMatcherFee":1,
+ "sellMatcherFee":1
+ }
+ """)
+
+ val buy = OrderV3(
+ PublicKeyAccount.fromBase58String("BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ").explicitGet(),
+ PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").explicitGet(),
+ AssetPair.createAssetPair("ZBS", "9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get,
+ OrderType.BUY,
+ 2,
+ 6000000000L,
+ 1526992336241L,
+ 1529584336241L,
+ 1,
+ extractAssetId("9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get,
+ Proofs(Seq(ByteStr.decodeBase58("2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs").get))
+ )
+
+ val sell = OrderV1(
+ PublicKeyAccount.fromBase58String("7E9Za8v8aT6EyU1sX91CVK7tWUeAetnNYDxzKZsyjyKV").explicitGet(),
+ PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").explicitGet(),
+ AssetPair.createAssetPair("ZBS", "9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get,
+ OrderType.SELL,
+ 3,
+ 5000000000L,
+ 1526992336241L,
+ 1529584336241L,
+ 2,
+ Base58.decode("2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq").get
+ )
+
+ val tx = ExchangeTransactionV2
+ .create(
+ buy,
+ sell,
+ 2,
+ 5000000000L,
+ 1,
+ 1,
+ 1,
+ 1526992336241L,
+ Proofs(Seq(ByteStr.decodeBase58("5NxNhjMrrH5EWjSFnVnPbanpThic6fnNL48APVAkwq19y2FpQp4tNSqoAZgboC2ykUfqQs9suwBQj6wERmsWWNqa").get))
+ )
+ .explicitGet()
+
+ js shouldEqual tx.json()
+ }
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala b/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala
index 39f9844..3cd83ca 100644
--- a/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala
@@ -31,7 +31,7 @@ class IssueTransactionV1Specification extends PropSpec with PropertyChecks with
"id": "9ekQuYn92natMnMq8KqeGK3Nn7cpKd3BvPEGgD6fFyyz",
"sender": "3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh",
"senderPublicKey": "FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z",
- "fee": 100000000,
+ "fee": 1000000,
"timestamp": 1526287561757,
"version": 1,
"signature": "28kE1uN1pX2bwhzr9UHw5UuB9meTFEDFgeunNgy6nZWpHX4pzkGYotu8DhQ88AdqUG6Yy5wcXgHseKPBUygSgRMJ",
@@ -53,7 +53,7 @@ class IssueTransactionV1Specification extends PropSpec with PropertyChecks with
10000000000L,
8,
true,
- 100000000,
+ 1000000,
1526287561757L,
ByteStr.decodeBase58("28kE1uN1pX2bwhzr9UHw5UuB9meTFEDFgeunNgy6nZWpHX4pzkGYotu8DhQ88AdqUG6Yy5wcXgHseKPBUygSgRMJ").get
)
diff --git a/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala
index 5757cde..32b4a67 100644
--- a/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala
@@ -12,36 +12,44 @@ import org.scalatest.prop.PropertyChecks
class OrderSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen with ValidationMatcher with NTPTime {
+ private def checkFieldsEquality(left: Order, right: Order): Assertion = {
+
+ def defaultChecks: Assertion = {
+ left.bytes() shouldEqual right.bytes()
+ left.idStr() shouldBe right.idStr()
+ left.senderPublicKey.publicKey shouldBe right.senderPublicKey.publicKey
+ left.matcherPublicKey shouldBe right.matcherPublicKey
+ left.assetPair shouldBe right.assetPair
+ left.orderType shouldBe right.orderType
+ left.price shouldBe right.price
+ left.amount shouldBe right.amount
+ left.timestamp shouldBe right.timestamp
+ left.expiration shouldBe right.expiration
+ left.matcherFee shouldBe right.matcherFee
+ left.signature shouldBe right.signature
+ }
+
+ (left, right) match {
+ case (l: OrderV3, r: OrderV3) => defaultChecks; l.matcherFeeAssetId shouldBe r.matcherFeeAssetId
+ case _ => defaultChecks
+ }
+ }
+
property("Order transaction serialization roundtrip") {
+
forAll(orderV1Gen) { order =>
val recovered = OrderV1.parseBytes(order.bytes()).get
- recovered.bytes() shouldEqual order.bytes()
- recovered.idStr() shouldBe order.idStr()
- recovered.senderPublicKey.publicKey shouldBe order.senderPublicKey.publicKey
- recovered.matcherPublicKey shouldBe order.matcherPublicKey
- recovered.assetPair shouldBe order.assetPair
- recovered.orderType shouldBe order.orderType
- recovered.price shouldBe order.price
- recovered.amount shouldBe order.amount
- recovered.timestamp shouldBe order.timestamp
- recovered.expiration shouldBe order.expiration
- recovered.matcherFee shouldBe order.matcherFee
- recovered.signature shouldBe order.signature
+ checkFieldsEquality(recovered, order)
}
+
forAll(orderV2Gen) { order =>
val recovered = OrderV2.parseBytes(order.bytes()).get
- recovered.bytes() shouldEqual order.bytes()
- recovered.idStr() shouldBe order.idStr()
- recovered.senderPublicKey.publicKey shouldBe order.senderPublicKey.publicKey
- recovered.matcherPublicKey shouldBe order.matcherPublicKey
- recovered.assetPair shouldBe order.assetPair
- recovered.orderType shouldBe order.orderType
- recovered.price shouldBe order.price
- recovered.amount shouldBe order.amount
- recovered.timestamp shouldBe order.timestamp
- recovered.expiration shouldBe order.expiration
- recovered.matcherFee shouldBe order.matcherFee
- recovered.signature shouldBe order.signature
+ checkFieldsEquality(recovered, order)
+ }
+
+ forAll(orderV3Gen) { order =>
+ val recovered = OrderV3.parseBytes(order.bytes()).get
+ checkFieldsEquality(recovered, order)
}
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala
index acf40f6..0bfb726 100644
--- a/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala
@@ -2,9 +2,12 @@ package com.zbsnetwork.transaction
import com.zbsnetwork.account.{AddressScheme, PublicKeyAccount}
import com.zbsnetwork.common.state.ByteStr
+import com.zbsnetwork.common.state.diffs.ProduceError._
import com.zbsnetwork.common.utils.EitherExt2
+import com.zbsnetwork.lang.StdLibVersion
+import com.zbsnetwork.lang.contract.Contract
import com.zbsnetwork.transaction.assets.SetAssetScriptTransaction
-import com.zbsnetwork.transaction.smart.script.Script
+import com.zbsnetwork.transaction.smart.script.{ContractScript, Script}
import org.scalacheck.Gen
import play.api.libs.json._
@@ -46,4 +49,19 @@ class SetAssetScriptTransactionSpecification extends GenericTransactionSpecifica
)
.explicitGet()))
def transactionName: String = "SetAssetScriptTransaction"
+
+ property("issuer can`t make SetAssetScript tx when Script is Contract") {
+ val accountA = PublicKeyAccount.fromBase58String("5k3gXC486CCFCwzUAgavH9JfPwmq9CbBZvTARnFujvgr").explicitGet()
+
+ SetAssetScriptTransaction
+ .create(
+ AddressScheme.current.chainId,
+ accountA,
+ ByteStr.decodeBase58("DUyJyszsWcmZG7q2Ctk1hisDeGBPB8dEzyU8Gs5V2j3n").get,
+ Some(ContractScript(StdLibVersion.V3, Contract(List.empty, List.empty, None)).explicitGet()),
+ 1222,
+ System.currentTimeMillis(),
+ Proofs.empty
+ ) should produce("not Contract")
+ }
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala
index 0beed8f..3206a2b 100644
--- a/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala
@@ -58,7 +58,7 @@ class SetScriptTransactionSpecification extends GenericTransactionSpecification[
def transactionName: String = "SetScriptTransaction"
property("SetScriptTransaction id doesn't depend on proof (spec)") {
- forAll(accountGen, proofsGen, proofsGen, scriptGen) {
+ forAll(accountGen, proofsGen, proofsGen, contractOrExpr) {
case (acc: PrivateKeyAccount, proofs1, proofs2, script) =>
val tx1 = SetScriptTransaction.create(acc, Some(script), 1, 1, proofs1).explicitGet()
val tx2 = SetScriptTransaction.create(acc, Some(script), 1, 1, proofs2).explicitGet()
diff --git a/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala
index 7622ea2..98481ab 100644
--- a/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala
@@ -47,7 +47,42 @@ class OrderJsonSpecification extends PropSpec with PropertyChecks with Matchers
o.timestamp shouldBe 0
o.expiration shouldBe 0
o.signature shouldBe Base58.decode("signature").get
+ }
+ val jsonOV3 = Json.parse(s"""
+ {
+ "version": 3,
+ "senderPublicKey": "$pubKeyStr",
+ "matcherPublicKey": "DZUxn4pC7QdYrRqacmaAJghatvnn1Kh1mkE2scZoLuGJ",
+ "assetPair": {
+ "amountAsset": "29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b",
+ "priceAsset": "GEtBMkg419zhDiYRXKwn2uPcabyXKqUqj4w3Gcs1dq44"
+ },
+ "orderType": "buy",
+ "amount": 0,
+ "matcherFee": 0,
+ "price": 0,
+ "timestamp": 0,
+ "expiration": 0,
+ "signature": "signature",
+ "matcherFeeAssetId": "29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b"
+ } """)
+
+ jsonOV3.validate[Order] match {
+ case JsError(e) =>
+ fail("Error: " + e.toString())
+ case JsSuccess(o, _) =>
+ o.senderPublicKey shouldBe PublicKeyAccount(pk.publicKey)
+ o.matcherPublicKey shouldBe PublicKeyAccount(Base58.decode("DZUxn4pC7QdYrRqacmaAJghatvnn1Kh1mkE2scZoLuGJ").get)
+ o.assetPair.amountAsset.get shouldBe ByteStr.decodeBase58("29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b").get
+ o.assetPair.priceAsset.get shouldBe ByteStr.decodeBase58("GEtBMkg419zhDiYRXKwn2uPcabyXKqUqj4w3Gcs1dq44").get
+ o.price shouldBe 0
+ o.amount shouldBe 0
+ o.matcherFee shouldBe 0
+ o.timestamp shouldBe 0
+ o.expiration shouldBe 0
+ o.signature shouldBe Base58.decode("signature").get
+ o.matcherFeeAssetId.get shouldBe ByteStr.decodeBase58("29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b").get
}
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala
index 7db9333..b639926 100644
--- a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala
@@ -33,7 +33,18 @@ class ScriptCompilerV1Test extends PropSpec with PropertyChecks with Matchers {
ScriptCompiler(script, isAssetScript = false) shouldBe Left("Can't parse language version")
}
- private val expectedExpr = BLOCK(
+ property("fails with right error position") {
+ val script =
+ """
+ | {-# STDLIB_VERSION 3 #-}
+ | {-# STDLIB_VERSION 3 #-}
+ | let a = 1000
+ | a > b
+ """.stripMargin
+ ScriptCompiler.compile(script) shouldBe Left("Compilation failed: A definition of 'b' is not found in 72-73")
+ }
+
+ private val expectedExpr = LET_BLOCK(
LET("x", CONST_LONG(10)),
FUNCTION_CALL(
PureContext.eq.header,
diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala
index f666f8f..f38b3bc 100644
--- a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala
@@ -1,33 +1,29 @@
package com.zbsnetwork.transaction.smart.script
-import com.zbsnetwork.common.utils.EitherExt2
-import com.zbsnetwork.crypto
-import com.zbsnetwork.lang.StdLibVersion.{V3, V1}
-import com.zbsnetwork.lang.contract.Contract
-import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction}
+import com.zbsnetwork.common.utils._
+import com.zbsnetwork.lang.StdLibVersion._
import com.zbsnetwork.lang.v1.Serde
-import com.zbsnetwork.lang.v1.compiler.Terms
-import com.zbsnetwork.lang.v1.compiler.Terms.TRUE
+import com.zbsnetwork.lang.v1.testing.TypedScriptGen
import com.zbsnetwork.state.diffs.produce
-import org.scalatest.{FreeSpec, Matchers}
+import com.zbsnetwork.{NoShrink, crypto}
+import org.scalatest.prop.PropertyChecks
+import org.scalatest.{Inside, Matchers, PropSpec}
-class ScriptReaderTest extends FreeSpec with Matchers {
+class ScriptReaderTest extends PropSpec with PropertyChecks with Matchers with TypedScriptGen with Inside with NoShrink {
val checksumLength = 4
- "should parse all bytes for V1" in {
- val body = Array(V1.toByte) ++ Serde.serialize(TRUE) ++ "foo".getBytes
- val allBytes = body ++ crypto.secureHash(body).take(checksumLength)
- ScriptReader.fromBytes(allBytes) should produce("bytes left")
+ property("should parse all bytes for V1") {
+ forAll(exprGen) { sc =>
+ val body = Array(V1.toByte) ++ Serde.serialize(sc) ++ "foo".getBytes
+ val allBytes = body ++ crypto.secureHash(body).take(checksumLength)
+ ScriptReader.fromBytes(allBytes) should produce("bytes left")
+ }
}
- "should parse all bytes for V3" in {
- val sc = ContractScript(
- V3,
- Contract(List.empty, List(CallableFunction(CallableAnnotation("sender"), Terms.FUNC("foo", List("a"), Terms.REF("a")))), None)
- ).explicitGet()
-
- val allBytes = sc.bytes().arr
- ScriptReader.fromBytes(allBytes) shouldBe Right(sc)
+ property("should parse all bytes for V3") {
+ forAll(contractGen) { sc =>
+ val allBytes = ContractScript.apply(V3, sc).explicitGet().bytes().arr
+ ScriptReader.fromBytes(allBytes).explicitGet().expr shouldBe sc
+ }
}
-
}
diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala
index 639c6cd..7e44513 100644
--- a/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala
+++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala
@@ -1,10 +1,12 @@
package com.zbsnetwork.transaction.smart.script
import cats.kernel.Monoid
+import com.zbsnetwork.common.state.ByteStr
import com.zbsnetwork.common.utils.EitherExt2
-import com.zbsnetwork.lang.v1.ScriptEstimator
-import com.zbsnetwork.lang.v1.evaluator.ctx.UserFunction
-import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext
+import com.zbsnetwork.lang.v1.FunctionHeader.User
+import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, ScriptEstimator}
+import com.zbsnetwork.lang.v1.compiler.Terms._
+import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs._
import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext}
import com.zbsnetwork.lang.v1.testing.TypedScriptGen
import com.zbsnetwork.lang.{Global, StdLibVersion}
@@ -17,37 +19,214 @@ import org.scalatest.{Matchers, PropSpec}
class UserFunctionComplexityTest extends PropSpec with PropertyChecks with Matchers with TypedScriptGen {
- private val version = StdLibVersion.V3
+ private def estimate(expr: EXPR, ctx: CTX, funcCosts: Map[FunctionHeader, Coeval[Long]]): Either[String, Long] = {
+ ScriptEstimator(ctx.evaluationContext.letDefs.keySet, funcCosts, expr)
+ }
+
+ private val ctxV1 = {
+ utils.functionCosts(StdLibVersion.V1)
+ Monoid
+ .combineAll(
+ Seq(
+ PureContext.build(StdLibVersion.V1),
+ CryptoContext.build(Global),
+ ZbsContext.build(
+ StdLibVersion.V1,
+ new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain),
+ isTokenContext = false
+ )
+ ))
+ }
+ private val funcCostsV1 = utils.functionCosts(StdLibVersion.V1)
+
+ property("estimate script for stdLib V1 with UserFunctions") {
+
+ def est: EXPR => Either[String, Long] = estimate(_, ctxV1, funcCostsV1)
+
+ val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2)))
+ est(exprNe).explicitGet() shouldBe 28
+
+ val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List())
+ est(exprThrow).explicitGet() shouldBe 2
+
+ val exprExtract = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.extract, List(REF("x")))
+ )
+ est(exprExtract).explicitGet() shouldBe 21
+
+ val exprIsDefined = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.isDefined, List(REF("x")))
+ )
+ est(exprIsDefined).explicitGet() shouldBe 43
- private val ctx = {
+ val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprDropRightBytes).explicitGet() shouldBe 21
+
+ val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprTakeRightBytes).explicitGet() shouldBe 21
+
+ val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprDropRightString).explicitGet() shouldBe 21
+
+ val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprTakeRightString).explicitGet() shouldBe 21
+
+ val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1)))
+ est(exprUMinus).explicitGet() shouldBe 10
+
+ val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE))
+ est(exprUNot).explicitGet() shouldBe 12
+
+ val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2))))
+ est(exprAddressFromPublicKey).explicitGet() shouldBe 83
+
+ val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address")))
+ est(exprAddressFromString).explicitGet() shouldBe 125
+
+ val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias")))
+ est(exprZbsBalance).explicitGet() shouldBe 110
+ }
+
+ private val ctxV2 = {
+ utils.functionCosts(StdLibVersion.V2)
+ Monoid
+ .combineAll(
+ Seq(
+ PureContext.build(StdLibVersion.V2),
+ CryptoContext.build(Global),
+ ZbsContext.build(
+ StdLibVersion.V2,
+ new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain),
+ isTokenContext = false
+ )
+ ))
+ }
+ private val funcCostsV2 = utils.functionCosts(StdLibVersion.V2)
+
+ property("estimate script for stdLib V2 with UserFunctions") {
+
+ def est: EXPR => Either[String, Long] = estimate(_, ctxV2, funcCostsV2)
+
+ val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2)))
+ est(exprNe).explicitGet() shouldBe 28
+
+ val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List())
+ est(exprThrow).explicitGet() shouldBe 2
+
+ val exprExtract = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.extract, List(REF("x")))
+ )
+ est(exprExtract).explicitGet() shouldBe 21
+
+ val exprIsDefined = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.isDefined, List(REF("x")))
+ )
+ est(exprIsDefined).explicitGet() shouldBe 43
+
+ val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprDropRightBytes).explicitGet() shouldBe 21
+
+ val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprTakeRightBytes).explicitGet() shouldBe 21
+
+ val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprDropRightString).explicitGet() shouldBe 21
+
+ val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprTakeRightString).explicitGet() shouldBe 21
+
+ val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1)))
+ est(exprUMinus).explicitGet() shouldBe 10
+
+ val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE))
+ est(exprUNot).explicitGet() shouldBe 12
+
+ val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2))))
+ est(exprAddressFromPublicKey).explicitGet() shouldBe 83
+
+ val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address")))
+ est(exprAddressFromString).explicitGet() shouldBe 125
+
+ val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias")))
+ est(exprZbsBalance).explicitGet() shouldBe 110
+ }
+
+ private val ctxV3 = {
utils.functionCosts(StdLibVersion.V3)
Monoid
.combineAll(
Seq(
- PureContext.build(version),
+ PureContext.build(StdLibVersion.V3),
CryptoContext.build(Global),
ZbsContext.build(
- version,
+ StdLibVersion.V3,
new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain),
isTokenContext = false
)
))
}
+ private val funcCostsV3 = utils.functionCosts(StdLibVersion.V3)
+
+ property("estimate script for stdLib V3 with UserFunctions") {
+
+ def est: EXPR => Either[String, Long] = estimate(_, ctxV3, funcCostsV3)
+
+ val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2)))
+ est(exprNe).explicitGet() shouldBe 3
+
+ val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List())
+ est(exprThrow).explicitGet() shouldBe 1
+
+ val exprExtract = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.extract, List(REF("x")))
+ )
+ est(exprExtract).explicitGet() shouldBe 21
+
+ val exprIsDefined = LET_BLOCK(
+ LET("x", CONST_LONG(2)),
+ FUNCTION_CALL(PureContext.isDefined, List(REF("x")))
+ )
+ est(exprIsDefined).explicitGet() shouldBe 9
+
+ val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprDropRightBytes).explicitGet() shouldBe 21
+
+ val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1)))
+ est(exprTakeRightBytes).explicitGet() shouldBe 21
+
+ val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprDropRightString).explicitGet() shouldBe 21
+
+ val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1)))
+ est(exprTakeRightString).explicitGet() shouldBe 21
+
+ val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1)))
+ est(exprUMinus).explicitGet() shouldBe 2
+
+ val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE))
+ est(exprUNot).explicitGet() shouldBe 2
+
+ val exprEnsure = FUNCTION_CALL(PureContext.ensure, List(TRUE))
+ est(exprEnsure).explicitGet() shouldBe 17
+
+ val exprDataByIndex = LET_BLOCK(
+ LET("arr", FUNCTION_CALL(PureContext.listConstructor, List(CONST_STRING("str_1"), REF("nil")))),
+ FUNCTION_CALL(User("getString"), List(REF("arr"), CONST_LONG(0)))
+ )
+ est(exprDataByIndex).explicitGet() shouldBe 43
+
+ val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2))))
+ est(exprAddressFromPublicKey).explicitGet() shouldBe 83
+
+ val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address")))
+ est(exprAddressFromString).explicitGet() shouldBe 125
- // If test fails than complexity of user function was changed and it could lead to fork.
- property("WARNING - NODE FORK - check if user functions complexity changed") {
- val funcCosts = utils.functionCosts(version)
-
- val userFuncs = ctx.functions.filter(_.isInstanceOf[UserFunction])
- userFuncs.foreach {
- case func: UserFunction =>
- import func.signature.args
- val complexity =
- Coeval.now(ScriptEstimator(ctx.evaluationContext.letDefs.keySet ++ args.map(_._1), funcCosts, func.ev).explicitGet() + args.size * 5).value
- if (complexity != func.cost) {
- fail(s"Complexity of ${func.name} should be ${func.cost}, actual: $complexity.")
- }
- case _ =>
- }
+ val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias")))
+ est(exprZbsBalance).explicitGet() shouldBe 110
}
}
diff --git a/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala b/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala
index 46f4325..d013560 100644
--- a/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala
@@ -1,5 +1,6 @@
package com.zbsnetwork.utils
+import com.zbsnetwork.lang.StdLibVersion
import com.zbsnetwork.lang.v1.compiler.Terms.{FUNCTION_CALL, TRUE}
import com.zbsnetwork.lang.v1.compiler.Types.BOOLEAN
import com.zbsnetwork.lang.v1.evaluator.ctx.{EvaluationContext, UserFunction}
@@ -16,7 +17,7 @@ class UtilsSpecification extends FreeSpec with Matchers {
letDefs = Map.empty,
functions = Seq(caller, callee).map(f => f.header -> f)(collection.breakOut)
)
- estimate(ctx).size shouldBe 2
+ estimate(StdLibVersion.V3, ctx).size shouldBe 2
}
}
}
diff --git a/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala b/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala
index 2fa8a6b..1b0e25a 100644
--- a/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala
+++ b/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala
@@ -19,13 +19,13 @@ import com.zbsnetwork.mining._
import com.zbsnetwork.settings._
import com.zbsnetwork.state._
import com.zbsnetwork.state.diffs._
-import com.zbsnetwork.transaction.Transaction
import com.zbsnetwork.transaction.ValidationError.SenderIsBlacklisted
import com.zbsnetwork.transaction.smart.SetScriptTransaction
import com.zbsnetwork.transaction.smart.script.Script
import com.zbsnetwork.transaction.smart.script.v1.ExprScript
import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer
import com.zbsnetwork.transaction.transfer._
+import com.zbsnetwork.transaction.{AssetId, Transaction}
import com.zbsnetwork.utils.Implicits.SubjectOps
import com.zbsnetwork.utils.Time
import monix.reactive.subjects.Subject
@@ -38,12 +38,12 @@ import org.scalatest.{FreeSpec, Matchers}
import scala.concurrent.duration._
private object UtxPoolSpecification {
- private val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address]
+ private val ignoreSpendableBalanceChanged = Subject.empty[(Address, Option[AssetId])]
final case class TempDB(fs: FunctionalitySettings) {
val path = Files.createTempDirectory("leveldb-test")
val db = openDB(path.toAbsolutePath.toString)
- val writer = new LevelDBWriter(db, ignorePortfolioChanged, fs, 100000, 2000, 120 * 60 * 1000)
+ val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, 100000, 2000, 120 * 60 * 1000)
Runtime.getRuntime.addShutdownHook(new Thread(() => {
db.close()
@@ -78,7 +78,7 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
)
val dbContext = TempDB(settings.blockchainSettings.functionalitySettings)
- val bcu = StorageFactory(settings, dbContext.db, new TestTime(), ignorePortfolioChanged)
+ val bcu = StorageFactory(settings, dbContext.db, new TestTime(), ignoreSpendableBalanceChanged)
bcu.processBlock(Block.genesis(genesisSettings).explicitGet()).explicitGet()
bcu
}
@@ -128,9 +128,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
new UtxPoolImpl(
time,
bcu,
- ignorePortfolioChanged,
+ ignoreSpendableBalanceChanged,
FunctionalitySettings.TESTNET,
- UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)
+ UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)
)
val amountPart = (senderBalance - fee) / 2 - fee
val txs = for (_ <- 1 to n) yield createZbsTransfer(sender, recipient, amountPart, fee, time.getTimestamp()).explicitGet()
@@ -145,9 +145,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
new UtxPoolImpl(
time,
bcu,
- ignorePortfolioChanged,
+ ignoreSpendableBalanceChanged,
FunctionalitySettings.TESTNET,
- UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)
+ UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)
)
(sender, bcu, utxPool)
}
@@ -159,8 +159,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
time = new TestTime()
txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time))
} yield {
- val settings = UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)
- val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings)
+ val settings = UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)
+ val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings)
txs.foreach(utxPool.putIfNew)
(sender, bcu, utxPool, time, settings)
}).label("withValidPayments")
@@ -172,8 +172,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time)) // @TODO: Random transactions
} yield {
val settings =
- UtxSettings(10, PoolDefaultMaxBytes, Set(sender.address), Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)
- val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings)
+ UtxSettings(10, PoolDefaultMaxBytes, Set(sender.address), Set.empty, allowTransactionsFromSmartAccounts = true)
+ val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings)
(sender, utxPool, txs)
}).label("withBlacklisted")
@@ -184,8 +184,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time)) // @TODO: Random transactions
} yield {
val settings =
- UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), Set(recipient.address), 5.minutes, allowTransactionsFromSmartAccounts = true)
- val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings)
+ UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), Set(recipient.address), allowTransactionsFromSmartAccounts = true)
+ val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings)
(sender, utxPool, txs)
}).label("withBlacklistedAndAllowedByRule")
@@ -199,20 +199,20 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
} yield {
val whitelist: Set[String] = if (allowRecipients) recipients.map(_.address).toSet else Set.empty
val settings =
- UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), whitelist, 5.minutes, allowTransactionsFromSmartAccounts = true)
- val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings)
+ UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), whitelist, allowTransactionsFromSmartAccounts = true)
+ val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings)
(sender, utxPool, txs)
}).label("massTransferWithBlacklisted")
- private def utxTest(utxSettings: UtxSettings =
- UtxSettings(20, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true),
- txCount: Int = 10)(f: (Seq[TransferTransactionV1], UtxPool, TestTime) => Unit): Unit =
+ private def utxTest(
+ utxSettings: UtxSettings = UtxSettings(20, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true),
+ txCount: Int = 10)(f: (Seq[TransferTransactionV1], UtxPool, TestTime) => Unit): Unit =
forAll(stateGen, chooseNum(2, txCount).label("txCount")) {
case ((sender, senderBalance, bcu), count) =>
val time = new TestTime()
forAll(listOfN(count, transfer(sender, senderBalance / 2, time))) { txs =>
- val utx = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, utxSettings)
+ val utx = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, utxSettings)
f(txs, utx, time)
}
}
@@ -229,9 +229,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
val utx = new UtxPoolImpl(
time,
bcu,
- ignorePortfolioChanged,
+ ignoreSpendableBalanceChanged,
FunctionalitySettings.TESTNET,
- UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)
+ UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)
)
(utx, time, tx1, tx2)
}
@@ -265,9 +265,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
val utx = new UtxPoolImpl(
new TestTime(),
bcu,
- ignorePortfolioChanged,
+ ignoreSpendableBalanceChanged,
smartAccountsFs,
- UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 1.day, allowTransactionsFromSmartAccounts = scEnabled)
+ UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = scEnabled)
)
(sender, senderBalance, utx, bcu.lastBlock.fold(0L)(_.timestamp))
@@ -283,13 +283,13 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
"UTX Pool" - {
"does not add new transactions when full" in utxTest(
- UtxSettings(1, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) =>
+ UtxSettings(1, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) =>
utx.putIfNew(txs.head) shouldBe 'right
all(txs.tail.map(t => utx.putIfNew(t))) should produce("pool size limit")
}
"does not add new transactions when full in bytes" in utxTest(
- UtxSettings(999999, 152, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) =>
+ UtxSettings(999999, 152, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) =>
utx.putIfNew(txs.head) shouldBe 'right
all(txs.tail.map(t => utx.putIfNew(t))) should produce("pool bytes size limit")
}
@@ -348,50 +348,73 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with
utx.all.size shouldBe 2
}
- "portfolio" - {
- "returns a count of assets from the state if there is no transaction" in forAll(emptyUtxPool) {
- case (sender, state, utxPool) =>
- val basePortfolio = state.portfolio(sender)
+ "pessimisticPortfolio" - {
+ "is not empty if there are transactions" in forAll(withValidPayments) {
+ case (sender, _, utxPool, _, _) =>
+ utxPool.size should be > 0
+ utxPool.pessimisticPortfolio(sender) should not be empty
+ }
+ "is empty if there is no transactions" in forAll(emptyUtxPool) {
+ case (sender, _, utxPool) =>
utxPool.size shouldBe 0
- val utxPortfolio = utxPool.portfolio(sender)
-
- basePortfolio shouldBe utxPortfolio
+ utxPool.pessimisticPortfolio(sender) shouldBe empty
}
- "taking into account unconfirmed transactions" in forAll(withValidPayments) {
- case (sender, state, utxPool, _, _) =>
- val basePortfolio = state.portfolio(sender)
-
- utxPool.size should be > 0
- val utxPortfolio = utxPool.portfolio(sender)
-
- utxPortfolio.balance should be <= basePortfolio.balance
- utxPortfolio.lease.out should be <= basePortfolio.lease.out
- // should not be changed
- utxPortfolio.lease.in shouldBe basePortfolio.lease.in
- utxPortfolio.assets.foreach {
- case (assetId, count) =>
- count should be <= basePortfolio.assets.getOrElse(assetId, count)
- }
+ "is empty if utx pool was cleaned" in forAll(withValidPayments) {
+ case (sender, _, utxPool, _, _) =>
+ utxPool.removeAll(utxPool.all)
+ utxPool.pessimisticPortfolio(sender) shouldBe empty
}
"is changed after transactions with these assets are removed" in forAll(withValidPayments) {
- case (sender, _, utxPool, time, settings) =>
- val utxPortfolioBefore = utxPool.portfolio(sender)
- val poolSizeBefore = utxPool.size
+ case (sender, _, utxPool, time, _) =>
+ val portfolioBefore = utxPool.pessimisticPortfolio(sender)
+ val poolSizeBefore = utxPool.size
time.advance(maxAge * 2)
utxPool.packUnconfirmed(limitByNumber(100))
poolSizeBefore should be > utxPool.size
- val utxPortfolioAfter = utxPool.portfolio(sender)
+ val portfolioAfter = utxPool.pessimisticPortfolio(sender)
- utxPortfolioAfter.balance should be >= utxPortfolioBefore.balance
- utxPortfolioAfter.lease.out should be >= utxPortfolioBefore.lease.out
- utxPortfolioAfter.assets.foreach {
- case (assetId, count) =>
- count should be >= utxPortfolioBefore.assets.getOrElse(assetId, count)
+ portfolioAfter should not be portfolioBefore
+ }
+ }
+
+ "spendableBalance" - {
+ "equal to state's portfolio if utx is empty" in forAll(emptyUtxPool) {
+ case (sender, state, utxPool) =>
+ val pessimisticAssetIds = {
+ val p = utxPool.pessimisticPortfolio(sender)
+ p.assetIds.filter(x => p.balanceOf(x) != 0)
+ }
+
+ pessimisticAssetIds shouldBe empty
+ }
+
+ "takes into account unconfirmed transactions" in forAll(withValidPayments) {
+ case (sender, state, utxPool, _, _) =>
+ val basePortfolio = state.portfolio(sender)
+ val baseAssetIds = basePortfolio.assetIds
+
+ val pessimisticAssetIds = {
+ val p = utxPool.pessimisticPortfolio(sender)
+ p.assetIds.filter(x => p.balanceOf(x) != 0)
+ }
+
+ val unchangedAssetIds = baseAssetIds -- pessimisticAssetIds
+ withClue("unchanged") {
+ unchangedAssetIds.foreach { assetId =>
+ basePortfolio.balanceOf(assetId) shouldBe basePortfolio.balanceOf(assetId)
+ }
+ }
+
+ val changedAssetIds = pessimisticAssetIds -- baseAssetIds
+ withClue("changed") {
+ changedAssetIds.foreach { assetId =>
+ basePortfolio.balanceOf(assetId) should not be basePortfolio.balanceOf(assetId)
+ }
}
}
}
diff --git a/zbs-mainnet.conf b/zbs-mainnet.conf
index 51d73df..879aed8 100644
--- a/zbs-mainnet.conf
+++ b/zbs-mainnet.conf
@@ -9,13 +9,14 @@ zbs {
# Port number
port = 7440
- known-peers = ["206.189.241.175:7440", "142.93.227.63:7440"]
+ known-peers = ["node1.0bsnetwork.com:7440", "node2.0bsnetwork.com:7440"]
# Node name to send during handshake. Give your Node a name, or comment this string out to set random node name.
- node-name = "ZBS Mainnet Node"
+ node-name = "0bsNetwork Full Node"
+
# String with IP address and port to send as external address during handshake. Could be set automatically if uPnP is enabled.
- #declared-address = "1.2.3.4:7440"
+ # declared-address = "1.2.3.4:7440"
}
# Wallet settings
@@ -33,7 +34,7 @@ zbs {
# Node's REST API settings. Enable if you want to use your node for testing your own software developed for the 0bsnetwork platform.
rest-api {
# Enable/disable node's REST API
- enable = no
+ enable = yes
# Network address to bind to. IF YOU WANT TO ENABLE ACCESS VIA THE INTERNET, DO IT USING A REVERSE PROXY (eg. Nginx) FOR SECURITY REASONS.
bind-address = "127.0.0.1"
@@ -42,14 +43,20 @@ zbs {
port = 7441
# Hash of API key string. CHANGE THIS IF YOU ENABLE NODE API. OTHERWISE YOUR NODE WILL BE OPEN TO EVERYONE.
- api-key-hash = "86GJVSoboK12zXHYJFzoucAKaFS1yyXA2NztWSt9tGiX"
+ api-key-hash = "86GJVSoboK12zXHYfdgfgS1yyXA2NztWSt9tGiX"
}
# Vote to activate features
features {
auto-shutdown-on-unsupported-feature = yes
- supported = [9]
+ supported = []
}
+
+ miner {
+ # Enable/disable block generation
+ enable = no
+
+ }
}
-include "local.conf"
\ No newline at end of file
+include "local.conf"
diff --git a/zbs-testnet.conf b/zbs-testnet.conf
index c31d9e3..21489b4 100644
--- a/zbs-testnet.conf
+++ b/zbs-testnet.conf
@@ -48,7 +48,7 @@ zbs {
# Vote to activate features
features {
auto-shutdown-on-unsupported-feature = yes
- supported = [9]
+ supported = [9,10,11]
}
}