diff --git a/src/main/scala/encry/api/http/DataHolderForApi.scala b/src/main/scala/encry/api/http/DataHolderForApi.scala index 72adcec939..3a046f5b1b 100644 --- a/src/main/scala/encry/api/http/DataHolderForApi.scala +++ b/src/main/scala/encry/api/http/DataHolderForApi.scala @@ -1,6 +1,7 @@ package encry.api.http import java.net.{InetAddress, InetSocketAddress} + import akka.actor.{Actor, ActorRef, Props, Stash} import akka.pattern._ import akka.util.Timeout @@ -26,11 +27,14 @@ import encry.view.NodeViewHolder.ReceivableMessages.{CreateAccountManagerFromSee import encry.view.history.History import encry.view.state.{UtxoState, UtxoStateReader} import encry.view.wallet.EncryWallet -import org.encryfoundation.common.crypto.PrivateKey25519 +import org.encryfoundation.common.crypto.{PrivateKey25519, PublicKey25519} import org.encryfoundation.common.modifiers.history.{Block, Header} import org.encryfoundation.common.modifiers.state.box.Box.Amount +import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId import org.encryfoundation.common.utils.Algos import org.encryfoundation.common.utils.TaggedTypes.ModifierId +import scorex.crypto.signatures.PublicKey + import scala.concurrent.Future class DataHolderForApi(settings: EncryAppSettings, ntp: NetworkTimeProvider) @@ -197,12 +201,8 @@ class DataHolderForApi(settings: EncryAppSettings, ntp: NetworkTimeProvider) }).pipeTo(sender) case GetViewGetBalance => - (nvhRef ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Map[String, List[(String, Amount)]]] { view => - val balance: Map[String, List[(String, Amount)]] = view.vault.getBalances.map { - case ((key, token), amount) => Map(key -> List((token, amount))) - }.foldLeft(Map.empty[String, List[(String, Amount)]]) { case (el1, el2) => el1 |+| el2 } - if (balance.isEmpty) Map.empty[String, List[(String, Amount)]] else balance - }).pipeTo(sender) + (nvhRef ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Map[(PublicKey, TokenId), Amount]] + (_.vault.getBalances.toMap)).pipeTo(sender) case GetViewPrintPrivKeys => (nvhRef ? GetDataFromCurrentView[History, UtxoState, EncryWallet, String] { view => diff --git a/src/main/scala/encry/api/http/routes/WalletInfoApiRoute.scala b/src/main/scala/encry/api/http/routes/WalletInfoApiRoute.scala index 17e4cc51d5..17e332fa39 100644 --- a/src/main/scala/encry/api/http/routes/WalletInfoApiRoute.scala +++ b/src/main/scala/encry/api/http/routes/WalletInfoApiRoute.scala @@ -1,7 +1,7 @@ package encry.api.http.routes -import akka.actor.{ActorRef, ActorRefFactory} -import akka.http.scaladsl.server.{Route, ValidationRejection} +import akka.actor.{ ActorRef, ActorRefFactory } +import akka.http.scaladsl.server.{ Route, ValidationRejection } import akka.pattern._ import cats.syntax.eq._ import cats.kernel.Eq @@ -18,22 +18,23 @@ import encry.view.state.UtxoState import encry.view.wallet.EncryWallet import io.circe.syntax._ import org.encryfoundation.common.crypto.PrivateKey25519 -import org.encryfoundation.common.modifiers.mempool.transaction.{PubKeyLockedContract, Transaction} -import org.encryfoundation.common.modifiers.state.box.{AssetBox, MonetaryBox, TokenIssuingBox} +import org.encryfoundation.common.modifiers.mempool.transaction.{ PubKeyLockedContract, Transaction } +import org.encryfoundation.common.modifiers.state.box.{ AssetBox, EncryBaseBox, MonetaryBox, TokenIssuingBox } import org.encryfoundation.common.utils.Algos import org.encryfoundation.common.utils.TaggedTypes.ADKey + import scala.concurrent.Future -import scala.util.{Failure, Success, Try} +import scala.util.{ Failure, Success, Try } -case class WalletInfoApiRoute(dataHolder: ActorRef, - settings: RESTApiSettings, - intrinsicTokenId: String)(implicit val context: ActorRefFactory) - extends EncryBaseApiRoute with FailFastCirceSupport with StrictLogging { +case class WalletInfoApiRoute(dataHolder: ActorRef, settings: RESTApiSettings, intrinsicTokenId: String)( + implicit val context: ActorRefFactory +) extends EncryBaseApiRoute + with FailFastCirceSupport + with StrictLogging { implicit val eqOptBytes = new Eq[Option[Array[Byte]]] { - override def eqv(x: Option[Array[Byte]], y: Option[Array[Byte]]) = { + override def eqv(x: Option[Array[Byte]], y: Option[Array[Byte]]) = x.exists(l => y.exists(_.sameElements(l))) - } } override val route: Route = pathPrefix("wallet") { @@ -42,13 +43,14 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, printAddressR ~ printPubKeysR ~ getBalanceR ~ - WebRoute.authRoute( + WebRoute.authRoute( createKeyR ~ - transferR ~ - transferContractR ~ - createTokenR ~ - dataTransactionR, settings - ) + transferR ~ + transferContractR ~ + createTokenR ~ + dataTransactionR, + settings + ) } private def getWallet: Future[EncryWallet] = @@ -56,11 +58,26 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, GetDataFromCurrentView[History, UtxoState, EncryWallet, EncryWallet](_.vault)) .mapTo[EncryWallet] + private def loopBoxes(input: List[EncryBaseBox], + acc: List[AssetBox], + totalAmount: Long, + requiredAmount: Long): List[AssetBox] = + input.headOption match { + case Some(value: AssetBox) => + val newAmount = totalAmount + value.amount + if (requiredAmount > newAmount) { + loopBoxes(input.drop(1), value :: acc, newAmount, requiredAmount) + } else value :: acc + case Some(_) => + loopBoxes(input.drop(1), acc, totalAmount, requiredAmount) + case None => acc + } + def infoR: Route = (path("info") & get) { getWallet.map { w => Map( - "balances" -> w.getBalances.map{ i => - if (i._1._2 != intrinsicTokenId) + "balances" -> w.getBalances.map { i => + if (Algos.encode(i._1._2) != intrinsicTokenId) s"TokenID(${i._1._2}) for contractHash ${i._1._1} : ${i._2}" else s"TokenID(${i._1._2}) for contractHash ${i._1._1} : ${BigDecimal(i._2) / 100000000}" @@ -72,26 +89,28 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, def printAddressR: Route = (path("addr") & get) { (dataHolder ? GetViewPrintAddress) - .mapTo[String].map(_.asJson).okJson() + .mapTo[String] + .map(_.asJson) + .okJson() } def createTokenR: Route = (path("createToken") & get) { - parameters('fee.as[Int], 'amount.as[Long]) { (fee, amount) => - (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { - wallet => - Try { - val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount - val boxes = wallet.vault.walletStorage - .getAllBoxes().collectFirst { case ab: AssetBox => ab }.toList.take(1) - TransactionFactory.assetIssuingTransactionScratch( - secret, - fee, - System.currentTimeMillis(), - boxes.map(_ -> None), - PubKeyLockedContract(wallet.vault.accountManagers.head.mandatoryAccount.publicImage.pubKeyBytes).contract, - amount) - }.toOption - }).flatMap { + parameters('fee.as[Long], 'amount.as[Long]) { (fee, amount) => + (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { wallet => + Try { + val actualFee: Long = fee * 100000000 + val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount + val boxes = loopBoxes(wallet.vault.walletStorage.getAllBoxes().toList, List.empty[AssetBox], 0L, actualFee) + TransactionFactory.assetIssuingTransactionScratch( + secret, + actualFee, + System.currentTimeMillis(), + boxes.map(_ -> None), + PubKeyLockedContract(wallet.vault.accountManagers.head.mandatoryAccount.publicImage.pubKeyBytes).contract, + amount + ) + }.toOption + }).flatMap { case Some(tx: Transaction) => EncryApp.system.eventStream.publish(NewTransaction(tx)) Future.unit @@ -99,25 +118,25 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, } complete("Token was created") } - } - + } def dataTransactionR: Route = (path("data") & get) { parameters('fee.as[Int], 'data) { (fee, data) => - (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { - wallet => - Try { - val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount - val boxes = wallet.vault.walletStorage - .getAllBoxes().collectFirst { case ab: AssetBox => ab }.toList.take(1) - TransactionFactory.dataTransactionScratch(secret, - fee, - System.currentTimeMillis(), - boxes.map(_ -> None), - PubKeyLockedContract(wallet.vault.accountManagers.head.mandatoryAccount.publicImage.pubKeyBytes).contract, - data.getBytes) - }.toOption - }).flatMap { + (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { wallet => + Try { + val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount + val actualFee: Long = fee * 100000000 + val boxes = loopBoxes(wallet.vault.walletStorage.getAllBoxes().toList, List.empty[AssetBox], 0L, actualFee) + TransactionFactory.dataTransactionScratch( + secret, + fee, + System.currentTimeMillis(), + boxes.map(_ -> None), + PubKeyLockedContract(wallet.vault.accountManagers.head.mandatoryAccount.publicImage.pubKeyBytes).contract, + data.getBytes + ) + }.toOption + }).flatMap { case Some(tx: Transaction) => EncryApp.system.eventStream.publish(NewTransaction(tx)) Future.unit @@ -129,44 +148,49 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, def transferR: Route = (path("transfer") & get) { parameters('addr, 'fee.as[Int], 'amount.as[Long], 'token.?) { (addr, fee, amount, token) => - (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { - wallet => - Try { - val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount - val decodedTokenOpt = token.map(s => Algos.decode(s) match { - case Success(value) => ADKey @@ value - case Failure(e) => throw new RuntimeException(s"Failed to decode tokeId $s. Cause: $e") - }) - val boxes: IndexedSeq[MonetaryBox] = wallet.vault.walletStorage - .getAllBoxes() - .collect { - case ab: AssetBox if ab.tokenIdOpt.isEmpty || ab.tokenIdOpt === decodedTokenOpt => ab - case tib: TokenIssuingBox if decodedTokenOpt.exists(_.sameElements(tib.tokenId)) => tib - }.foldLeft(List.empty[MonetaryBox]) { - case (seq, box) if decodedTokenOpt.isEmpty && seq.map(_.amount).sum < (amount + fee) => seq :+ box - case (seq, _) if decodedTokenOpt.isEmpty => seq - case (seq, box: AssetBox) if box.tokenIdOpt.isEmpty => - if (seq.collect { case ab: AssetBox if ab.tokenIdOpt.isEmpty => ab.amount }.sum < fee) seq :+ box else seq - case (seq, box: AssetBox) => - val totalAmount = - seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + - seq.collect { case tib: TokenIssuingBox => tib.amount }.sum - if (totalAmount < amount) seq :+ box else seq - case (seq, box: TokenIssuingBox) => - val totalAmount = - seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + - seq.collect { case tib: TokenIssuingBox => tib.amount }.sum - if (totalAmount < amount) seq :+ box else seq + (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { wallet => + Try { + val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount + val actualFee = fee * 100000000 + val actualAmount = amount * 100000000 + val decodedTokenOpt = token.map( + s => + Algos.decode(s) match { + case Success(value) => ADKey @@ value + case Failure(e) => throw new RuntimeException(s"Failed to decode tokeId $s. Cause: $e") } - .toIndexedSeq - TransactionFactory.defaultPaymentTransaction(secret, - fee, - System.currentTimeMillis(), - boxes.map(_ -> None), - addr, - amount, - decodedTokenOpt) - }.toOption + ) + val boxes: IndexedSeq[MonetaryBox] = wallet.vault.walletStorage + .getAllBoxes() + .collect { + case ab: AssetBox if ab.tokenIdOpt.isEmpty || ab.tokenIdOpt === decodedTokenOpt => ab + case tib: TokenIssuingBox if decodedTokenOpt.exists(_.sameElements(tib.tokenId)) => tib + }.foldLeft(List.empty[MonetaryBox]) { + case (seq, box) if decodedTokenOpt.isEmpty && seq.map(_.amount).sum < (actualAmount + actualFee) => seq :+ box + case (seq, _) if decodedTokenOpt.isEmpty => seq + case (seq, box: AssetBox) if box.tokenIdOpt.isEmpty => + if (seq.collect { case ab: AssetBox if ab.tokenIdOpt.isEmpty => ab.amount }.sum < actualFee) seq :+ box else seq + case (seq, box: AssetBox) => + val totalAmount = + seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + + seq.collect { case tib: TokenIssuingBox => tib.amount }.sum + if (totalAmount < actualAmount) seq :+ box else seq + case (seq, box: TokenIssuingBox) => + val totalAmount = + seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + + seq.collect { case tib: TokenIssuingBox => tib.amount }.sum + if (totalAmount < actualAmount) seq :+ box else seq + }.toIndexedSeq + TransactionFactory.defaultPaymentTransaction( + secret, + actualFee, + System.currentTimeMillis(), + boxes.map(_ -> None), + addr, + actualAmount, + decodedTokenOpt + ) + }.toOption }).flatMap { case Some(tx: Transaction) => EncryApp.system.eventStream.publish(NewTransaction(tx)) @@ -178,47 +202,51 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, } def transferContractR: Route = (path("transferContract") & get) { - parameters('contract, 'fee.as[Int], 'amount.as[Long], 'token.?) { - (contract, fee, amount, token) => - (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { - wallet => - Try { - val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount - val decodedTokenOpt = token.map(s => Algos.decode(s) match { - case Success(value) => ADKey @@ value - case Failure(_) => throw new RuntimeException(s"Failed to decode tokeId $s") - }) - val boxes: IndexedSeq[MonetaryBox] = wallet.vault.walletStorage - .getAllBoxes() - .collect { - case ab: AssetBox if ab.tokenIdOpt.isEmpty || ab.tokenIdOpt === decodedTokenOpt => ab - case tib: TokenIssuingBox if decodedTokenOpt.exists(_.sameElements(tib.tokenId)) => tib - }.foldLeft(List.empty[MonetaryBox]) { - case (seq, box) if decodedTokenOpt.isEmpty => - if (seq.map(_.amount).sum < (amount + fee)) seq :+ box else seq - case (seq, box: AssetBox) if box.tokenIdOpt.isEmpty => - if (seq.collect{ case ab: AssetBox if ab.tokenIdOpt.isEmpty => ab.amount }.sum < fee) seq :+ box else seq - case (seq, box: AssetBox) => - val totalAmount = - seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + - seq.collect{ case tib: TokenIssuingBox => tib.amount }.sum - if (totalAmount < amount) seq :+ box else seq - case (seq, box: TokenIssuingBox) => - val totalAmount = - seq.collect{ case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount}.sum + - seq.collect{ case tib: TokenIssuingBox => tib.amount }.sum - if (totalAmount < amount) seq :+ box else seq + parameters('contract, 'fee.as[Int], 'amount.as[Long], 'token.?) { (contract, fee, amount, token) => + (dataHolder ? GetDataFromCurrentView[History, UtxoState, EncryWallet, Option[Transaction]] { wallet => + Try { + val secret: PrivateKey25519 = wallet.vault.accountManagers.head.mandatoryAccount + val decodedTokenOpt = token.map( + s => + Algos.decode(s) match { + case Success(value) => ADKey @@ value + case Failure(_) => throw new RuntimeException(s"Failed to decode tokeId $s") } - .toIndexedSeq - TransactionFactory.defaultContractTransaction(secret, - fee, - System.currentTimeMillis(), - boxes.map(_ -> None), - contract, - amount, - decodedTokenOpt) - }.toOption - }).flatMap { + ) + val actualFee = fee * 100000000 + val actualAmount = amount * 100000000 + val boxes: IndexedSeq[MonetaryBox] = wallet.vault.walletStorage + .getAllBoxes() + .collect { + case ab: AssetBox if ab.tokenIdOpt.isEmpty || ab.tokenIdOpt === decodedTokenOpt => ab + case tib: TokenIssuingBox if decodedTokenOpt.exists(_.sameElements(tib.tokenId)) => tib + }.foldLeft(List.empty[MonetaryBox]) { + case (seq, box) if decodedTokenOpt.isEmpty => + if (seq.map(_.amount).sum < (actualAmount + actualFee)) seq :+ box else seq + case (seq, box: AssetBox) if box.tokenIdOpt.isEmpty => + if (seq.collect { case ab: AssetBox if ab.tokenIdOpt.isEmpty => ab.amount }.sum < actualFee) seq :+ box else seq + case (seq, box: AssetBox) => + val totalAmount = + seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + + seq.collect { case tib: TokenIssuingBox => tib.amount }.sum + if (totalAmount < actualAmount) seq :+ box else seq + case (seq, box: TokenIssuingBox) => + val totalAmount = + seq.collect { case ab: AssetBox if ab.tokenIdOpt.nonEmpty => ab.amount }.sum + + seq.collect { case tib: TokenIssuingBox => tib.amount }.sum + if (totalAmount < actualAmount) seq :+ box else seq + }.toIndexedSeq + TransactionFactory.defaultContractTransaction( + secret, + actualFee, + System.currentTimeMillis(), + boxes.map(_ -> None), + contract, + actualAmount, + decodedTokenOpt + ) + }.toOption + }).flatMap { case Some(tx: Transaction) => EncryApp.system.eventStream.publish(NewTransaction(tx)) Future.unit @@ -230,20 +258,26 @@ case class WalletInfoApiRoute(dataHolder: ActorRef, def createKeyR: Route = (path("createKey") & get) { (dataHolder ? GetViewCreateKey) - .mapTo[PrivateKey25519].map(key => Algos.encode(key.privKeyBytes).asJson).okJson() + .mapTo[PrivateKey25519] + .map(key => Algos.encode(key.privKeyBytes).asJson) + .okJson() } def printPubKeysR: Route = (path("pubKeys") & get) { (dataHolder ? GetViewPrintPubKeys) - .mapTo[List[String]].map(_.asJson).okJson() + .mapTo[List[String]] + .map(_.asJson) + .okJson() } def getBalanceR: Route = (path("balance") & get) { (dataHolder ? GetViewGetBalance) - .mapTo[String].map(_.asJson).okJson() + .mapTo[String] + .map(_.asJson) + .okJson() } def getUtxosR: Route = (path("utxos") & get) { getWallet.map(wallet => wallet.walletStorage.getAllBoxes().asJson).okJson() } -} \ No newline at end of file +} diff --git a/src/main/scala/encry/api/http/routes/WalletRoute.scala b/src/main/scala/encry/api/http/routes/WalletRoute.scala index 1dde32a658..c397d25989 100644 --- a/src/main/scala/encry/api/http/routes/WalletRoute.scala +++ b/src/main/scala/encry/api/http/routes/WalletRoute.scala @@ -8,10 +8,11 @@ import com.typesafe.scalalogging.StrictLogging import encry.api.http.DataHolderForApi.{GetViewGetBalance, GetViewPrintPubKeys} import encry.settings.{EncryAppSettings, RESTApiSettings} import org.encryfoundation.common.modifiers.state.box.Box.Amount +import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId import org.encryfoundation.common.utils.Algos -import org.encryfoundation.common.utils.TaggedTypes.ADKey import scalatags.Text import scalatags.Text.all.{div, span, _} +import scorex.crypto.signatures.PublicKey import scala.concurrent.Future import scala.language.implicitConversions import scala.util.Success @@ -24,19 +25,18 @@ case class WalletRoute(settings: RESTApiSettings, val EttTokenId: String = Algos.encode(encrySettings.constants.IntrinsicTokenId) - def walletF: Future[Map[String, List[(String, Amount)]]] = + def walletF: Future[Map[(PublicKey, TokenId), Amount]] = (dataHolder ? GetViewGetBalance) - .mapTo[Map[String, List[(String, Amount)]]] + .mapTo[Map[(PublicKey, TokenId), Amount]] def pubKeysF: Future[List[String]] = (dataHolder ? GetViewPrintPubKeys).mapTo[List[String]] - def info: Future[(Map[String, List[(String, Amount)]], List[String])] = for { + def info: Future[(Map[(PublicKey, TokenId), Amount], List[String])] = for { wallet <- walletF pubKeys <- pubKeysF } yield (wallet, pubKeys) - def walletScript(balances: Map[String, List[(String, Amount)]]): Text.TypedTag[String] = { - + def walletScript(balances: Map[(PublicKey, TokenId), Amount]): Text.TypedTag[String] = { html( scalatags.Text.all.head( meta(charset := "utf-8"), @@ -426,10 +426,9 @@ case class WalletRoute(settings: RESTApiSettings, div(cls := "form-group", select(cls := "form-control", id :="coin", name:="coin", for { - coinI <- balances.toList - coinIds <- coinI._2 + walletMap <- balances.toList } yield { - option(value := coinIds._1, if (coinIds._1 == EttTokenId) s"ETT (${coinIds._2/100000000})" else coinIds._1) + option(value := walletMap._1._2.toString, if (Algos.encode(walletMap._1._2) == EttTokenId) s"ETT (${walletMap._2/100000000})" else Algos.encode(walletMap._1._2)) } ) ), @@ -686,9 +685,9 @@ case class WalletRoute(settings: RESTApiSettings, div(cls := "form-group", select(cls := "form-control", id :="coin", name:="coin", if (balances.nonEmpty) { - balances.values.flatten.toList.map( coinIds => - option(value := coinIds._1, if (coinIds._1 == EttTokenId) s"ETT (${coinIds._2/100000000})" else coinIds._1) - ) + balances.toList.map { coinIds => + option(value := Algos.encode(coinIds._1._2), if (Algos.encode(coinIds._1._2) == EttTokenId) s"ETT (${coinIds._2 / 100000000})" else Algos.encode(coinIds._1._2)) + } } else { option(value := "", "") } @@ -721,17 +720,16 @@ case class WalletRoute(settings: RESTApiSettings, tbody( if (balances.nonEmpty) { (for { - mapKeyValue <- balances - tokenAmount <- mapKeyValue._2 + mapKeyValue <- balances.toList } yield { - val tknStr = tokenAmount._1 match { - case tokenId if tokenId == EttTokenId => "ETT" - case tokenId => tokenId + val tknStr = mapKeyValue._1._2 match { + case tokenId if Algos.encode(tokenId) == EttTokenId => "ETT" + case tokenId => Algos.encode(tokenId) } tr( - th(mapKeyValue._1), + th(Algos.encode(mapKeyValue._1._1)), th(tknStr), - if (tokenAmount._1 == EttTokenId ) th(tokenAmount._2/100000000) else th(tokenAmount._2) + if (Algos.encode(mapKeyValue._1._2) == EttTokenId ) th(mapKeyValue._2/100000000) else th(mapKeyValue._2) ) }).toList } else { @@ -764,7 +762,7 @@ case class WalletRoute(settings: RESTApiSettings, ), tbody( if(balances.keys.nonEmpty) { - for (p <- balances.keys.toList) yield { + for (p <- balances.toList.map(x => Algos.encode(x._1._1))) yield { tr(th(attr("scope") := "row", p)) } } else { diff --git a/src/main/scala/encry/cli/commands/GetBalance.scala b/src/main/scala/encry/cli/commands/GetBalance.scala index ee745df54d..c210b1230a 100644 --- a/src/main/scala/encry/cli/commands/GetBalance.scala +++ b/src/main/scala/encry/cli/commands/GetBalance.scala @@ -26,7 +26,7 @@ object GetBalance extends Command { { val balance: String = view.vault.getBalances.foldLeft("")((str, tokenInfo) => - if (tokenInfo._1._2 != Algos.encode(settings.constants.IntrinsicTokenId)) + if (Algos.encode(tokenInfo._1._2) != Algos.encode(settings.constants.IntrinsicTokenId)) str.concat(s"TokenID(${tokenInfo._1._2}) for key ${tokenInfo._1._1} : ${tokenInfo._2}\n") else str.concat(s"TokenID(${tokenInfo._1._2}) for key ${tokenInfo._1._1} : ${BigDecimal(tokenInfo._2) / 100000000}\n") ) diff --git a/src/main/scala/encry/storage/levelDb/versionalLevelDB/WalletVersionalLevelDB.scala b/src/main/scala/encry/storage/levelDb/versionalLevelDB/WalletVersionalLevelDB.scala index 9991ea0933..ed96feb130 100644 --- a/src/main/scala/encry/storage/levelDb/versionalLevelDB/WalletVersionalLevelDB.scala +++ b/src/main/scala/encry/storage/levelDb/versionalLevelDB/WalletVersionalLevelDB.scala @@ -1,5 +1,6 @@ package encry.storage.levelDb.versionalLevelDB +import cats.Semigroup import cats.instances.all._ import cats.syntax.semigroup._ import com.google.common.primitives.Longs @@ -13,8 +14,10 @@ import org.encryfoundation.common.modifiers.state.box.EncryBaseBox import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId import org.encryfoundation.common.utils.Algos import org.encryfoundation.common.utils.TaggedTypes.{ADKey, ModifierId} +import org.encryfoundation.prismlang.compiler.CompiledContract.ContractHash import org.iq80.leveldb.DB import scorex.crypto.hash.Digest32 + import scala.util.Success case class WalletVersionalLevelDB(db: DB, settings: LevelDBSettings) extends StrictLogging with AutoCloseable { @@ -32,9 +35,11 @@ case class WalletVersionalLevelDB(db: DB, settings: LevelDBSettings) extends Str def getBoxById(id: ADKey): Option[EncryBaseBox] = levelDb.get(VersionalLevelDbKey @@ id.untag(ADKey)) .flatMap(wrappedBx => StateModifierSerializer.parseBytes(wrappedBx, id.head).toOption) - def getTokenBalanceById(id: TokenId): Option[Amount] = getBalances - .find(_._1._2 == Algos.encode(id)) - .map(_._2) + def getTokenBalanceById(id: TokenId): Option[Amount] = { + getBalances + .find(_._1._2 sameElements id) + .map(_._2) + } def containsBox(id: ADKey): Boolean = getBoxById(id).isDefined @@ -43,17 +48,27 @@ case class WalletVersionalLevelDB(db: DB, settings: LevelDBSettings) extends Str def updateWallet(modifierId: ModifierId, newBxs: Seq[EncryBaseBox], spentBxs: Seq[EncryBaseBox], intrinsicTokenId: ADKey): Unit = { val bxsToInsert: Seq[EncryBaseBox] = newBxs.filter(bx => !spentBxs.contains(bx)) - val newBalances: Map[(String, String), Amount] = { - val toRemoveFromBalance = BalanceCalculator.balanceSheet(spentBxs, intrinsicTokenId) - .map { case ((hash, key), value) => (hash, ByteStr(key)) -> value * -1 } - val toAddToBalance = BalanceCalculator.balanceSheet(newBxs, intrinsicTokenId) - .map { case ((hash, key), value) => (hash, ByteStr(key)) -> value } - val prevBalance = getBalances.map { case ((hash, id), value) => (hash, ByteStr(Algos.decode(id).get)) -> value } - (toAddToBalance |+| toRemoveFromBalance |+| prevBalance).map { case ((hash, tokenId), value) => (hash, tokenId.toString) -> value } + val newBalances: Map[(ContractHash, TokenId), Amount] = { + // (String, String) is a (ContractHash, TokenId) + val toRemoveFromBalance: Map[(String, String), Amount] = + BalanceCalculator + .balanceSheet(spentBxs, intrinsicTokenId) + .map { case ((hash, key), value) => (Algos.encode(hash), Algos.encode(key)) -> value * -1 } + val toAddToBalance: Map[(String, String), Amount] = + BalanceCalculator + .balanceSheet(newBxs, intrinsicTokenId) + .map { case ((hash, key), value) => (Algos.encode(hash), Algos.encode(key)) -> value } + val prevBalance: Map[(String, String), Amount] = getBalances.map { + case ((hash, id), value) => (Algos.encode(hash), Algos.encode(id)) -> value + } + val res = toRemoveFromBalance |+| toAddToBalance |+| prevBalance + res.map { + case ((hash, tokenId), value) => (Algos.decode(hash).get, Algos.decode(tokenId).get) -> value + } } val newBalanceKeyValue = BALANCE_KEY -> VersionalLevelDbValue @@ newBalances.foldLeft(Array.emptyByteArray) { case (acc, ((hash, tokenId), balance)) => - acc ++ Algos.decode(hash).get ++ Algos.decode(tokenId).get ++ Longs.toByteArray(balance) + acc ++ hash ++ tokenId ++ Longs.toByteArray(balance) } levelDb.insert(LevelDbDiff(LevelDBVersion @@ modifierId.untag(ModifierId), newBalanceKeyValue :: bxsToInsert.map(bx => (VersionalLevelDbKey @@ bx.id.untag(ADKey), @@ -62,10 +77,10 @@ case class WalletVersionalLevelDB(db: DB, settings: LevelDBSettings) extends Str ) } - def getBalances: Map[(String, String), Amount] = + def getBalances: Map[(ContractHash, TokenId), Amount] = levelDb.get(BALANCE_KEY) .map(_.sliding(72, 72) - .map(ch => (Algos.encode(ch.take(32)), Algos.encode(ch.slice(32, 64))) -> Longs.fromByteArray(ch.takeRight(8))) + .map(ch => (ch.take(32), ch.slice(32, 64)) -> Longs.fromByteArray(ch.takeRight(8))) .toMap).getOrElse(Map.empty) override def close(): Unit = levelDb.close() diff --git a/src/main/scala/encry/utils/BalanceCalculator.scala b/src/main/scala/encry/utils/BalanceCalculator.scala index e15f30ba99..3941fcb7c9 100644 --- a/src/main/scala/encry/utils/BalanceCalculator.scala +++ b/src/main/scala/encry/utils/BalanceCalculator.scala @@ -1,28 +1,66 @@ package encry.utils +import cats.instances.long._ +import cats.instances.map._ +import cats.syntax.semigroup._ import org.encryfoundation.common.modifiers.state.box.Box.Amount import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId -import org.encryfoundation.common.modifiers.state.box.{AssetBox, EncryBaseBox, TokenIssuingBox} +import org.encryfoundation.common.modifiers.state.box.{ AssetBox, EncryBaseBox, TokenIssuingBox } import org.encryfoundation.common.utils.Algos +import org.encryfoundation.prismlang.compiler.CompiledContract.ContractHash object BalanceCalculator { def balanceSheet(bxs: Traversable[EncryBaseBox], defaultTokenId: TokenId, - excludeTokenIssuance: Boolean = false): Map[(String, TokenId), Amount] = - bxs.foldLeft(Map.empty[(String, ByteStr), Amount]) { - case (cache, bx: AssetBox) => - val tokenId: ByteStr = ByteStr(bx.tokenIdOpt.getOrElse(defaultTokenId)) - val contractHash = Algos.encode(bx.proposition.contractHash) - cache.get(contractHash -> tokenId).map { amount => - cache.updated(contractHash -> tokenId, amount + bx.amount) - }.getOrElse(cache.updated(contractHash -> tokenId, bx.amount)) - case (cache, bx: TokenIssuingBox) if !excludeTokenIssuance => - val contractHash = Algos.encode(bx.proposition.contractHash) - val tokenId: ByteStr = ByteStr(bx.tokenId) - cache.get(contractHash -> tokenId).map { amount => - cache.updated(contractHash -> tokenId, amount + bx.amount) - }.getOrElse(cache.updated(contractHash -> tokenId, bx.amount)) - case (cache, _) => cache - }.map { case ((hash, id), am) => (hash -> id.arr) -> am } + excludeTokenIssuance: Boolean = false): Map[(ContractHash, TokenId), Amount] = + bxs + .foldLeft(Map.empty[(ByteStr, ByteStr), Amount]) { + case (cache, bx: AssetBox) => + val tokenId: ByteStr = ByteStr(bx.tokenIdOpt.getOrElse(defaultTokenId)) + val contractHash: ByteStr = ByteStr(bx.proposition.contractHash) + cache + .get(contractHash -> tokenId) + .map { amount => + cache.updated(contractHash -> tokenId, amount + bx.amount) + } + .getOrElse(cache.updated(contractHash -> tokenId, bx.amount)) + case (cache, bx: TokenIssuingBox) if !excludeTokenIssuance => + val contractHash: ByteStr = ByteStr(bx.proposition.contractHash) + val tokenId: ByteStr = ByteStr(bx.tokenId) + cache + .get(contractHash -> tokenId) + .map { amount => + cache.updated(contractHash -> tokenId, amount + bx.amount) + } + .getOrElse(cache.updated(contractHash -> tokenId, bx.amount)) + case (cache, _) => cache + } + .map { case ((hash, id), am) => (hash.arr -> id.arr) -> am } + + def balanceSheet1( + boxes: List[EncryBaseBox], + defaultTokenId: TokenId, + excludeTokenIssuing: Boolean = false + ): Map[ContractHash, Map[TokenId, Amount]] = + boxes + .foldLeft(Map.empty[String, Map[String, Amount]]) { + case (hashToValue: Map[String, Map[String, Amount]], box: AssetBox) => + hashToValue |+| Map( + Algos.encode(box.proposition.contractHash) -> Map( + Algos.encode(box.tokenIdOpt.getOrElse(defaultTokenId)) -> box.amount + ) + ) + case (hashToValue: Map[String, Map[String, Amount]], box: TokenIssuingBox) => + hashToValue |+| Map( + Algos.encode(box.proposition.contractHash) -> Map(Algos.encode(box.tokenId) -> box.amount) + ) + case (hashToValue: Map[String, Map[String, Amount]], _) => hashToValue + } + .map { + case (str: String, stringToAmount: Map[String, Amount]) => + Algos.decode(str).get -> stringToAmount.map { + case (str: String, amount: Amount) => Algos.decode(str).get -> amount + } + } } diff --git a/src/main/scala/encry/view/wallet/EncryWallet.scala b/src/main/scala/encry/view/wallet/EncryWallet.scala index 604a3a187e..e10e4642cc 100644 --- a/src/main/scala/encry/view/wallet/EncryWallet.scala +++ b/src/main/scala/encry/view/wallet/EncryWallet.scala @@ -14,7 +14,7 @@ import encry.storage.VersionalStorage import encry.storage.VersionalStorage.{StorageKey, StorageValue} import encry.storage.levelDb.versionalLevelDB.{LevelDbFactory, WalletVersionalLevelDB, WalletVersionalLevelDBCompanion} import encry.utils.CoreTaggedTypes.VersionTag -import encry.utils.Mnemonic +import encry.utils.{ByteStr, Mnemonic} import encry.view.state.UtxoStateReader import encry.view.state.avlTree.{InternalNode, LeafNode, Node, ShadowNode} import io.iohk.iodb.{LSMStore, Store} @@ -23,10 +23,12 @@ import org.encryfoundation.common.modifiers.PersistentModifier import org.encryfoundation.common.modifiers.history.Block import org.encryfoundation.common.modifiers.mempool.transaction.Transaction import org.encryfoundation.common.modifiers.state.StateModifierSerializer +import org.encryfoundation.common.modifiers.state.box.Box.Amount +import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId import org.encryfoundation.common.modifiers.state.box.{EncryBaseBox, EncryProposition, MonetaryBox} -import org.encryfoundation.common.utils.Algos import org.encryfoundation.common.utils.TaggedTypes.ModifierId import org.iq80.leveldb.{DB, Options} +import scorex.crypto.signatures.PublicKey import scala.util.{Failure, Success, Try} case class EncryWallet(walletStorage: WalletVersionalLevelDB, accountManagers: Seq[AccountManager], private val accountStore: Store) @@ -87,21 +89,19 @@ case class EncryWallet(walletStorage: WalletVersionalLevelDB, accountManagers: S def rollback(to: VersionTag): Try[Unit] = Try(walletStorage.rollback(ModifierId @@ to.untag(VersionTag))) - def getBalances: Seq[((String, String), Long)] = { - val pubKeys = publicKeys - val contractHashToKey = contractHashesToKeys(pubKeys) - val positiveBalance = walletStorage.getBalances.map { case ((hash, tokenId), amount) => - (contractHashToKey(hash), tokenId) -> amount + def getBalances: List[((PublicKey, TokenId), Amount)] = { + val pubKeys: Set[PublicKey25519] = publicKeys + val contractHashToKey: Map[ByteStr, PublicKey] = pubKeysToContractHashes(pubKeys) + val positiveBalance: Map[(PublicKey, TokenId), Amount] = walletStorage.getBalances.map { + case ((hash, tokenId), amount) => (contractHashToKey(ByteStr(hash)), tokenId) -> amount } - (pubKeys.map(k => Algos.encode(k.pubKeyBytes)) -- positiveBalance.keys.map(_._1)) - .map(_ -> Algos.encode(settings.constants.IntrinsicTokenId) -> 0L).toSeq ++ positiveBalance - }.sortBy(_._1._1 != Algos.encode(accountManagers.head.publicAccounts.head.pubKeyBytes)) + (pubKeys.map(k => ByteStr(k.pubKeyBytes)) -- positiveBalance.keys.map(l => ByteStr(l._1))) + .map(l => (PublicKey @@ l.arr) -> settings.constants.IntrinsicTokenId -> 0L).toSeq ++ positiveBalance + }.sortBy(l => !(l._1._1 sameElements accountManagers.head.publicAccounts.head.pubKeyBytes)).toList - def contractHashesToKeys(pubKeys: Set[PublicKey25519]): Map[String, String] = pubKeys - .map(key => Algos.encode(key.pubKeyBytes) -> key.address.address) - .map { case (key, addr) => - Algos.encode(EncryProposition.addressLocked(addr).contractHash) -> key - }.toMap + def pubKeysToContractHashes(pubKeys: Set[PublicKey25519]): Map[ByteStr, PublicKey] = pubKeys + .map(key => ByteStr(EncryProposition.addressLocked(key.address.address).contractHash) -> key.pubKeyBytes) + .toMap private def validateMnemonicKey(mnemonic: String): Either[NonEmptyChain[String], String] = { val words: Array[String] = mnemonic.split(" ") @@ -154,7 +154,7 @@ object EncryWallet extends StrictLogging { } case leafNode: LeafNode[StorageKey, StorageValue] => StateModifierSerializer.parseBytes(leafNode.value, leafNode.key.head) match { - case Success(bx) => collectBx(bx, accounts) + case Success(bx) => collectBx(bx, accounts) case Failure(exception) => throw exception //??????? } case shadowNode: ShadowNode[StorageKey, StorageValue] => List.empty @@ -172,9 +172,10 @@ object EncryWallet extends StrictLogging { val db: DB = LevelDbFactory.factory.open(walletDir, new Options) val accountManagerStore: LSMStore = new LSMStore(keysDir, keepVersions = 0, keySize = 34) // 34 = 1 prefix byte + 1 account number byte + 32 key bytes val walletStorage: WalletVersionalLevelDB = WalletVersionalLevelDBCompanion(db, settings.levelDB) - val password: String = settings.wallet.map(_.password).getOrElse(throw new RuntimeException("Password not specified")) + val password: String = + settings.wallet.map(_.password).getOrElse(throw new RuntimeException("Password not specified")) val restoredAccounts: Seq[AccountManager] = AccountManager.restoreAccounts(accountManagerStore, password) //init keys EncryWallet(walletStorage, restoredAccounts, accountManagerStore) } -} \ No newline at end of file +} diff --git a/src/main/scala/encry/view/wallet/WalletDB.scala b/src/main/scala/encry/view/wallet/WalletDB.scala new file mode 100644 index 0000000000..90b2deda93 --- /dev/null +++ b/src/main/scala/encry/view/wallet/WalletDB.scala @@ -0,0 +1,42 @@ +package encry.view.wallet + +import encry.settings.EncryAppSettings +import encry.storage.levelDb.versionalLevelDB.VersionalLevelDB +import org.encryfoundation.common.modifiers.state.box.Box.Amount +import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId +import org.encryfoundation.common.modifiers.state.box.{ AssetBox, DataBox, EncryBaseBox, TokenIssuingBox } +import org.encryfoundation.common.utils.TaggedTypes.{ ADKey, ModifierId } +import org.encryfoundation.prismlang.compiler.CompiledContract.ContractHash + +trait WalletDB { + + def getBoxById(id: ADKey): Option[EncryBaseBox] + + def getAssetBoxesByPredicate(contractHash: ContractHash, f: List[AssetBox] => Boolean): List[AssetBox] + + def getTokenIssuingBoxesByPredicate( + contractHash: ContractHash, + f: List[TokenIssuingBox] => Boolean + ): List[TokenIssuingBox] + + def getDataBoxesByPredicate(contractHash: ContractHash, f: List[DataBox] => Boolean): List[DataBox] + + def getBalancesByContractHash(contractHash: ContractHash): Map[TokenId, Amount] + + def getBalances: Map[ContractHash, Map[TokenId, Amount]] + + def contains(id: ADKey): Boolean + + def updateWallet( + modifierId: ModifierId, + newBxs: List[EncryBaseBox], + spentBxs: List[EncryBaseBox], + intrinsicTokenId: ADKey + ): Unit + + def rollback(modId: ModifierId): Unit +} + +object WalletDB { + def apply(levelDb: VersionalLevelDB, settings: EncryAppSettings): WalletDB = WalletDBImpl.apply(levelDb, settings) +} diff --git a/src/main/scala/encry/view/wallet/WalletDBImpl.scala b/src/main/scala/encry/view/wallet/WalletDBImpl.scala new file mode 100644 index 0000000000..709e1e9e12 --- /dev/null +++ b/src/main/scala/encry/view/wallet/WalletDBImpl.scala @@ -0,0 +1,294 @@ +package encry.view.wallet + +import cats.syntax.semigroup._ +import cats.instances.map._ +import cats.instances.long._ +import cats.instances.set._ +import com.google.common.primitives.Longs +import com.typesafe.scalalogging.StrictLogging +import encry.settings.EncryAppSettings +import encry.storage.levelDb.versionalLevelDB.VersionalLevelDBCompanion.{ + LevelDBVersion, + VersionalLevelDbKey, + VersionalLevelDbValue +} +import encry.storage.levelDb.versionalLevelDB.{ LevelDbDiff, VersionalLevelDB } +import encry.utils.BalanceCalculator +import org.encryfoundation.common.modifiers.state.StateModifierSerializer +import org.encryfoundation.common.modifiers.state.box.Box.Amount +import org.encryfoundation.common.modifiers.state.box.TokenIssuingBox.TokenId +import org.encryfoundation.common.modifiers.state.box.{ AssetBox, DataBox, EncryBaseBox, TokenIssuingBox } +import org.encryfoundation.common.utils.Algos +import org.encryfoundation.common.utils.TaggedTypes.{ ADKey, ModifierId } +import org.encryfoundation.prismlang.compiler.CompiledContract.ContractHash +import scorex.crypto.hash.Digest32 + +import scala.annotation.tailrec +import scala.reflect.ClassTag + +class WalletDBImpl( + levelDb: VersionalLevelDB, + settings: EncryAppSettings +) extends WalletDB + with StrictLogging + with AutoCloseable { + + private def getTypedBoxById[BXT: ClassTag](id: ADKey): Option[BXT] = + levelDb + .get(VersionalLevelDbKey @@ id.untag(ADKey)) + .flatMap(StateModifierSerializer.parseBytes(_, id.head).toOption) + .collect { case box: BXT => box } + + override def getBoxById(id: ADKey): Option[EncryBaseBox] = getTypedBoxById[EncryBaseBox](id) + + override def getBalances: Map[ContractHash, Map[TokenId, Amount]] = + getAllWallets.map(hash => hash -> getBalancesByContractHash(hash)).toMap + + override def getBalancesByContractHash(contractHash: ContractHash): Map[TokenId, Amount] = + levelDb + .get(hashToTokens(contractHash)) + .map(_.grouped(32).toList.map(id => id -> getTokenBalanceByContractHash(contractHash, id)).toMap) + .getOrElse(Map.empty) + + def getAllWallets: List[ContractHash] = + levelDb + .get(CONTRACT_HASH_ACCOUNTS) + .map(_.grouped(32).toList) + .getOrElse(List.empty[ContractHash]) + + private def getTokenIds(hash: ContractHash): List[TokenId] = + levelDb + .get(hashToTokens(hash)) + .map(_.grouped(32).toList) + .getOrElse(List.empty[TokenId]) + + private def getBoxesIdsByKey(key: VersionalLevelDbKey): List[ADKey] = + levelDb + .get(key) + .map(_.grouped(32).toList.map(ADKey @@ _)) + .getOrElse(List.empty[ADKey]) + + @tailrec + private def computePredicate[BXT: ClassTag](acc: List[BXT], ids: List[ADKey], f: List[BXT] => Boolean): List[BXT] = + ids.headOption match { + case Some(boxId: ADKey) => + val newAcc: List[BXT] = getTypedBoxById[BXT](boxId).fold(acc)(_ :: acc) + if (f(newAcc)) newAcc else computePredicate(newAcc, ids.drop(1), f) + case None => List.empty[BXT] + } + + //List.empty + override def getAssetBoxesByPredicate( + contractHash: ContractHash, + f: List[AssetBox] => Boolean + ): List[AssetBox] = computePredicate[AssetBox]( + List.empty[AssetBox], + getBoxesIdsByKey(assetBoxesByContractHashKey(contractHash)), + f + ) + + override def getTokenIssuingBoxesByPredicate( + contractHash: ContractHash, + f: List[TokenIssuingBox] => Boolean + ): List[TokenIssuingBox] = computePredicate[TokenIssuingBox]( + List.empty[TokenIssuingBox], + getBoxesIdsByKey(tokenIssuingBoxesByContractHashKey(contractHash)), + f + ) + + override def getDataBoxesByPredicate( + contractHash: ContractHash, + f: List[DataBox] => Boolean + ): List[DataBox] = computePredicate[DataBox]( + List.empty[DataBox], + getBoxesIdsByKey(dataBoxesByContractHashKey(contractHash)), + f + ) + + private def getTokenBalanceByContractHash(contractHash: ContractHash, tokenId: TokenId): Amount = + levelDb.get(tokenKeyByContractHash(contractHash, tokenId)).map(Longs.fromByteArray).getOrElse(0L) + + override def contains(id: ADKey): Boolean = getBoxById(id).isDefined + + override def updateWallet( + modifierId: ModifierId, + newBxs: List[EncryBaseBox], + spentBxs: List[EncryBaseBox], + intrinsicTokenId: ADKey + ): Unit = { + val boxesToInsert: List[EncryBaseBox] = newBxs.filterNot(spentBxs.contains) + + def balanceSheetFunction(list: List[EncryBaseBox], x: Long = -1): Map[String, Map[String, Amount]] = + BalanceCalculator.balanceSheet1(list, intrinsicTokenId).map { + case (hash: ContractHash, idToAmount: Map[TokenId, Amount]) => + Algos.encode(hash) -> idToAmount.map { + case (id: TokenId, amount: Amount) => Algos.encode(id) -> (x * amount) + } + } + + val infoAboutWalletToInsert: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = { + val toAddBalances: Map[String, Map[String, Amount]] = balanceSheetFunction(newBxs, 1L) + println(s"toAdd = $toAddBalances") + val toRemoveBalances: Map[String, Map[String, Amount]] = balanceSheetFunction(spentBxs) + println(s"toRemove = $toRemoveBalances") + val currentBalances: Map[String, Map[String, Amount]] = + getBalances.map { + case (hash: ContractHash, idToAmount: Map[TokenId, Amount]) => + println(s"idsToAmount = ${idToAmount.map { + case (a, b) => Algos.encode(a) -> b + }}") + Algos.encode(hash) -> idToAmount.map { + case (id: TokenId, amount: Amount) => Algos.encode(id) -> amount + } + } + println(s"currentBalance = $currentBalances") + val updatedWallets = toAddBalances |+| toRemoveBalances |+| currentBalances + println(s"updateWallets = $updatedWallets") + val newContractHashes: Array[Byte] = updatedWallets.keys.toList.flatMap(id => Algos.decode(id).get).toArray + val contractHashToInsert + : (VersionalLevelDbKey, VersionalLevelDbValue) = CONTRACT_HASH_ACCOUNTS -> VersionalLevelDbValue @@ newContractHashes + + updatedWallets.flatMap { + case (hash: String, idToAmount: Map[String, Amount]) => + val decodedHash: Array[Byte] = Algos.decode(hash).get + val a: List[String] = getTokenIds(decodedHash).map(l => Algos.encode(l)) + val b: List[String] = idToAmount.keys.toList + val c: List[String] = a ::: b + val d: Set[String] = c.toSet + val e: List[String] = d.toList + val f: List[Array[Byte]] = e.map(j => Algos.decode(j).get) + val g: Array[Array[Byte]] = f.toArray + val h: Array[Byte] = g.flatten + val tokenIdsToUpdate: (VersionalLevelDbKey, VersionalLevelDbValue) = + hashToTokens(decodedHash) -> VersionalLevelDbValue @@ h + val tokenIdUpdatedAmount: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = idToAmount.map { + case (id: String, amount: Amount) => + tokenKeyByContractHash(decodedHash, Algos.decode(id).get) -> + VersionalLevelDbValue @@ Longs.toByteArray(amount) + }.toList + + contractHashToInsert :: tokenIdsToUpdate :: tokenIdUpdatedAmount + }.toList + } + + println( + s"infoAboutWalletToInsert -> ${infoAboutWalletToInsert.map(l => Algos.encode(l._1) -> Algos.encode(l._2)).mkString(",")}" + ) + + val boxesIdsToContractHashToInsert: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = { + def updatedFunction( + hashToBxIds: Map[String, Set[String]], + nextHash: String, + key: VersionalLevelDbKey + ): Map[String, Set[String]] = + hashToBxIds.updated( + nextHash, + getBoxesIdsByKey(key) + .filterNot(l => spentBxs.exists(_.id sameElements l)) + .map(Algos.encode) + .toSet + ) + val ( + assetsFromDb: Map[String, Set[String]], + dataFromDB: Map[String, Set[String]], + tokensFromDB: Map[String, Set[String]] + ) = getAllWallets.foldLeft( + Map.empty[String, Set[String]], + Map.empty[String, Set[String]], + Map.empty[String, Set[String]] + ) { + case ((hashToAssetIds, hashToDataIds, hashToTokenIds), nextHash) => + val nextHashEncoded: String = Algos.encode(nextHash) + (updatedFunction(hashToAssetIds, nextHashEncoded, assetBoxesByContractHashKey(nextHash)), + updatedFunction(hashToDataIds, nextHashEncoded, dataBoxesByContractHashKey(nextHash)), + updatedFunction(hashToTokenIds, nextHashEncoded, tokenIssuingBoxesByContractHashKey(nextHash))) + } + val ( + hashToAssetBoxes: Map[String, Set[String]], + hashToDataBoxes: Map[String, Set[String]], + hashToTokenBoxes: Map[String, Set[String]] + ) = boxesToInsert.foldLeft( + Map.empty[String, Set[String]], + Map.empty[String, Set[String]], + Map.empty[String, Set[String]] + ) { + case ((assets, data, token), nextBox: AssetBox) => + val hash = Algos.encode(nextBox.proposition.contractHash) + (assets |+| Map(hash -> Set(Algos.encode(nextBox.id))), data, token) + case ((assets, data, token), nextBox: DataBox) => + val hash = Algos.encode(nextBox.proposition.contractHash) + (assets, data |+| Map(hash -> Set(Algos.encode(nextBox.id))), token) + case ((assets, data, token), nextBox: TokenIssuingBox) => + val hash = Algos.encode(nextBox.proposition.contractHash) + (assets, data, token |+| Map(hash -> Set(Algos.encode(nextBox.id)))) + } + + def hashToBxsIdsToDB( + typeToDb: Map[String, Set[String]], + hashType: Map[String, Set[String]], + key: ContractHash => VersionalLevelDbKey + ): List[(VersionalLevelDbKey, VersionalLevelDbValue)] = + (typeToDb |+| hashType).map { + case (hash: String, value: Set[String]) => + key(Algos.decode(hash).get) -> VersionalLevelDbValue @@ value.toArray + .flatMap(k => Algos.decode(k).get) + }.toList + + val newAssetsToDB: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = + hashToBxsIdsToDB(assetsFromDb, hashToAssetBoxes, assetBoxesByContractHashKey) + val newDataToDB: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = + hashToBxsIdsToDB(dataFromDB, hashToDataBoxes, dataBoxesByContractHashKey) + val newTokenToDB: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = + hashToBxsIdsToDB(tokensFromDB, hashToTokenBoxes, tokenIssuingBoxesByContractHashKey) + newAssetsToDB ::: newDataToDB ::: newTokenToDB + } + + val toInsertBoxes: List[(VersionalLevelDbKey, VersionalLevelDbValue)] = + boxesToInsert.map(box => (VersionalLevelDbKey @@ box.id.untag(ADKey)) -> VersionalLevelDbValue @@ box.bytes) + + val toRemoveBoxes: List[VersionalLevelDbKey] = spentBxs.map(l => VersionalLevelDbKey @@ l.id.untag(ADKey)) + + levelDb.insert( + LevelDbDiff( + LevelDBVersion @@ modifierId.untag(ModifierId), + infoAboutWalletToInsert ::: boxesIdsToContractHashToInsert ::: toInsertBoxes, + toRemoveBoxes + ) + ) + } + + override def rollback(modId: ModifierId): Unit = levelDb.rollbackTo(LevelDBVersion @@ modId.untag(ModifierId)) + + override def close(): Unit = levelDb.close() + + private val ASSET_BOXES_BYTES: Array[Byte] = "ASSET_BOXES_BY_USER_KEY".getBytes() + + private val TOKEN_ISSUING_BOXES_BYTES: Array[Byte] = "TOKEN_ISSUING_BOXES_BYTES".getBytes() + + private val DATA_BOXES_BYTES: Array[Byte] = "DATA_BOXES_BYTES".getBytes() + + private val CONTRACT_HASH_TOKEN_IDS: Array[Byte] = "CONTRACT_HASH_TOKEN_IDS".getBytes() + + private val CONTRACT_HASH_ACCOUNTS: VersionalLevelDbKey = VersionalLevelDbKey @@ Algos + .hash("CONTRACT_HASH_ACCOUNTS") + .untag(Digest32) + + private def hashToTokens(userHash: ContractHash): VersionalLevelDbKey = + VersionalLevelDbKey @@ Algos.hash(userHash ++ CONTRACT_HASH_TOKEN_IDS) + + private def assetBoxesByContractHashKey(userHash: ContractHash): VersionalLevelDbKey = + VersionalLevelDbKey @@ Algos.hash(userHash ++ ASSET_BOXES_BYTES) + + private def tokenIssuingBoxesByContractHashKey(userHash: ContractHash): VersionalLevelDbKey = + VersionalLevelDbKey @@ Algos.hash(userHash ++ TOKEN_ISSUING_BOXES_BYTES) + + private def dataBoxesByContractHashKey(userHash: ContractHash): VersionalLevelDbKey = + VersionalLevelDbKey @@ Algos.hash(userHash ++ DATA_BOXES_BYTES) + + private def tokenKeyByContractHash(userHash: ContractHash, tokenId: TokenId): VersionalLevelDbKey = + VersionalLevelDbKey @@ Algos.hash(userHash ++ tokenId) +} + +object WalletDBImpl { + def apply(levelDb: VersionalLevelDB, settings: EncryAppSettings): WalletDBImpl = new WalletDBImpl(levelDb, settings) +} diff --git a/src/test/scala/encry/view/wallet/WalletDbSpec.scala b/src/test/scala/encry/view/wallet/WalletDbSpec.scala new file mode 100644 index 0000000000..bb2a527e70 --- /dev/null +++ b/src/test/scala/encry/view/wallet/WalletDbSpec.scala @@ -0,0 +1,335 @@ +package encry.view.wallet + +import com.typesafe.scalalogging.StrictLogging +import encry.modifiers.InstanceFactory +import encry.settings.{ LevelDBSettings, Settings } +import encry.storage.levelDb.versionalLevelDB.{ LevelDbFactory, VersionalLevelDB, VersionalLevelDBCompanion } +import encry.utils.{ EncryGenerator, FileHelper } +import org.encryfoundation.common.crypto.PrivateKey25519 +import org.encryfoundation.common.modifiers.state.box._ +import org.encryfoundation.common.utils.Algos +import org.encryfoundation.common.utils.TaggedTypes.{ ADKey, ModifierId } +import org.iq80.leveldb.{ DB, Options } +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{ Matchers, WordSpecLike } +import scorex.utils.Random + +class WalletDbSpec + extends WordSpecLike + with Matchers + with InstanceFactory + with EncryGenerator + with StrictLogging + with Settings + with MockitoSugar { + + def initTestState: (WalletDB, Seq[EncryBaseBox]) = { + val levelDB: DB = LevelDbFactory.factory.open(FileHelper.getRandomTempDir, new Options) + val vlDB: VersionalLevelDB = VersionalLevelDBCompanion(levelDB, LevelDBSettings(5)) + def dbInstance: WalletDB = WalletDB.apply(vlDB, settings) + val boxesToInsert: List[EncryBaseBox] = genValidPaymentTxs(3).flatMap(_.newBoxes).toList + dbInstance.updateWallet( + ModifierId @@ Random.randomBytes(), + boxesToInsert, + List.empty, + settings.constants.IntrinsicTokenId + ) + (dbInstance, boxesToInsert) + } + + "WalletDb.getBoxById" should { + "return non empty value for existed box in db" in { + val (walletDb: WalletDB, inserted: Seq[EncryBaseBox]) = initTestState + val comparisonResult: Boolean = inserted.forall { box => + val boxFromDB: Option[EncryBaseBox] = walletDb.getBoxById(box.id) + boxFromDB.isDefined && boxFromDB.forall { takenBox => + box.bytes.sameElements(takenBox.bytes) + } + } + comparisonResult shouldBe true + + val boxesToInsert: List[EncryBaseBox] = genValidPaymentTxs(5).flatMap(_.newBoxes).toList + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + boxesToInsert, + inserted.toList, + settings.constants.IntrinsicTokenId + ) + val comparisonResultNegative: Boolean = inserted.forall { box => + walletDb.getBoxById(box.id).isEmpty + } + val comparisonResultPositive: Boolean = boxesToInsert.forall { box => + val boxFromDB: Option[EncryBaseBox] = walletDb.getBoxById(box.id) + boxFromDB.isDefined && boxFromDB.forall { takenBox => + box.bytes.sameElements(takenBox.bytes) + } + } + (comparisonResultNegative && comparisonResultPositive) shouldBe true + } + "return empty value for non existed box in db" in { + val (walletDb: WalletDBImpl, _) = initTestState + walletDb.getBoxById(ADKey @@ Random.randomBytes()).isEmpty shouldBe true + } + } + + "WalletDb.getAllWallets" should { + "return all inserted wallets" in { + val (walletDb: WalletDBImpl, inserted: Seq[EncryBaseBox]) = initTestState + val wallets = walletDb.getAllWallets.map(Algos.encode) + val neededWallets = inserted + .map(l => Algos.encode(l.proposition.contractHash)) + .toSet + (wallets.size == neededWallets.size && wallets.forall(neededWallets.contains)) shouldBe true + + val boxesToInsert: List[EncryBaseBox] = genValidPaymentTxs(5).flatMap(_.newBoxes).toList + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + boxesToInsert, + inserted.toList, + settings.constants.IntrinsicTokenId + ) + + val walletsNew = walletDb.getAllWallets.map(Algos.encode) + val neededWalletsNew = inserted.map(l => Algos.encode(l.proposition.contractHash)) ++ boxesToInsert.map( + l => Algos.encode(l.proposition.contractHash) + ) + (walletsNew.size == neededWalletsNew.size && walletsNew.forall(neededWalletsNew.contains)) shouldBe true + } + } + + "WalletDb.getBalances" should { + "return a correct result" in {} + } + + "WalletDb.getAssetBoxesByPredicate" should { + "return a correct result if the result satisfies the predicate" in { + val (walletDb: WalletDBImpl, _) = initTestState + val moreBoxes: IndexedSeq[AssetBox] = { + val key: PrivateKey25519 = genPrivKeys(1).head + IndexedSeq( + AssetBox(EncryProposition.addressLocked(key.publicImage.address.address), 11, 999, None), + AssetBox(EncryProposition.addressLocked(key.publicImage.address.address), 111, 9999, None), + AssetBox(EncryProposition.addressLocked(key.publicImage.address.address), 112, 9991, None), + AssetBox(EncryProposition.addressLocked(key.publicImage.address.address), 113, 999654, None), + ) + } + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + moreBoxes.toList, + List.empty, + settings.constants.IntrinsicTokenId + ) + val boxes = walletDb.getAssetBoxesByPredicate( + moreBoxes.head.proposition.contractHash, + list => list.map(_.amount).sum == moreBoxes.map(_.amount).sum + ) + boxes.nonEmpty && boxes.forall { box => + moreBoxes.exists(_.bytes sameElements box.bytes) + } shouldBe true + + walletDb + .getAssetBoxesByPredicate( + moreBoxes.head.proposition.contractHash, + list => list.map(_.amount).sum > moreBoxes.map(_.amount).sum + ) + .isEmpty shouldBe true + + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + List.empty, + moreBoxes.take(2).toList, + settings.constants.IntrinsicTokenId + ) + + val boxesNew = walletDb.getAssetBoxesByPredicate( + moreBoxes.head.proposition.contractHash, + list => list.map(_.amount).sum == moreBoxes.map(_.amount).sum + ) + boxesNew.nonEmpty && boxesNew.forall { box => + moreBoxes.exists(_.bytes sameElements box.bytes) + } shouldBe false + + val boxesNew1 = walletDb.getAssetBoxesByPredicate( + moreBoxes.head.proposition.contractHash, + list => list.map(_.amount).sum == moreBoxes.drop(2).map(_.amount).sum + ) + boxesNew1.nonEmpty && boxesNew1.forall { box => + moreBoxes.drop(2).exists(_.bytes sameElements box.bytes) + } shouldBe true + } + } + + "WalletDb.getDataBoxes" should { + "return a correct result if the result satisfies the predicate" in { + val (walletDb: WalletDBImpl, _) = initTestState + val dataBoxesToInsert: IndexedSeq[DataBox] = { + val key: PrivateKey25519 = genPrivKeys(1).head + IndexedSeq( + DataBox(EncryProposition.addressLocked(key.publicImage.address.address), 99, Random.randomBytes()), + DataBox(EncryProposition.addressLocked(key.publicImage.address.address), 991, Random.randomBytes()), + DataBox(EncryProposition.addressLocked(key.publicImage.address.address), 992, Random.randomBytes()) + ) + } + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + dataBoxesToInsert.toList, + List.empty, + settings.constants.IntrinsicTokenId + ) + + walletDb + .getDataBoxesByPredicate( + dataBoxesToInsert.head.proposition.contractHash, + boxes => boxes.size == dataBoxesToInsert.size + ) + .nonEmpty shouldBe true + + walletDb + .getDataBoxesByPredicate( + dataBoxesToInsert.head.proposition.contractHash, + boxes => boxes.size > dataBoxesToInsert.size + ) + .nonEmpty shouldBe false + } + } + + "WalletDb.getBalances" should { + "return correct balances" in { + val (walletDb: WalletDBImpl, _) = initTestState + val tkId1 = Random.randomBytes() + val tkId2 = Random.randomBytes() + val key = genPrivKeys(1) + val key2 = genPrivKeys(1) + val boxesToInsertForPerson1: IndexedSeq[EncryBox[EncryProposition]] = { + IndexedSeq( + TokenIssuingBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 1234L, 340, tkId1), + TokenIssuingBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 4321L, 570, tkId1), + DataBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 99, Random.randomBytes()), + AssetBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 11, 2000, None), + AssetBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 111, 3000, None) + ) + } + val boxesToInsertForPerson2: IndexedSeq[EncryBox[EncryProposition]] = { + IndexedSeq( + TokenIssuingBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 1234L, 999, tkId2), + TokenIssuingBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 4321L, 9991, tkId2), + DataBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 99, Random.randomBytes()), + AssetBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 11, 999, None), + AssetBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 111, 9999, None) + ) + } + val ch1 = boxesToInsertForPerson1.head.proposition.contractHash + val ch2 = boxesToInsertForPerson2.head.proposition.contractHash + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + boxesToInsertForPerson1.toList ::: boxesToInsertForPerson2.toList, + List.empty, + settings.constants.IntrinsicTokenId + ) + + boxesToInsertForPerson1.take(2).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch1) + .filter { case (id, _) => Algos.encode(id) != Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + + boxesToInsertForPerson2.take(2).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch2) + .filter { case (id, _) => Algos.encode(id) != Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + + boxesToInsertForPerson1.drop(3).map(x => x.asInstanceOf[AssetBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch1) + .filter { case (id, _) => Algos.encode(id) == Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + + boxesToInsertForPerson2.drop(3).map(x => x.asInstanceOf[AssetBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch2) + .filter { case (id, _) => Algos.encode(id) == Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + + val boxesToRemoveForPerson1 = IndexedSeq( + TokenIssuingBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 1234L, 900, tkId1), + AssetBox(EncryProposition.addressLocked(key.head.publicImage.address.address), 111, 4000, None) + ) + + val boxesToRemoveForPerson2 = IndexedSeq( + TokenIssuingBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 1234L, 900, tkId2), + AssetBox(EncryProposition.addressLocked(key2.head.publicImage.address.address), 111, 4000, None) + ) + + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + List.empty, + boxesToRemoveForPerson1.toList ++ boxesToRemoveForPerson2.toList, + settings.constants.IntrinsicTokenId + ) + + boxesToInsertForPerson1.take(2).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum - + boxesToRemoveForPerson1.take(1).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch1) + .filter { case (id, _) => Algos.encode(id) != Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + + boxesToInsertForPerson2.take(2).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum - + boxesToRemoveForPerson2.take(1).map(x => x.asInstanceOf[TokenIssuingBox]).map(x => x.amount).sum shouldEqual + walletDb + .getBalancesByContractHash(ch2) + .filter { case (id, _) => Algos.encode(id) != Algos.encode(settings.constants.IntrinsicTokenId) } + .values + .toList + .sum + } + } + + "WalletDb.getTokenIssuingBoxes" should { + "return a correct result if the result satisfies the predicate" in { + val (walletDb: WalletDBImpl, _) = initTestState + val tid1 = Random.randomBytes() + val tid2 = Random.randomBytes() + val tokenBoxesToInsert: IndexedSeq[TokenIssuingBox] = { + val key: PrivateKey25519 = genPrivKeys(1).head + IndexedSeq( + TokenIssuingBox(EncryProposition.addressLocked(key.publicImage.address.address), 1234L, 999, tid1), + TokenIssuingBox(EncryProposition.addressLocked(key.publicImage.address.address), 4321L, 9991, tid1), + TokenIssuingBox(EncryProposition.addressLocked(key.publicImage.address.address), 9887L, 9992, tid2), + TokenIssuingBox(EncryProposition.addressLocked(key.publicImage.address.address), 768594L, 9993, tid2) + ) + } + walletDb.updateWallet( + ModifierId @@ Random.randomBytes(), + tokenBoxesToInsert.toList, + List.empty, + settings.constants.IntrinsicTokenId + ) + + walletDb + .getTokenIssuingBoxesByPredicate( + tokenBoxesToInsert.head.proposition.contractHash, + boxes => boxes.size == tokenBoxesToInsert.size + ) + .nonEmpty shouldBe true + + walletDb + .getDataBoxesByPredicate( + tokenBoxesToInsert.head.proposition.contractHash, + boxes => boxes.size > tokenBoxesToInsert.size + ) + .nonEmpty shouldBe false + } + } +} diff --git a/src/test/scala/encry/view/wallet/WalletSpec.scala b/src/test/scala/encry/view/wallet/WalletSpec.scala index 97a0ea31d0..e968ea612f 100644 --- a/src/test/scala/encry/view/wallet/WalletSpec.scala +++ b/src/test/scala/encry/view/wallet/WalletSpec.scala @@ -164,12 +164,12 @@ class WalletSpec extends PropSpec with Matchers with InstanceFactory with EncryG wallet.scanPersistent(block) - val addr1 = Algos.encode(keyManagerOne.mandatoryAccount.publicKeyBytes) - val addr2 = Algos.encode(keyManagerTwo.mandatoryAccount.publicKeyBytes) - val addr3 = Algos.encode(extraAcc.publicKeyBytes) + val addr1: String = Algos.encode(keyManagerOne.mandatoryAccount.publicKeyBytes) + val addr2: String = Algos.encode(keyManagerTwo.mandatoryAccount.publicKeyBytes) + val addr3: String = Algos.encode(extraAcc.publicKeyBytes) - wallet.getBalances.filter(_._1._1 == addr1).map(_._2).sum shouldEqual txsQty * Props.boxValue - wallet.getBalances.filter(_._1._1 == addr2).map(_._2).sum shouldEqual (txsQty - 1) * Props.boxValue - wallet.getBalances.filter(_._1._1 == addr3).map(_._2).sum shouldEqual (txsQty - 2) * Props.boxValue + wallet.getBalances.filter(x => Algos.encode(x._1._1) == addr1).map(_._2).sum shouldEqual txsQty * Props.boxValue + wallet.getBalances.filter(x => Algos.encode(x._1._1) == addr2).map(_._2).sum shouldEqual (txsQty - 1) * Props.boxValue + wallet.getBalances.filter(x => Algos.encode(x._1._1) == addr3).map(_._2).sum shouldEqual (txsQty - 2) * Props.boxValue } } \ No newline at end of file