diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 4ad7f508be..9587de29f5 100644 --- a/src/main/resources/api/openapi.yaml +++ b/src/main/resources/api/openapi.yaml @@ -6134,6 +6134,13 @@ paths: schema: type: string default: desc + - in: query + name: includeUnconfirmed + required: false + description: if true include unconfirmed transactions from mempool + schema: + type: boolean + default: false responses: '200': description: unspent boxes associated with wanted address @@ -6289,6 +6296,13 @@ paths: schema: type: string default: desc + - in: query + name: includeUnconfirmed + required: false + description: if true include unconfirmed transactions from mempool + schema: + type: boolean + default: false responses: '200': description: unspent boxes with wanted ergotree diff --git a/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala index 408f7a0364..df2bfbbb71 100644 --- a/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/BlockchainApiRoute.scala @@ -46,6 +46,8 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting } private val sortDir: Directive[Tuple1[Direction]] = parameters("sortDirection".as(sortMarshaller) ? DESC) + private val unconfirmed: Directive[Tuple1[Boolean]] = parameters("includeUnconfirmed".as[Boolean] ? false) + /** * Total number of boxes/transactions that can be requested at once to avoid too heavy requests ([[BlocksApiRoute.MaxHeaders]]) */ @@ -94,11 +96,11 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting private def getHistoryWithMempool: Future[(ErgoHistoryReader,ErgoMemPoolReader)] = (readersHolder ? GetReaders).mapTo[Readers].map(r => (r.h, r.m)) - private def getAddress(tree: ErgoTree)(history: ErgoHistoryReader): Option[IndexedErgoAddress] = { + private def getAddress(tree: ErgoTree)(history: ErgoHistoryReader): Option[IndexedErgoAddress] = history.typedExtraIndexById[IndexedErgoAddress](hashErgoTree(tree)) - } - private def getAddress(addr: ErgoAddress)(history: ErgoHistoryReader): Option[IndexedErgoAddress] = getAddress(addr.script)(history) + private def getAddress(addr: ErgoAddress)(history: ErgoHistoryReader): Option[IndexedErgoAddress] = + getAddress(addr.script)(history) private def getTxById(id: ModifierId)(history: ErgoHistoryReader): Option[IndexedErgoTransaction] = history.typedExtraIndexById[IndexedErgoTransaction](id) match { @@ -237,10 +239,10 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting validateAndGetBoxesByAddress(address, offset, limit) } - private def getBoxesByAddressUnspent(addr: ErgoAddress, offset: Int, limit: Int, sortDir: Direction): Future[Seq[IndexedErgoBox]] = - getHistory.map { history => + private def getBoxesByAddressUnspent(addr: ErgoAddress, offset: Int, limit: Int, sortDir: Direction, unconfirmed: Boolean): Future[Seq[IndexedErgoBox]] = + getHistoryWithMempool.map { case (history, mempool) => getAddress(addr)(history) match { - case Some(addr) => addr.retrieveUtxos(history, offset, limit, sortDir) + case Some(addr) => addr.retrieveUtxos(history, mempool, offset, limit, sortDir, unconfirmed) case None => Seq.empty[IndexedErgoBox] } } @@ -248,26 +250,27 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting private def validateAndGetBoxesByAddressUnspent(address: ErgoAddress, offset: Int, limit: Int, - dir: Direction): Route = { + dir: Direction, + unconfirmed: Boolean): Route = { if (limit > MaxItems) { BadRequest(s"No more than $MaxItems boxes can be requested") } else if (dir == SortDirection.INVALID) { BadRequest("Invalid parameter for sort direction, valid values are \"ASC\" and \"DESC\"") } else { - ApiResponse(getBoxesByAddressUnspent(address, offset, limit, dir)) + ApiResponse(getBoxesByAddressUnspent(address, offset, limit, dir, unconfirmed)) } } private def getBoxesByAddressUnspentR: Route = - (post & pathPrefix("box" / "unspent" / "byAddress") & ergoAddress & paging & sortDir) { - (address, offset, limit, dir) => - validateAndGetBoxesByAddressUnspent(address, offset, limit, dir) + (post & pathPrefix("box" / "unspent" / "byAddress") & ergoAddress & paging & sortDir & unconfirmed) { + (address, offset, limit, dir, unconfirmed) => + validateAndGetBoxesByAddressUnspent(address, offset, limit, dir, unconfirmed) } private def getBoxesByAddressUnspentGetRoute: Route = - (pathPrefix("box" / "unspent" / "byAddress") & get & addressPass & paging & sortDir) { - (address, offset, limit, dir) => - validateAndGetBoxesByAddressUnspent(address, offset, limit, dir) + (pathPrefix("box" / "unspent" / "byAddress") & get & addressPass & paging & sortDir & unconfirmed) { + (address, offset, limit, dir, unconfirmed) => + validateAndGetBoxesByAddressUnspent(address, offset, limit, dir, unconfirmed) } private def getBoxRange(offset: Int, limit: Int): Future[Seq[ModifierId]] = @@ -304,21 +307,21 @@ case class BlockchainApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSetting } } - private def getBoxesByErgoTreeUnspent(tree: ErgoTree, offset: Int, limit: Int, sortDir: Direction): Future[Seq[IndexedErgoBox]] = - getHistory.map { history => + private def getBoxesByErgoTreeUnspent(tree: ErgoTree, offset: Int, limit: Int, sortDir: Direction, unconfirmed: Boolean): Future[Seq[IndexedErgoBox]] = + getHistoryWithMempool.map { case (history, mempool) => getAddress(tree)(history) match { - case Some(iEa) => iEa.retrieveUtxos(history, offset, limit, sortDir) - case None => Seq.empty[IndexedErgoBox] + case Some(addr) => addr.retrieveUtxos(history, mempool, offset, limit, sortDir, unconfirmed) + case None => Seq.empty[IndexedErgoBox] } } - private def getBoxesByErgoTreeUnspentR: Route = (post & pathPrefix("box" / "unspent" / "byErgoTree") & ergoTree & paging & sortDir) { (tree, offset, limit, dir) => + private def getBoxesByErgoTreeUnspentR: Route = (post & pathPrefix("box" / "unspent" / "byErgoTree") & ergoTree & paging & sortDir & unconfirmed) { (tree, offset, limit, dir, unconfirmed) => if(limit > MaxItems) { BadRequest(s"No more than $MaxItems boxes can be requested") }else if (dir == SortDirection.INVALID) { - BadRequest("Invalid parameter for sort direction, valid values are \"ASC\" and \"DESC\"") + BadRequest("Invalid parameter for sort direction, valid values are 'ASC' and 'DESC'") }else { - ApiResponse(getBoxesByErgoTreeUnspent(tree, offset, limit, dir)) + ApiResponse(getBoxesByErgoTreeUnspent(tree, offset, limit, dir, unconfirmed)) } } diff --git a/src/main/scala/org/ergoplatform/nodeView/history/extra/IndexedErgoAddress.scala b/src/main/scala/org/ergoplatform/nodeView/history/extra/IndexedErgoAddress.scala index 0965922603..8abd05bba1 100644 --- a/src/main/scala/org/ergoplatform/nodeView/history/extra/IndexedErgoAddress.scala +++ b/src/main/scala/org/ergoplatform/nodeView/history/extra/IndexedErgoAddress.scala @@ -5,7 +5,8 @@ import org.ergoplatform.http.api.SortDirection.{ASC, DESC, Direction} import org.ergoplatform.nodeView.history.{ErgoHistory, ErgoHistoryReader} import org.ergoplatform.nodeView.history.extra.ExtraIndexer.{ExtraIndexTypeId, fastIdToBytes} import org.ergoplatform.nodeView.history.extra.IndexedErgoAddress.{getBoxes, getFromSegments, getTxs} -import org.ergoplatform.nodeView.history.extra.IndexedErgoAddressSerializer.{boxSegmentId, txSegmentId} +import org.ergoplatform.nodeView.history.extra.IndexedErgoAddressSerializer.{boxSegmentId, hashErgoTree, txSegmentId} +import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader import org.ergoplatform.settings.Algos import scorex.core.serialization.ErgoSerializer import scorex.util.{ModifierId, ScorexLogging, bytesToId} @@ -141,14 +142,21 @@ case class IndexedErgoAddress(treeHash: ModifierId, /** * Get a range of the boxes associated with this address that are NOT spent * @param history - history to use + * @param mempool - mempool to use, if unconfirmed is true * @param offset - items to skip from the start * @param limit - items to retrieve * @param sortDir - whether to start retreival from newest box ([[DESC]]) or oldest box ([[ASC]]) + * @param unconfirmed - whether to include unconfirmed boxes * @return array of unspent boxes */ - def retrieveUtxos(history: ErgoHistoryReader, offset: Int, limit: Int, sortDir: Direction): Array[IndexedErgoBox] = { + def retrieveUtxos(history: ErgoHistoryReader, + mempool: ErgoMemPoolReader, + offset: Int, + limit: Int, + sortDir: Direction, + unconfirmed: Boolean): Seq[IndexedErgoBox] = { val data: ArrayBuffer[IndexedErgoBox] = ArrayBuffer.empty[IndexedErgoBox] - sortDir match { + val confirmedBoxes: Seq[IndexedErgoBox] = sortDir match { case DESC => data ++= boxes.filter(_ > 0).map(n => NumericBoxIndex.getBoxByNumber(history, n).get) var segment: Int = boxSegmentCount @@ -157,7 +165,7 @@ case class IndexedErgoAddress(treeHash: ModifierId, history.typedExtraIndexById[IndexedErgoAddress](boxSegmentId(treeHash, segment)).get.boxes .filter(_ > 0).map(n => NumericBoxIndex.getBoxByNumber(history, n).get) ++=: data } - data.reverse.slice(offset, offset + limit).toArray + data.reverse.slice(offset, offset + limit) case ASC => var segment: Int = 0 while (data.length < (limit + offset) && segment < boxSegmentCount) { @@ -167,8 +175,18 @@ case class IndexedErgoAddress(treeHash: ModifierId, } if(data.length < (limit + offset)) data ++= boxes.filter(_ > 0).map(n => NumericBoxIndex.getBoxByNumber(history, n).get) - data.slice(offset, offset + limit).toArray + data.slice(offset, offset + limit) } + if(unconfirmed) { + val mempoolBoxes = mempool.getAll.flatMap(_.transaction.outputs) + .filter(box => hashErgoTree(box.ergoTree) == treeHash) + val unconfirmedBoxes = mempoolBoxes.map(new IndexedErgoBox(0, None, None, _, 0)) + sortDir match { + case DESC => unconfirmedBoxes ++ confirmedBoxes + case ASC => confirmedBoxes ++ unconfirmedBoxes + } + } else + confirmedBoxes } /**