Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR 211 rebased on master #244

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class AddressesRoutes[F[_]: Concurrent: ContextShift: Timer: AdaptThrowabl

private def getTotalBalanceR =
interpreter.toRoutes(defs.getTotalBalanceDef) { addr =>
addresses.totalBalanceOf(addr).adaptThrowable.value
addresses.totalBalanceWithConsiderationOfMempoolFor(addr).adaptThrowable.value
}

private def getBatchAddressInfo =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ trait Addresses[F[_]] {
def totalBalanceOf(address: Address): F[TotalBalance]

def addressInfoOf(batch: List[Address], minConfirmations: Int = 0): F[Map[Address, AddressInfo]]

/** Get TotalBalance of address with consideration of mempool data
*/
def totalBalanceWithConsiderationOfMempoolFor(address: Address): F[TotalBalance]
}

object Addresses {
Expand Down Expand Up @@ -73,6 +77,12 @@ object Addresses {
)) ||> trans.xa
}

def totalBalanceWithConsiderationOfMempoolFor(address: Address): F[TotalBalance] =
for {
confirmed <- confirmedBalanceOf(address, 0)
unconfirmed <- memprops.getUnconfirmedBalanceByAddress(address, confirmed)
} yield TotalBalance(confirmed, unconfirmed)

private def hasBeenUsedByErgoTree(ergoTree: HexString): F[Boolean] =
outputRepo.countAllByErgoTree(ergoTree).map(_ > 0) ||> trans.xa

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import org.ergoplatform.explorer.db.models.aggregates.{ExtendedAsset, ExtendedUA
import org.ergoplatform.explorer.db.models.{UOutput, UTransaction}
import org.ergoplatform.explorer.db.repositories.bundles.UtxRepoBundle
import org.ergoplatform.explorer.http.api.streaming.CompileStream
import org.ergoplatform.explorer.http.api.v1.models.{UInputInfo, UOutputInfo, UTransactionInfo}
import org.ergoplatform.explorer.http.api.v1.models.{Balance, UInputInfo, UOutputInfo, UTransactionInfo}
import org.ergoplatform.explorer.http.api.v1.utils.BuildUnconfirmedBalance
import org.ergoplatform.explorer.settings.{ServiceSettings, UtxCacheSettings}
import org.ergoplatform.explorer.{Address, BoxId, ErgoTree, TxId}
import org.ergoplatform.{explorer, ErgoAddressEncoder}
import org.ergoplatform.{ErgoAddressEncoder, explorer}
import org.ergoplatform.explorer.protocol.sigma.addressToErgoTreeNewtype
import org.ergoplatform.explorer.syntax.stream._
import tofu.Throws
Expand All @@ -27,6 +28,7 @@ trait MempoolProps[F[_], D[_]] {
def hasUnconfirmedBalance(ergoTree: ErgoTree): F[Boolean]
def mkUnspentOutputInfo: Pipe[D, Chunk[UOutput], UOutputInfo]
def mkTransaction: Pipe[D, Chunk[UTransaction], UTransactionInfo]
def getUnconfirmedBalanceByAddress(address: Address, confirmedBalance: Balance): F[Balance]
}

object MempoolProps {
Expand All @@ -47,6 +49,29 @@ object MempoolProps {

import repo._

def getUnconfirmedBalanceByAddress(
address: Address,
confirmedBalance: Balance
): F[Balance] = {
val ergoTree = addressToErgoTreeNewtype(address)
txs
.countByErgoTree(ergoTree.value)
.flatMap {
case 0 => Balance.empty.pure[D]
case _ =>
txs
.streamRelatedToErgoTree(ergoTree, 0, Int.MaxValue)
.chunkN(settings.chunkSize)
.through(mkTransaction)
.to[List]
.map { poolItems =>
BuildUnconfirmedBalance(poolItems, confirmedBalance, ergoTree, ergoTree.value)
}
}
.thrushK(trans.xa)
}


def hasUnconfirmedBalance(ergoTree: ErgoTree): F[Boolean] =
txs
.countByErgoTree(ergoTree.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.ergoplatform.explorer.http.api.v1.utils

import org.ergoplatform.explorer.http.api.models.AssetInstanceInfo
import org.ergoplatform.explorer.http.api.v1.models._
import org.ergoplatform.explorer.{ErgoTree, HexString, TokenId}

object BuildUnconfirmedBalance {

/** Build unconfirmed balance considering MemPool Data
* by reducing a `List[`[[org.ergoplatform.explorer.http.api.v1.models.UTransactionInfo]]`]`
* into a [[org.ergoplatform.explorer.http.api.v1.models.Balance]]
*
* Unconfirmed Balance arithmetic is completed in three steps:
* <li> determine if a [[org.ergoplatform.explorer.http.api.v1.models.UTransactionInfo]] is a credit or debit by
* matching its [[org.ergoplatform.explorer.http.api.v1.models.UInputInfo]] to wallets </li>
* <li> reducing similar [[org.ergoplatform.explorer.http.api.v1.models.UOutputInfo]]
* sums (credits/debits) into a single value: `debitSum/creditSum` </li>
* <li> subtracting or adding `debitSum/creditSum` into iterations current balance </li>
*
* @param items unsigned transactions from the MemPool
* @param confirmedBalance last signed & unspent balance
* @param ergoTree for transaction type identification
* @param hexString for NSum evaluation
* @return
*/
def apply(
items: List[UTransactionInfo],
confirmedBalance: Balance,
ergoTree: ErgoTree,
hexString: HexString
): Balance =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply formatting

items.foldLeft(confirmedBalance) { case (balance, transactionInfo) =>
transactionInfo.inputs.head.ergoTree match {
case ieT if ieT == ergoTree =>
val debitSum = transactionInfo.outputs.foldLeft(0L) { case (sum, outputInfo) =>
if (outputInfo.ergoTree != hexString) sum + outputInfo.value
else sum
}

val debitSumTokenGroups = transactionInfo.outputs
.foldLeft(Map[TokenId, AssetInstanceInfo]()) { case (groupedTokens, outputInfo) =>
if (outputInfo.ergoTree != hexString)
outputInfo.assets.foldLeft(groupedTokens) { case (gT, asset) =>
val gTAsset = gT.getOrElse(asset.tokenId, asset.copy(amount = 0L))
gT + (asset.tokenId -> gTAsset.copy(amount = gTAsset.amount + asset.amount))
}
else groupedTokens
}

val newTokensBalance = balance.tokens.map { token =>
debitSumTokenGroups.get(token.tokenId).map { assetInfo =>
token.copy(amount = token.amount - assetInfo.amount)
} match {
case Some(value) => value
case None => token
}
}

Balance(balance.nanoErgs - debitSum, newTokensBalance)
case _ =>
val creditSum = transactionInfo.outputs.foldLeft(0L) { case (sum, outputInfo) =>
if (outputInfo.ergoTree == hexString) sum + outputInfo.value
else sum
}

val creditSumTokenGroups = transactionInfo.outputs
.foldLeft(Map[TokenId, AssetInstanceInfo]()) { case (groupedTokens, outputInfo) =>
if (outputInfo.ergoTree == hexString)
outputInfo.assets.foldLeft(groupedTokens) { case (gT, asset) =>
val gTAsset = gT.getOrElse(asset.tokenId, asset.copy(amount = 0L))
gT + (asset.tokenId -> gTAsset.copy(amount = gTAsset.amount + asset.amount))
}
else groupedTokens
}

val newTokensBalance = balance.tokens.map { token =>
creditSumTokenGroups.get(token.tokenId).map { assetInfo =>
token.copy(amount = token.amount + assetInfo.amount)
} match {
case Some(value) => value
case None => token
}
}

Balance(balance.nanoErgs + creditSum, newTokensBalance)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import org.ergoplatform.explorer.db.repositories.{
TokenRepo,
TransactionRepo
}
import org.ergoplatform.explorer.testSyntax.runConnectionIO._
import org.ergoplatform.explorer.Address
import org.ergoplatform.explorer.commonGenerators.forSingleInstance
import org.ergoplatform.explorer.db.models.aggregates.AggregatedAsset
import org.ergoplatform.explorer.http.api.v1.models.TokenAmount
import org.ergoplatform.explorer.protocol.sigma
import org.ergoplatform.explorer.v1.services.constants.SenderAddressString
import org.ergoplatform.explorer.http.api.streaming.CompileStream
import org.ergoplatform.explorer.http.api.v1.services.{Addresses, Mempool}
import org.ergoplatform.explorer.settings.{RedisSettings, ServiceSettings, UtxCacheSettings}
Expand Down Expand Up @@ -55,7 +62,81 @@ class AS_A extends AddressesSpec {
implicit val addressEncoder: ErgoAddressEncoder =
ErgoAddressEncoder(networkPrefix.value.toByte)

"Address Service" should "" in {}
"Address Service" should "get confirmed Balance (nanoErgs) of address" in {
implicit val trans: Trans[ConnectionIO, IO] = Trans.fromDoobie(xa)
import tofu.fs2Instances._
withResources[IO](container.mappedPort(redisTestPort))
.use { case (settings, utxCache, redis) =>
withServices[IO, ConnectionIO](settings, utxCache, redis) { (addr, _, _) =>
val addressT = Address.fromString[Try](SenderAddressString)
addressT.isSuccess should be(true)
val addressTree = sigma.addressToErgoTreeHex(addressT.get)
val boxValues = List((100.toNanoErgo, 1), (200.toNanoErgo, 1))
withLiveRepos[ConnectionIO] { (headerRepo, txRepo, oRepo, _, _, _) =>
forSingleInstance(
balanceOfAddressGen(
mainChain = true,
address = addressT.get,
addressTree,
values = boxValues
)
) { infoTupleList =>
infoTupleList.foreach { case (header, out, tx) =>
headerRepo.insert(header).runWithIO()
oRepo.insert(out).runWithIO()
txRepo.insert(tx).runWithIO()
}
val balance = addr.confirmedBalanceOf(addressT.get, 0).unsafeRunSync().nanoErgs
balance should be(300.toNanoErgo)
}
}

}
}
.unsafeRunSync()
}

}

class AS_B extends AddressesSpec {

val networkPrefix: String Refined ValidByte = "16" // strictly run test-suite with testnet network prefix
implicit val addressEncoder: ErgoAddressEncoder =
ErgoAddressEncoder(networkPrefix.value.toByte)

"Address Service" should "get confirmed balance (tokens) of address" in {
implicit val trans: Trans[ConnectionIO, IO] = Trans.fromDoobie(xa)
import tofu.fs2Instances._
withResources[IO](container.mappedPort(redisTestPort))
.use { case (settings, utxCache, redis) =>
withServices[IO, ConnectionIO](settings, utxCache, redis) { (addr, _, _) =>
val addressT = Address.fromString[Try](SenderAddressString)
addressT.isSuccess should be(true)
val addressTree = sigma.addressToErgoTreeHex(addressT.get)
withLiveRepos[ConnectionIO] { (headerRepo, txRepo, oRepo, _, tokenRepo, assetRepo) =>
forSingleInstance(
balanceOfAddressWithTokenGen(mainChain = true, address = addressT.get, addressTree, 1, 5)
) { infoTupleList =>
infoTupleList.foreach { case (header, out, tx, _, token, asset) =>
headerRepo.insert(header).runWithIO()
oRepo.insert(out).runWithIO()
txRepo.insert(tx).runWithIO()
tokenRepo.insert(token).runWithIO()
assetRepo.insert(asset).runWithIO()
}
val tokeAmount = infoTupleList.map { x =>
val tk = x._5
val ase = x._6
TokenAmount(AggregatedAsset(tk.id, ase.amount, tk.name, tk.decimals, tk.`type`))
}
val tokenBalance = addr.confirmedBalanceOf(addressT.get, 0).unsafeRunSync().tokens
tokenBalance should contain theSameElementsAs tokeAmount
}
}
}
}
.unsafeRunSync()
}
}

class AS_C extends AddressesSpec {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.ergoplatform.explorer.v1.services

import org.ergoplatform.explorer.TokenId
import org.ergoplatform.explorer.http.api.v1.utils.BuildUnconfirmedBalance
import org.ergoplatform.explorer.v1.utils.TransactionSimulator.Simulator
import org.ergoplatform.explorer.v1.utils.TransactionSimulator.constants.{SigUSD1, SigUSD2, TransactionFee, WalletT}
import org.scalatest._
import org.scalatest.flatspec._
import org.scalatest.matchers._
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import scala.util.Success

class BuildUnconfirmedBalanceSpec extends AnyFlatSpec with should.Matchers with ScalaCheckDrivenPropertyChecks {

// 100Erg 100Token
val senderWallet: WalletT = WalletT(
100000000000L,
Map(
TokenId(SigUSD1) -> 10000L,
TokenId(SigUSD2) -> 10000L
)
)

// 10Erg 10
val receivingWallet: WalletT = WalletT(
100000000000L,
Map(
TokenId(SigUSD1) -> 10000L,
TokenId(SigUSD2) -> 10000L
)
)

"BuildUnconfirmedBalance protocol" should "calculate sending wallet nanoErg unconfirmed balance" in {
val toSend = 5000000000L // 5Erg
val res = Simulator(senderWallet, receivingWallet, toSend, None)
res shouldBe a[Success[_]]

val txInfo = res.get.sender

val unconfirmedBalance =
BuildUnconfirmedBalance(txInfo.items, WalletT.toBalance(senderWallet), txInfo.ergoTree, txInfo.ergoTree.value)

unconfirmedBalance.nanoErgs shouldBe (senderWallet.balance - TransactionFee - toSend)
}

it should "calculate receiving wallet nanoErg unconfirmed balance" in {
val toSend = 8000000000L // 8Erg
val res = Simulator(senderWallet, receivingWallet, toSend, None)
res shouldBe a[Success[_]]

val txInfo = res.get.receiver

val unconfirmedBalance =
BuildUnconfirmedBalance(txInfo.items, WalletT.toBalance(receivingWallet), txInfo.ergoTree, txInfo.ergoTree.value)

unconfirmedBalance.nanoErgs shouldBe (receivingWallet.balance + toSend)
}

it should "calculate sending wallet assets unconfirmed balance" in {
val tokenSentAmount = 100L
val res = Simulator(
senderWallet,
receivingWallet,
TransactionFee,
Some(
Map(
TokenId(SigUSD1) -> tokenSentAmount,
TokenId(SigUSD2) -> tokenSentAmount
)
)
)
res shouldBe a[Success[_]]

val txInfo = res.get.sender
val sWalletAsBalance = WalletT.toBalance(senderWallet)

val unconfirmedBalance =
BuildUnconfirmedBalance(txInfo.items, sWalletAsBalance, txInfo.ergoTree, txInfo.ergoTree.value)

unconfirmedBalance.tokens shouldBe sWalletAsBalance.tokens.map(t => t.copy(amount = t.amount - tokenSentAmount))

}

it should "calculate receiving wallet assets unconfirmed balance" in {
val tokenSentAmount = 100L
val res = Simulator(
senderWallet,
receivingWallet,
TransactionFee,
Some(
Map(
TokenId(SigUSD1) -> tokenSentAmount,
TokenId(SigUSD2) -> tokenSentAmount
)
)
)
res shouldBe a[Success[_]]

val txInfo = res.get.receiver
val rWalletAsBalance = WalletT.toBalance(receivingWallet)

val unconfirmedBalance =
BuildUnconfirmedBalance(txInfo.items, rWalletAsBalance, txInfo.ergoTree, txInfo.ergoTree.value)

unconfirmedBalance.tokens shouldBe rWalletAsBalance.tokens.map(t => t.copy(amount = t.amount + tokenSentAmount))
}
}
Loading