diff --git a/app/org/tronscan/actions/ActionRunner.scala b/app/org/tronscan/actions/ActionRunner.scala index b4d2f7c..b1c5088 100644 --- a/app/org/tronscan/actions/ActionRunner.scala +++ b/app/org/tronscan/actions/ActionRunner.scala @@ -7,6 +7,7 @@ import javax.inject.Inject import play.api.Logger import play.api.cache.NamedCache import play.api.cache.redis.CacheAsyncApi +import play.api.inject.ConfigurationProvider import scala.concurrent.duration._ @@ -15,7 +16,8 @@ class ActionRunner @Inject()( representativeListReader: RepresentativeListReader, statsOverview: StatsOverview, voteList: VoteList, - voteScraper: VoteScraper) extends Actor { + voteScraper: VoteScraper, + configurationProvider: ConfigurationProvider) extends Actor { val decider: Supervision.Decider = { exc => Logger.error("CACHE WARMER ERROR", exc) @@ -57,10 +59,12 @@ class ActionRunner @Inject()( } override def preStart(): Unit = { - startWitnessReader() - startVoteListWarmer() - startVoteScraper() - startStatsOverview() + if (configurationProvider.get.get[Boolean]("cache.warmer")) { + startWitnessReader() + startVoteListWarmer() + startVoteScraper() + startStatsOverview() + } } def receive = { diff --git a/app/org/tronscan/actions/StatsOverview.scala b/app/org/tronscan/actions/StatsOverview.scala index 792bed4..2b11082 100644 --- a/app/org/tronscan/actions/StatsOverview.scala +++ b/app/org/tronscan/actions/StatsOverview.scala @@ -5,6 +5,9 @@ import org.tron.common.repositories.StatsRepository import scala.concurrent.ExecutionContext +/** + * Reads the total stats + */ class StatsOverview @Inject()( statsRepository: StatsRepository) { diff --git a/app/org/tronscan/api/AccountApi.scala b/app/org/tronscan/api/AccountApi.scala index 6cc3246..f23a6f4 100644 --- a/app/org/tronscan/api/AccountApi.scala +++ b/app/org/tronscan/api/AccountApi.scala @@ -34,6 +34,7 @@ import scala.async.Async._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ +import org.tronscan.Extensions._ @Api( value = "Accounts", @@ -55,6 +56,10 @@ class AccountApi @Inject()( val key = configurationProvider.get.get[String]("play.http.secret.key") + /** + * Query accounts + * @return + */ @ApiResponses(Array( new ApiResponse( code = 200, @@ -112,6 +117,9 @@ class AccountApi @Inject()( } } + /** + * Find account by the given address + */ @ApiOperation( value = "Find account by address", response = classOf[AccountModel]) @@ -149,7 +157,7 @@ class AccountApi @Inject()( "allowance" -> account.allowance, "url" -> witness.map(_.url), ), - "name" -> new String(account.accountName.toByteArray).toString, + "name" -> account.accountName.decodeString, "address" -> address, "bandwidth" -> Json.obj( "freeNetUsed" -> accountBandwidth.freeNetUsed, @@ -166,6 +174,7 @@ class AccountApi @Inject()( ), "balances" -> Json.toJson(balances), "balance" -> account.balance, + "allowance" -> account.allowance, "tokenBalances" -> Json.toJson(balances), "frozen" -> Json.obj( "total" -> account.frozen.map(_.frozenBalance).sum, @@ -180,6 +189,9 @@ class AccountApi @Inject()( } } + /** + * Retrieves the balances for the given address + */ @ApiOperation( value = "", hidden = true @@ -218,6 +230,9 @@ class AccountApi @Inject()( } } + /** + * Votes cast by the given address + */ @ApiOperation( value = "", hidden = true @@ -226,7 +241,7 @@ class AccountApi @Inject()( for { wallet <- walletClient.full account <- wallet.getAccount(Account( - address = ByteString.copyFrom(Base58.decode58Check(address)) + address = address.decodeAddress, )) } yield { @@ -248,6 +263,11 @@ class AccountApi @Inject()( } } + /** + * Retrieve the Super Representative github link + * @param address address of the SR + * @return + */ @ApiOperation( value = "", hidden = true @@ -267,6 +287,9 @@ class AccountApi @Inject()( } } + /** + * Update the super representative pages + */ @ApiOperation( value = "", hidden = true) @@ -308,70 +331,9 @@ class AccountApi @Inject()( } } - @ApiOperation( - value = "", - hidden = true - ) - def resync = Action.async { req => - - throw new Exception("DISABLED") - - val decider: Supervision.Decider = { - case exc => - println("SOLIDITY STREAM ERROR", exc, ExceptionUtils.getStackTrace(exc)) - Supervision.Resume - } - - implicit val materializer = ActorMaterializer( - ActorMaterializerSettings(system) - .withSupervisionStrategy(decider))(system) - - async { - - val accounts = await(repo.findAll).toList - - val source = Source(accounts) - .mapAsync(8) { existingAccount => - async { - - val walletSolidity = await(walletClient.full) - - val account = await(walletSolidity.getAccount(Account( - address = ByteString.copyFrom(Base58.decode58Check(existingAccount.address)) - ))) - - if (account != null) { - - val accountModel = AccountModel( - address = existingAccount.address, - name = new String(account.accountName.toByteArray), - balance = account.balance, - power = account.frozen.map(_.frozenBalance).sum, - tokenBalances = Json.toJson(account.asset), - dateUpdated = DateTime.now, - ) - - List(repo.buildInsertOrUpdate(accountModel)) ++ addressBalanceModelRepository.buildUpdateBalance(accountModel) - } else { - List.empty - } - - } - } - .flatMapConcat(queries => Source(queries)) - .groupedWithin(150, 10.seconds) - .mapAsync(1) { queries => - blockModelRepository.executeQueries(queries) - } - .toMat(Sink.ignore)(Keep.right) - .run - - await(source) - - Ok("Done") - } - } - + /** + * Synchronises the given account to the database + */ @ApiOperation( value = "", hidden = true) @@ -484,7 +446,8 @@ class AccountApi @Inject()( } /** - * Generates a new private key + * Generates a new account with private key + * * @return */ def create = Action { diff --git a/app/org/tronscan/api/BaseApi.scala b/app/org/tronscan/api/BaseApi.scala index 469de0f..c986e45 100644 --- a/app/org/tronscan/api/BaseApi.scala +++ b/app/org/tronscan/api/BaseApi.scala @@ -11,6 +11,9 @@ trait BaseApi extends InjectedController { "count", ) + /** + * Strip query parameters which are related to paging and navigation of the results + */ def stripNav(params: Map[String, String], sortParams: List[String] = List.empty) = { val navs = navParams ++ sortParams params.filterKeys(x => !navs.contains(x)) diff --git a/app/org/tronscan/api/BlockApi.scala b/app/org/tronscan/api/BlockApi.scala index 7e2eec6..8e67264 100644 --- a/app/org/tronscan/api/BlockApi.scala +++ b/app/org/tronscan/api/BlockApi.scala @@ -73,7 +73,7 @@ class BlockApi @Inject() ( val queryParams = request.queryString.map(x => x._1.toLowerCase -> x._2.mkString) val queryHash = queryParams.map(x => x._1 + "-" + x._2).mkString val filterHash = stripNav(queryParams).map(x => x._1 + "-" + x._2).mkString - val includeCount = request.getQueryString("count").exists(x => true) + val includeCount = request.getQueryString("count").isDefined def getBlocks = { @@ -146,10 +146,10 @@ class BlockApi @Inject() ( size = block.toByteArray.length, hash = block.hash, timestamp = new DateTime(header.timestamp), - txTrieRoot = Base58.encode58Check(header.txTrieRoot.toByteArray), + txTrieRoot = ByteUtil.toHexString(header.txTrieRoot.toByteArray), parentHash = ByteUtil.toHexString(header.parentHash.toByteArray), witnessId = header.witnessId, - witnessAddress = Base58.encode58Check(header.witnessAddress.toByteArray), + witnessAddress = header.witnessAddress.encodeAddress, nrOfTrx = block.transactions.size, ).asJson) } diff --git a/app/org/tronscan/api/GrpcFullApi.scala b/app/org/tronscan/api/GrpcFullApi.scala index 11fb11b..5e1de92 100644 --- a/app/org/tronscan/api/GrpcFullApi.scala +++ b/app/org/tronscan/api/GrpcFullApi.scala @@ -4,15 +4,15 @@ package tronscan.api import com.google.protobuf.ByteString import io.circe.Json import io.circe.syntax._ -import io.swagger.annotations.Api +import io.swagger.annotations.{Api, ApiImplicitParam, ApiImplicitParams, ApiOperation} import javax.inject.Inject -import org.tron.api.api.{BytesMessage, EmptyMessage, NumberMessage, WalletGrpc} -import org.tron.common.utils.{Base58, ByteArray, ByteUtil} -import org.tron.protos.Tron.Account +import org.tron.api.api._ +import org.tron.common.utils.ByteArray +import org.tronscan.Extensions._ +import org.tronscan.api.models.TransactionSerializer._ import org.tronscan.api.models.{TransactionSerializer, TronModelsSerializers} import org.tronscan.grpc.{GrpcService, WalletClient} import play.api.mvc.Request -import org.tronscan.api.models.TransactionSerializer._ import scala.concurrent.Future @@ -30,6 +30,12 @@ class GrpcFullApi @Inject() ( val serializer = new TronModelsSerializers import serializer._ + /** + * Retrieves a GRPC client + * + * Uses the full node by default, but can be overridden by using the ?ip= parameter + * Uses the 50051 port by default but can be overridden by using the ?port= parameter + */ def getClient(implicit request: Request[_]): Future[WalletGrpc.WalletStub] = { request.getQueryString("ip") match { case Some(ip) => @@ -40,11 +46,12 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve the latest block on") def getNowBlock = Action.async { implicit req => for { - client <- getClient - block <- client.getNowBlock(EmptyMessage()) + block <- walletClient.fullRequest(_.getNowBlock(EmptyMessage())) } yield { Ok(Json.obj( "data" -> block.asJson @@ -52,12 +59,12 @@ class GrpcFullApi @Inject() ( } } - + @ApiOperation( + value = "Retrieve a block by number") def getBlockByNum(number: Long) = Action.async { implicit req => for { - client <- getClient - block <- client.getBlockByNum(NumberMessage(number)) + block <- walletClient.fullRequest(_.getBlockByNum(NumberMessage(number))) } yield { block.blockHeader match { case Some(_) => @@ -70,11 +77,42 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve a range of blocks") + @ApiImplicitParams(Array( + new ApiImplicitParam( + name = "from", + value = "From block", + required = true, + dataType = "long", + paramType = "query"), + new ApiImplicitParam( + name = "to", + value = "To block", + required = true, + dataType = "long", + paramType = "query"), + )) + def getBlockByLimitNext = Action.async { implicit req => + + val from = req.getQueryString("from").get.toLong + val to = req.getQueryString("to").get.toLong + + for { + blocks <- walletClient.fullRequest(_.getBlockByLimitNext(BlockLimit(from, to))) + } yield { + Ok(Json.obj( + "data" -> blocks.block.sortBy(_.getBlockHeader.getRawData.number).toList.asJson + )) + } + } + + @ApiOperation( + value = "Retrieve a transaction by the transaction hash") def getTransactionById(hash: String) = Action.async { implicit req => for { - client <- getClient - transaction <- client.getTransactionById(BytesMessage(ByteString.copyFrom(ByteArray.fromHexString(hash)))) + transaction <- walletClient.fullRequest(_.getTransactionById(BytesMessage(ByteString.copyFrom(ByteArray.fromHexString(hash))))) } yield { Ok(Json.obj( "data" -> TransactionSerializer.serialize(transaction) @@ -82,11 +120,12 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve total number of transactions") def totalTransaction = Action.async { implicit req => for { - client <- getClient - total <- client.totalTransaction(EmptyMessage()) + total <- walletClient.fullRequest(_.totalTransaction(EmptyMessage())) } yield { Ok(Json.obj( "data" -> total.num.asJson @@ -94,13 +133,12 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve account by address") def getAccount(address: String) = Action.async { implicit req => for { - client <- getClient - account <- client.getAccount(Account( - address = ByteString.copyFrom(Base58.decode58Check(address)) - )) + account <- walletClient.fullRequest(_.getAccount(address.toAccount)) } yield { Ok(Json.obj( "data" -> account.asJson @@ -108,13 +146,12 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve bandwidth information for the given account") def getAccountNet(address: String) = Action.async { implicit req => for { - client <- getClient - accountNet <- client.getAccountNet(Account( - address = ByteString.copyFrom(Base58.decode58Check(address)) - )) + accountNet <- walletClient.fullRequest(_.getAccountNet(address.toAccount)) } yield { Ok(Json.obj( "data" -> accountNet.asJson @@ -122,12 +159,12 @@ class GrpcFullApi @Inject() ( } } - + @ApiOperation( + value = "Retrieve nodes") def listNodes = Action.async { implicit req => for { - client <- getClient - nodes <- client.listNodes(EmptyMessage()) + nodes <- walletClient.fullRequest(_.listNodes(EmptyMessage())) } yield { Ok(Json.obj( "data" -> nodes.nodes.map(node => Json.obj( @@ -137,15 +174,56 @@ class GrpcFullApi @Inject() ( } } + @ApiOperation( + value = "Retrieve all witnesses") def listWitnesses = Action.async { implicit req => for { - client <- getClient - witnessList <- client.listWitnesses(EmptyMessage()) + witnessList <- walletClient.fullRequest(_.listWitnesses(EmptyMessage())) } yield { Ok(Json.obj( "data" -> witnessList.witnesses.map(_.asJson).asJson )) } } + + @ApiOperation( + value = "Retrieve all proposals") + def listProposals = Action.async { implicit req => + + for { + proposalList <- walletClient.fullRequest(_.listProposals(EmptyMessage())) + } yield { + Ok(Json.obj( + "data" -> proposalList.proposals.map(_.asJson).asJson + )) + } + } + + @ApiOperation( + value = "Retrieve exchanges") + def listExchanges = Action.async { implicit req => + + for { + exchangeList <- walletClient.fullRequest(_.listExchanges(EmptyMessage())) + } yield { + Ok(Json.obj( + "data" -> exchangeList.exchanges.map(_.asJson).asJson + )) + } + } + + @ApiOperation( + value = "Retrieve blockchain parameters") + def getChainParameters = Action.async { implicit req => + + for { + chainParameters <- walletClient.fullRequest(_.getChainParameters(EmptyMessage())) + } yield { + Ok(Json.obj( + "data" -> chainParameters.chainParameter.map(_.asJson).asJson + )) + } + } + } diff --git a/app/org/tronscan/api/SystemApi.scala b/app/org/tronscan/api/SystemApi.scala index 7bd31f2..f053de7 100644 --- a/app/org/tronscan/api/SystemApi.scala +++ b/app/org/tronscan/api/SystemApi.scala @@ -2,7 +2,7 @@ package org.tronscan.api import akka.actor.ActorRef import javax.inject.{Inject, Named} -import org.tron.api.api.EmptyMessage +import org.tron.api.api.{BlockLimit, EmptyMessage, WalletGrpc} import org.tron.api.api.WalletSolidityGrpc.WalletSolidity import play.api.cache.Cached import play.api.inject.ConfigurationProvider @@ -16,6 +16,7 @@ import scala.concurrent.Future import scala.concurrent.duration._ import akka.pattern.ask import akka.util.Timeout +import io.grpc.ManagedChannelBuilder class SystemApi @Inject()( walletClient: WalletClient, @@ -83,4 +84,34 @@ class SystemApi @Inject()( } } } +// +// def test = Action.async { +// import scala.concurrent.ExecutionContext.Implicits.global +// +// val channel = ManagedChannelBuilder +// .forAddress("47.254.146.147", 50051) +// .usePlainText() +// .build +// +// val wallet = WalletGrpc.stub(channel) +// +// val blocks = for (i <- 1 to 500 by 50) yield i +// +// val done = Future.sequence(blocks.sliding(2).toList.map { case Seq(now, next) => +// wallet.getBlockByLimitNext(BlockLimit(now, next)).map { result => +// val resultBlocks = result.block.sortBy(_.getBlockHeader.getRawData.number).map(_.getBlockHeader.getRawData.number) +// if (resultBlocks.head != now) { +// println(s"INVALID HEAD ${resultBlocks.head} => $now") +// } +// if (resultBlocks.last != next) { +// println(s"INVALID HEAD ${resultBlocks.last} => $next") +// } +// resultBlocks +// } +// }) +// +// done.map { _ => +// Ok("done") +// } +// } } diff --git a/app/org/tronscan/api/TransactionApi.scala b/app/org/tronscan/api/TransactionApi.scala index 75a378c..ac7b1a0 100644 --- a/app/org/tronscan/api/TransactionApi.scala +++ b/app/org/tronscan/api/TransactionApi.scala @@ -8,6 +8,7 @@ import javax.inject.Inject import org.joda.time.DateTime import org.tron.common.utils.ByteArray import org.tron.protos.Tron.Transaction +import org.tron.protos.Tron.Transaction.Contract.ContractType import org.tronscan.api.models.TransactionSerializer import org.tronscan.db.PgProfile.api._ import org.tronscan.grpc.WalletClient @@ -53,7 +54,7 @@ class TransactionApi @Inject()( paramType = "query"), )) @ApiOperation( - value = "List Transfers", + value = "Retrieve transactions", response = classOf[TransactionModel], responseContainer = "List") def findAll() = Action.async { implicit request => @@ -102,6 +103,10 @@ class TransactionApi @Inject()( query.filter(x => x.timestamp <= dateStart) case (query, ("contract_type", value)) => query.filter(x => x.contractType === value.toInt) + case (query, ("token", value)) if value.toUpperCase == "TRX" => + query.filter(x => x.contractType === ContractType.TransferContract.value) + case (query, ("token", value)) => + query.filter(x => x.contractType === ContractType.TransferAssetContract.value && x.contractData.+>>("token") === value) case (query, _) => query } diff --git a/app/org/tronscan/api/TransactionBuilderApi.scala b/app/org/tronscan/api/TransactionBuilderApi.scala index d2021d0..d4446e7 100644 --- a/app/org/tronscan/api/TransactionBuilderApi.scala +++ b/app/org/tronscan/api/TransactionBuilderApi.scala @@ -7,7 +7,7 @@ import io.circe.{Decoder, Json} import io.swagger.annotations._ import javax.inject.Inject import org.tron.common.utils.ByteArray -import org.tron.protos.Contract.{AccountCreateContract, AccountUpdateContract, TransferAssetContract, TransferContract} +import org.tron.protos.Contract.{AccountCreateContract, AccountUpdateContract, TransferAssetContract, TransferContract, _} import org.tron.protos.Tron.Transaction import org.tron.protos.Tron.Transaction.Contract.ContractType import org.tronscan.Extensions._ @@ -17,20 +17,7 @@ import org.tronscan.service.TransactionBuilder import play.api.mvc.{AnyContent, Request, Result} import scala.concurrent.Future -import io.circe.syntax._ -import org.tron.protos.Contract._ -import org.tron.protos.Tron.Transaction.Contract.ContractType -import scalapb.Message - -case class TransactionAction( - contract: Transaction.Contract, - broadcast: Boolean, - key: Option[String] = None, - data: Option[String] = None, -) - -case class Signature(pk: String) @Api( value = "Transaction Builder", @@ -39,6 +26,13 @@ class TransactionBuilderApi @Inject()( transactionBuilder: TransactionBuilder, walletClient: WalletClient) extends BaseApi { + + case class TransactionAction( + contract: Transaction.Contract, + broadcast: Boolean, + key: Option[String] = None, + data: Option[String] = None) + import TransactionSerializer._ import scala.concurrent.ExecutionContext.Implicits.global @@ -76,6 +70,16 @@ class TransactionBuilderApi @Inject()( Transaction.Contract( `type` = ContractType.WithdrawBalanceContract, parameter = Some(Any.pack(c.asInstanceOf[WithdrawBalanceContract]))) + + case c: ProposalApproveContract => + Transaction.Contract( + `type` = ContractType.ProposalApproveContract, + parameter = Some(Any.pack(c.asInstanceOf[ProposalApproveContract]))) + + case c: UpdateAssetContract => + Transaction.Contract( + `type` = ContractType.UpdateAssetContract, + parameter = Some(Any.pack(c.asInstanceOf[UpdateAssetContract]))) } TransactionAction(transactionContract, broadcast.getOrElse(false), key, json.hcursor.downField("data").as[String].toOption) @@ -126,31 +130,87 @@ class TransactionBuilderApi @Inject()( @ApiOperation( value = "Build TransferContract") + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.TransferTransaction", + paramType = "body"), + )) def transfer = Action.async { implicit req => handleTransaction[org.tron.protos.Contract.TransferContract]() } @ApiOperation( value = "Build TransferAssetContract" ) + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.TransferAssetTransaction", + paramType = "body"), + )) def transferAsset = Action.async { implicit req => handleTransaction[org.tron.protos.Contract.TransferAssetContract]() } @ApiOperation( value = "Build AccountCreateContract" ) + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.AccountCreateTransaction", + paramType = "body"), + )) def accountCreate = Action.async { implicit req => handleTransaction[org.tron.protos.Contract.AccountCreateContract]() } @ApiOperation( value = "Build AccountUpdateContract" ) + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.AccountUpdateTransaction", + paramType = "body"), + )) def accountUpdate = Action.async { implicit req => handleTransaction[org.tron.protos.Contract.AccountUpdateContract]() } @ApiOperation( value = "Build WithdrawBalancecontract" ) + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.WithdrawBalanceTransaction", + paramType = "body"), + )) def withdrawBalance = Action.async { implicit req => handleTransaction[org.tron.protos.Contract.WithdrawBalanceContract]() } + + @ApiOperation( + value = "Build ProposalApproveContract") + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.ProposalApproveTransaction", + paramType = "body"), + )) + def proposalApprove = Action.async { implicit req => + handleTransaction[org.tron.protos.Contract.ProposalApproveContract]() + } + + @ApiOperation( + value = "Build UpdateAssetContract") + @ApiImplicitParams(Array( + new ApiImplicitParam( + required = true, + dataType = "org.tronscan.api.models.UpdateAssetTransaction", + paramType = "body"), + )) + def updateAsset = Action.async { implicit req => + handleTransaction[org.tron.protos.Contract.UpdateAssetContract]() + } } + + diff --git a/app/org/tronscan/api/VoteApi.scala b/app/org/tronscan/api/VoteApi.scala index fd02cdc..f865c1d 100644 --- a/app/org/tronscan/api/VoteApi.scala +++ b/app/org/tronscan/api/VoteApi.scala @@ -4,14 +4,10 @@ package tronscan.api import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ -import io.circe.generic.auto._ -import io.circe.syntax._ import javax.inject.Inject import org.joda.time.DateTime import org.tron.api.api.EmptyMessage import org.tron.api.api.WalletSolidityGrpc.WalletSolidity -import org.tronscan.App._ -import org.tronscan.actions.VoteList import org.tronscan.actions.VoteList import org.tronscan.db.PgProfile.api._ import org.tronscan.domain.Constants @@ -20,8 +16,6 @@ import org.tronscan.models._ import play.api.cache.redis.CacheAsyncApi import play.api.cache.{Cached, NamedCache} import play.api.mvc.InjectedController -import play.api.cache.{Cached, NamedCache} -import play.api.mvc.InjectedController import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -36,6 +30,8 @@ class VoteApi @Inject()( voteSnapshotModelRepository: VoteSnapshotModelRepository, walletSolidity: WalletSolidity, voteList: VoteList, + roundVoteModelRepository: RoundVoteModelRepository, + maintenanceRoundModelRepository: MaintenanceRoundModelRepository, @NamedCache("redis") redisCache: CacheAsyncApi) extends InjectedController { def findAll() = Action.async { implicit request => @@ -134,4 +130,49 @@ class VoteApi @Inject()( )) } } -} \ No newline at end of file + + /** + * Retrieves a single voting round + */ + def round(id: Int) = Action.async { + maintenanceRoundModelRepository.findByNumber(id).map { + case Some(round) => + Ok(round.asJson) + case _ => + NotFound + } + } + + /** + * Retrieve the votes for the given round + */ + def roundVotes(id: Int) = Action.async { implicit req => + + import roundVoteModelRepository._ + + var q = sortWithRequest() { + case (t, "votes") => t.votes + } + + q = q andThen { _.filter(_.round === id) } + + q = q andThen filterRequest { + case (query, ("voter", value)) => + query.filter(_.address === value) + case (query, ("candidate", value)) => + query.filter(_.candidate === value) + case (query, _) => + query + } + + for { + total <- readTotals(q) + accounts <- readQuery(q andThen limitWithRequest()) + } yield { + Ok(Json.obj( + "total" -> total.asJson, + "data" -> accounts.asJson + )) + } + } +} diff --git a/app/org/tronscan/api/models/TransactionBuilder.scala b/app/org/tronscan/api/models/TransactionBuilder.scala new file mode 100644 index 0000000..43d9c78 --- /dev/null +++ b/app/org/tronscan/api/models/TransactionBuilder.scala @@ -0,0 +1,79 @@ +/* + * Models which are shown in the Swagger API for the TransactionBuilder will be listed here + */ + +package org.tronscan.api.models + +import io.swagger.annotations.ApiModelProperty + + +// Approve Proposal +case class ProposalApproveTransaction( + contract: ProposalApprove) extends TransactionCreateBase + +case class ProposalApprove( + ownerAddress: String, + @ApiModelProperty(value = "ID of the proposal") + proposalId: Long, + @ApiModelProperty(value = "true to approve, false to reject") + approved: Boolean) + + +// Transfer +case class TransferTransaction( + contract: Transfer) extends TransactionCreateBase + +case class Transfer( + ownerAddress: String, + toAddress: String, + @ApiModelProperty(value = "Amount of TRX to send in Sun") + amount: Long) + +// Transfer Asset +case class TransferAssetTransaction( + contract: TransferAsset) extends TransactionCreateBase + +case class TransferAsset( + ownerAddress: String, + toAddress: String, + @ApiModelProperty(value = "Name of the token") + assetName: String, + @ApiModelProperty(value = "Amount of tokens to send") + amount: Long) + + +// Withdraw Balance +case class WithdrawBalanceTransaction( + contract: WithdrawBalance) extends TransactionCreateBase + +case class WithdrawBalance( + ownerAddress: String) + +// Account Update +case class AccountUpdateTransaction( + contract: AccountUpdate) extends TransactionCreateBase + +case class AccountUpdate( + ownerAddress: String, + accountName: String) + +// Account Create +case class AccountCreateTransaction( + contract: AccountCreate) extends TransactionCreateBase + +case class AccountCreate( + ownerAddress: String, + accountAddress: String) + + +// Update Asset +case class UpdateAssetTransaction( + contract: UpdateAsset) extends TransactionCreateBase + +case class UpdateAsset( + ownerAddress: String, + url: String, + description: String, + newLimit: Option[Long], + newPublicLimit: Option[Long]) + diff --git a/app/org/tronscan/api/models/TransactionCreate.scala b/app/org/tronscan/api/models/TransactionCreate.scala new file mode 100644 index 0000000..3e743ff --- /dev/null +++ b/app/org/tronscan/api/models/TransactionCreate.scala @@ -0,0 +1,32 @@ +package org.tronscan.api.models + +import io.swagger.annotations.{ApiModel, ApiModelProperty} +import org.tron.protos.Tron.Transaction + +@ApiModel( + value="TransactionCreate") +case class TransactionCreate( + @ApiModelProperty(value = "Contract") + contract: Any, + + @ApiModelProperty(value = "If the transaction should be broadcast to the network") + broadcast: Boolean, + + @ApiModelProperty(value = "Private Key to sign the transaction") + key: Option[String] = None, + + @ApiModelProperty(value = "Optional extra data to attach to the transaction") + data: Option[String] = None +) + + +trait TransactionCreateBase { + @ApiModelProperty(value = "If the transaction should be broadcast to the network") + val broadcast: Boolean = false + + @ApiModelProperty(value = "Private Key to sign the transaction") + val key: Option[String] = None + + @ApiModelProperty(value = "Optional extra data to attach to the transaction") + val data: Option[String] = None +} \ No newline at end of file diff --git a/app/org/tronscan/api/models/TransactionSerializer.scala b/app/org/tronscan/api/models/TransactionSerializer.scala index 003098b..50f240d 100644 --- a/app/org/tronscan/api/models/TransactionSerializer.scala +++ b/app/org/tronscan/api/models/TransactionSerializer.scala @@ -1,13 +1,11 @@ package org package tronscan.api.models -import com.google.protobuf.ByteString import io.circe.syntax._ -import io.circe.{Decoder, Encoder, HCursor, Json => Js} +import io.circe.{Decoder, DecodingFailure, Encoder, HCursor, Json => Js} import org.joda.time.DateTime -import org.tron.common.crypto.ECKey -import org.tron.common.utils.{Base58, ByteArray, Crypto, Sha256Hash} -import org.tron.protos.Tron.Transaction.Contract.ContractType.{AccountCreateContract, AccountUpdateContract, AssetIssueContract, DeployContract, FreezeBalanceContract, ParticipateAssetIssueContract, TransferAssetContract, TransferContract, UnfreezeAssetContract, UnfreezeBalanceContract, UpdateAssetContract, VoteAssetContract, VoteWitnessContract, WithdrawBalanceContract, WitnessCreateContract, WitnessUpdateContract} +import org.tron.common.utils.{Base58, ByteArray, Crypto} +import org.tron.protos.Tron.Transaction.Contract.ContractType.{AccountCreateContract, AccountUpdateContract, AssetIssueContract, CreateSmartContract, ExchangeCreateContract, ExchangeInjectContract, ExchangeTransactionContract, ExchangeWithdrawContract, FreezeBalanceContract, ParticipateAssetIssueContract, ProposalApproveContract, ProposalCreateContract, ProposalDeleteContract, TransferAssetContract, TransferContract, TriggerSmartContract, UnfreezeAssetContract, UnfreezeBalanceContract, UpdateAssetContract, VoteAssetContract, VoteWitnessContract, WithdrawBalanceContract, WitnessCreateContract, WitnessUpdateContract} import org.tron.protos.Tron.{AccountType, Transaction} import org.tronscan.Extensions._ import org.tronscan.protocol.MainNetFormatter @@ -47,6 +45,24 @@ object TransactionSerializer { ) } + implicit val decodeUpdateAssetContract = new Decoder[org.tron.protos.Contract.UpdateAssetContract] { + def apply(c: HCursor) = { + for { + from <- c.downField("ownerAddress").as[String] + description <- c.downField("description").as[String] + url <- c.downField("url").as[String] + } yield { + org.tron.protos.Contract.UpdateAssetContract( + ownerAddress = from.decodeAddress, + description = description.encodeString, + url = url.encodeString, + newLimit = c.downField("newLimit").as[Long].getOrElse(0L), + newPublicLimit = c.downField("newPublicLimit").as[Long].getOrElse(0L) + ) + } + } + } + implicit val encodeTransferContract = new Encoder[org.tron.protos.Contract.TransferContract] { def apply(transferContract: org.tron.protos.Contract.TransferContract): Js = Js.obj( "from" -> Base58.encode58Check(transferContract.ownerAddress.toByteArray).asJson, @@ -134,6 +150,30 @@ object TransactionSerializer { ) } + implicit val decodeVoteWitnessContractVote = new Decoder[org.tron.protos.Contract.VoteWitnessContract.Vote] { + def apply(c: HCursor) = { + for { + voteAddress <- c.downField("voteAddress").as[String] + voteCount <- c.downField("voteCount").as[Long] + } yield org.tron.protos.Contract.VoteWitnessContract.Vote( + voteAddress = voteAddress.decodeAddress, + voteCount = voteCount + ) + } + } + + implicit val decodeVoteWitnessContract = new Decoder[org.tron.protos.Contract.VoteWitnessContract] { + def apply(c: HCursor) = { + for { + ownerAddress <- c.downField("ownerAddress").as[String] + votes <- c.downField("votes").as[List[org.tron.protos.Contract.VoteWitnessContract.Vote]] + } yield org.tron.protos.Contract.VoteWitnessContract( + ownerAddress = ownerAddress.decodeAddress, + votes = votes + ) + } + } + implicit val encodeAccountUpdateContract = new Encoder[org.tron.protos.Contract.AccountUpdateContract] { def apply(accountUpdateContract: org.tron.protos.Contract.AccountUpdateContract): Js = Js.obj( "ownerAddress" -> accountUpdateContract.ownerAddress.encodeAddress.asJson, @@ -216,14 +256,129 @@ object TransactionSerializer { implicit val encodeUnfreezeAssetContract = new Encoder[org.tron.protos.Contract.UnfreezeAssetContract] { def apply(contract: org.tron.protos.Contract.UnfreezeAssetContract): Js = Js.obj( - "ownerAddress" -> Base58.encode58Check(contract.ownerAddress.toByteArray).asJson, + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, ) } - implicit val encodeDeployContract = new Encoder[org.tron.protos.Contract.DeployContract] { - def apply(contract: org.tron.protos.Contract.DeployContract): Js = Js.obj( - "ownerAddress" -> Base58.encode58Check(contract.ownerAddress.toByteArray).asJson, - "script" -> ByteArray.toHexString(contract.script.toByteArray).asJson, + + implicit val encodeProposal = new Encoder[org.tron.protos.Tron.Proposal] { + def apply(contract: org.tron.protos.Tron.Proposal): Js = Js.obj( + "proposalId" -> contract.proposalId.asJson, + "proposerAddress" -> contract.proposerAddress.encodeAddress.asJson, + "parameters" -> contract.parameters.asJson, + "expirationTime" -> contract.expirationTime.asJson, + "createTime" -> contract.createTime.asJson, + "approvals" -> contract.approvals.map(_.encodeAddress).asJson, + "state" -> contract.state.name.asJson, + ) + } + + implicit val encodeExchange = new Encoder[org.tron.protos.Tron.Exchange] { + def apply(contract: org.tron.protos.Tron.Exchange): Js = Js.obj( + "exchangeId" -> contract.exchangeId.asJson, + "creatorAddress" -> contract.creatorAddress.encodeAddress.asJson, + "createTime" -> contract.createTime.asJson, + "firstTokenId" -> contract.firstTokenId.decodeString.asJson, + "firstTokenBalance" -> contract.firstTokenBalance.asJson, + "secondTokenId" -> contract.secondTokenId.decodeString.asJson, + "secondTokenBalance" -> contract.secondTokenBalance.asJson, + ) + } + + implicit val encodeChainParameter = new Encoder[org.tron.protos.Tron.ChainParameters.ChainParameter] { + def apply(contract: org.tron.protos.Tron.ChainParameters.ChainParameter): Js = Js.obj( + "key" -> contract.key.asJson, + "value" -> contract.value.asJson, + ) + } + + implicit val encodeProposalCreateContract = new Encoder[org.tron.protos.Contract.ProposalCreateContract] { + def apply(contract: org.tron.protos.Contract.ProposalCreateContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "parameters" -> contract.parameters.asJson, + ) + } + + implicit val encodeProposalApproveContract = new Encoder[org.tron.protos.Contract.ProposalApproveContract] { + def apply(contract: org.tron.protos.Contract.ProposalApproveContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "proposalId" -> contract.proposalId.asJson, + "isAddApproval" -> contract.isAddApproval.asJson, + ) + } + + implicit val decodeProposalApproveContract = new Decoder[org.tron.protos.Contract.ProposalApproveContract] { + def apply(c: HCursor) = { + for { + ownerAddress <- c.downField("ownerAddress").as[String] + proposalId <- c.downField("proposalId").as[Long] + isAddApproval <- c.downField("approve").as[Boolean] + } yield { + org.tron.protos.Contract.ProposalApproveContract( + ownerAddress = ownerAddress.decodeAddress, + proposalId = proposalId, + isAddApproval = isAddApproval + ) + } + } + } + + implicit val encodeProposalDeleteContract = new Encoder[org.tron.protos.Contract.ProposalDeleteContract] { + def apply(contract: org.tron.protos.Contract.ProposalDeleteContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "proposalId" -> contract.proposalId.asJson, + ) + } + + implicit val encodeCreateSmartContract = new Encoder[org.tron.protos.Contract.CreateSmartContract] { + def apply(contract: org.tron.protos.Contract.CreateSmartContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + ) + } + + implicit val encodeTriggerSmartContract = new Encoder[org.tron.protos.Contract.TriggerSmartContract] { + def apply(contract: org.tron.protos.Contract.TriggerSmartContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "contractAddress" -> contract.contractAddress.encodeAddress.asJson, + "callValue" -> contract.callValue.asJson, + "data" -> ByteArray.toHexString(contract.data.toByteArray).asJson, + ) + } + + implicit val encodeExchangeCreateContract = new Encoder[org.tron.protos.Contract.ExchangeCreateContract] { + def apply(contract: org.tron.protos.Contract.ExchangeCreateContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "firstTokenId" -> contract.firstTokenId.decodeString.asJson, + "firstTokenBalance" -> contract.firstTokenBalance.asJson, + "secondTokenId" -> contract.secondTokenId.decodeString.asJson, + "secondTokenBalance" -> contract.secondTokenBalance.asJson, + ) + } + + implicit val encodeExchangeInjectContract = new Encoder[org.tron.protos.Contract.ExchangeInjectContract] { + def apply(contract: org.tron.protos.Contract.ExchangeInjectContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "exchangeId" -> contract.exchangeId.asJson, + "tokenId" -> contract.tokenId.decodeString.asJson, + "quant" -> contract.quant.asJson, + ) + } + + implicit val encodeExchangeWithdrawContract = new Encoder[org.tron.protos.Contract.ExchangeWithdrawContract] { + def apply(contract: org.tron.protos.Contract.ExchangeWithdrawContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "exchangeId" -> contract.exchangeId.asJson, + "tokenId" -> contract.tokenId.decodeString.asJson, + "quant" -> contract.quant.asJson, + ) + } + + implicit val encodeExchangeTransactionContract = new Encoder[org.tron.protos.Contract.ExchangeTransactionContract] { + def apply(contract: org.tron.protos.Contract.ExchangeTransactionContract): Js = Js.obj( + "ownerAddress" -> contract.ownerAddress.encodeAddress.asJson, + "exchangeId" -> contract.exchangeId.asJson, + "tokenId" -> contract.tokenId.decodeString.asJson, + "quant" -> contract.quant.asJson, ) } @@ -247,9 +402,6 @@ object TransactionSerializer { case AssetIssueContract => org.tron.protos.Contract.AssetIssueContract.parseFrom(contract.getParameter.value.toByteArray).asJson - case DeployContract => - org.tron.protos.Contract.DeployContract.parseFrom(contract.getParameter.value.toByteArray).asJson - case ParticipateAssetIssueContract => org.tron.protos.Contract.ParticipateAssetIssueContract.parseFrom(contract.getParameter.value.toByteArray).asJson @@ -277,10 +429,44 @@ object TransactionSerializer { case UpdateAssetContract => org.tron.protos.Contract.UpdateAssetContract.parseFrom(contract.getParameter.value.toByteArray).asJson + case ProposalCreateContract => + org.tron.protos.Contract.ProposalCreateContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case ProposalApproveContract => + org.tron.protos.Contract.ProposalApproveContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case ProposalDeleteContract => + org.tron.protos.Contract.ProposalDeleteContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case CreateSmartContract => + org.tron.protos.Contract.CreateSmartContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case TriggerSmartContract => + org.tron.protos.Contract.TriggerSmartContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + // case BuyStorageBytesContract => + // org.tron.protos.Contract.BuyStorageBytesContract.parseFrom(any.value.toByteArray) + + // case BuyStorageContract => + // org.tron.protos.Contract.BuyStorageContract.parseFrom(any.value.toByteArray) + + // case SellStorageContract => + // org.tron.protos.Contract.SellStorageContract.parseFrom(any.value.toByteArray) + + case ExchangeCreateContract => + org.tron.protos.Contract.ExchangeCreateContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case ExchangeInjectContract => + org.tron.protos.Contract.ExchangeInjectContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case ExchangeWithdrawContract => + org.tron.protos.Contract.ExchangeWithdrawContract.parseFrom(contract.getParameter.value.toByteArray).asJson + + case ExchangeTransactionContract => + org.tron.protos.Contract.ExchangeTransactionContract.parseFrom(contract.getParameter.value.toByteArray).asJson + case _ => - Js.obj( - "type" -> contract.`type`.toString().asJson - ) + Js.obj() } @@ -304,7 +490,7 @@ object TransactionSerializer { "signatures" -> transaction.signature.map { signature => Js.obj( "bytes" -> Crypto.getBase64FromByteString(signature).asJson, - "address" -> ByteString.copyFrom(ECKey.signatureToAddress(Sha256Hash.of(transaction.getRawData.toByteArray).getBytes, Crypto.getBase64FromByteString(transaction.signature(0)))).encodeAddress.asJson, +// "address" -> ByteString.copyFrom(ECKey.signatureToAddress(Sha256Hash.of(transaction.getRawData.toByteArray).getBytes, Crypto.getBase64FromByteString(transaction.signature(0)))).encodeAddress.asJson, ) }.asJson, ) diff --git a/app/org/tronscan/db/Repository.scala b/app/org/tronscan/db/Repository.scala index 8f35aec..3d168f9 100644 --- a/app/org/tronscan/db/Repository.scala +++ b/app/org/tronscan/db/Repository.scala @@ -63,7 +63,6 @@ case class EntityModel(id: Option[Int]) extends Entity * @tparam T Entity type */ abstract class EntityTable[T <: Entity](tag: Tag, tableName: String) extends Table[T](tag, tableName) { - def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc) } @@ -127,7 +126,6 @@ trait TableRepository[T <: Table[E], E <: Any] extends Repository { func(table).length.result } - def sortWithRequest(sortParam: String = "sort")(sorter: PartialFunction[(T, String), Rep[_ <: Any]]) (implicit request: Request[AnyContent]): QueryType => QueryType = { query: QueryType => (for { diff --git a/app/org/tronscan/grpc/GrpcBalancer.scala b/app/org/tronscan/grpc/GrpcBalancer.scala index 2ff57a2..6b15490 100644 --- a/app/org/tronscan/grpc/GrpcBalancer.scala +++ b/app/org/tronscan/grpc/GrpcBalancer.scala @@ -1,5 +1,6 @@ package org.tronscan.grpc +import java.util.UUID import java.util.concurrent.TimeUnit import akka.actor.{Actor, ActorRef, Cancellable, Props, Terminated} @@ -16,9 +17,9 @@ import scala.collection.JavaConverters._ import scala.concurrent.Future import scala.concurrent.duration._ -case class GrpcRequest(request: WalletStub => Future[Any]) -case class GrpcRetry(request: GrpcRequest) -case class GrpcResponse(response: Any) +case class GrpcRequest(request: WalletStub => Future[Any], id: UUID) +case class GrpcRetry(request: GrpcRequest, sender: ActorRef) +case class GrpcResponse(response: Any, id: UUID, ip: NodeAddress) case class GrpcBlock(num: Long, hash: String) case class OptimizeNodes() case class GrpcBalancerStats( @@ -110,7 +111,7 @@ class GrpcBalancer @Inject() (configurationProvider: ConfigurationProvider) exte override def preStart(): Unit = { import context.dispatcher - pinger = Some(context.system.scheduler.schedule(6.second, 6.seconds, self, OptimizeNodes())) + pinger = Some(context.system.scheduler.schedule(4.second, 6.seconds, self, OptimizeNodes())) } override def postStop(): Unit = { @@ -120,8 +121,8 @@ class GrpcBalancer @Inject() (configurationProvider: ConfigurationProvider) exte def receive = { case w: GrpcRequest ⇒ router.route(w, sender()) - case GrpcRetry(request) => - router.route(request, sender()) + case GrpcRetry(request, s) => + router.route(request, s) case stats: GrpcStats => nodeStatuses = nodeStatuses ++ Map(stats.ip -> stats) case Terminated(a) ⇒ @@ -145,7 +146,7 @@ class GrpcClient(nodeAddress: NodeAddress) extends Actor { lazy val channel = ManagedChannelBuilder .forAddress(nodeAddress.ip, nodeAddress.port) - .usePlaintext(true) + .usePlaintext() .build lazy val walletStub = { @@ -193,16 +194,15 @@ class GrpcClient(nodeAddress: NodeAddress) extends Actor { } } - def handleRequest(request: GrpcRequest) = { + def handleRequest(request: GrpcRequest, s: ActorRef) = { import context.dispatcher - val s = sender() request.request(walletStub.withDeadlineAfter(5, TimeUnit.SECONDS)).map { x => - s ! GrpcResponse(x) + s ! GrpcResponse(x, request.id, nodeAddress) requestsHandled += 1 }.recover { case _ => requestErrors += 1 - context.parent.tell(GrpcRetry(request), s) + context.parent.tell(GrpcRetry(request, s), s) } } @@ -217,7 +217,7 @@ class GrpcClient(nodeAddress: NodeAddress) extends Actor { def receive = { case c: GrpcRequest => - handleRequest(c) + handleRequest(c, sender()) case "ping" => ping() diff --git a/app/org/tronscan/grpc/WalletClient.scala b/app/org/tronscan/grpc/WalletClient.scala index 853d074..fb52308 100644 --- a/app/org/tronscan/grpc/WalletClient.scala +++ b/app/org/tronscan/grpc/WalletClient.scala @@ -1,5 +1,7 @@ package org.tronscan.grpc +import java.util.UUID + import akka.actor.ActorRef import akka.util import javax.inject.{Inject, Named, Singleton} @@ -9,6 +11,7 @@ import org.tronscan.grpc.GrpcPool.{Channel, RequestChannel} import akka.pattern.ask import akka.util.Timeout import org.tron.api.api.WalletGrpc.WalletStub +import play.api.Logger import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -29,9 +32,9 @@ class WalletClient @Inject() ( (grpcPool ? RequestChannel(ip, port)).mapTo[Channel].map(c => WalletGrpc.stub(c.channel)) } - def fullRequest[A](request: WalletStub => Future[A]) = { - implicit val timeout = Timeout(9.seconds) - (grpcBalancer ? GrpcRequest(request)).mapTo[GrpcResponse].map(_.response.asInstanceOf[A]) + def fullRequest[A](request: WalletStub => Future[A], id: UUID = UUID.randomUUID()) = { + implicit val timeout = Timeout(3.seconds) + (grpcBalancer ? GrpcRequest(request, id)).mapTo[GrpcResponse].map(_.response.asInstanceOf[A]) } def solidity = { diff --git a/app/org/tronscan/importer/BlockChainStreamBuilder.scala b/app/org/tronscan/importer/BlockChainStreamBuilder.scala index 09c6caa..e0e61ac 100644 --- a/app/org/tronscan/importer/BlockChainStreamBuilder.scala +++ b/app/org/tronscan/importer/BlockChainStreamBuilder.scala @@ -1,5 +1,7 @@ package org.tronscan.importer +import java.util.UUID + import akka.NotUsed import akka.event.EventStream import akka.stream.OverflowStrategy @@ -14,6 +16,7 @@ import org.tronscan.grpc.WalletClient import org.tronscan.importer.StreamTypes.ContractFlow import org.tronscan.models._ import org.tronscan.utils.ModelUtils +import play.api.Logger import scala.concurrent.{ExecutionContext, Future} @@ -41,24 +44,36 @@ class BlockChainStreamBuilder { * Reads all the blocks using batch calls */ def readFullNodeBlocksBatched(from: Long, to: Long, batchSize: Int = 50)(client: WalletClient)(implicit executionContext: ExecutionContext): Source[Block, NotUsed] = { - Source.unfold(from) { prev => - if (prev < to) { - - val toBlock = if (prev + batchSize > to) to else prev + batchSize + Source.unfold(from) { fromBlock => + if (fromBlock < to) { - Some((toBlock, (prev, toBlock))) + val nextBlock = fromBlock + batchSize + val toBlock = if (nextBlock <= to) nextBlock else to + Some((toBlock + 1, (fromBlock, toBlock))) } else { None } } - .mapAsync(30) { case (fromBlock, toBlock) => - client.fullRequest(_.getBlockByLimitNext(BlockLimit(fromBlock, toBlock))).map { blocks => - blocks.block.filter(_.blockHeader.isDefined).sortBy(_.getBlockHeader.getRawData.number) + .mapAsync(1) { case (fromBlock, toBlock) => + val range = BlockLimit(fromBlock, toBlock + 1) + client.fullRequest(_.getBlockByLimitNext(range)).map { blocks => + val bs = blocks.block.sortBy(_.getBlockHeader.getRawData.number) +// if (range.startNum != bs.head.getBlockHeader.getRawData.number) { +// Logger.warn(s"WRONG START BLOCK: ${range.startNum} => ${bs.head.getBlockHeader.getRawData.number}") +// } +// if (range.endNum - 1 != bs.last.getBlockHeader.getRawData.number) { +// Logger.warn(s"WRONG END BLOCK: ${range.endNum} => ${bs.last.getBlockHeader.getRawData.number}") +// } +// Logger.info(s"DOWNLOADED $fromBlock to $toBlock. GOT ${bs.headOption.map(_.getBlockHeader.getRawData.number)} => ${bs.lastOption.map(_.getBlockHeader.getRawData.number)}") + bs } } .mapConcat(x => x.toList) - .buffer(50000, OverflowStrategy.backpressure) + .map { block => + Logger.info("Block: " + block.getBlockHeader.getRawData.number) + block + } } def filterContracts(contractTypes: List[Transaction.Contract.ContractType]) = { diff --git a/app/org/tronscan/importer/BlockImporter.scala b/app/org/tronscan/importer/BlockImporter.scala index 804aa22..16ee529 100644 --- a/app/org/tronscan/importer/BlockImporter.scala +++ b/app/org/tronscan/importer/BlockImporter.scala @@ -3,6 +3,7 @@ package org.tronscan.importer import akka.{Done, NotUsed} import akka.stream.scaladsl.{Flow, Keep, Sink, Source} import javax.inject.Inject +import org.joda.time.DateTime import org.tron.protos.Tron.Block import org.tronscan.models._ import org.tronscan.utils.ModelUtils @@ -17,11 +18,12 @@ import scala.concurrent.{ExecutionContext, Future} class BlockImporter @Inject() ( blockModelRepository: BlockModelRepository, + maintenanceRoundModelRepository: MaintenanceRoundModelRepository, transactionModelRepository: TransactionModelRepository, transferRepository: TransferModelRepository, assetIssueContractModelRepository: AssetIssueContractModelRepository, participateAssetIssueModelRepository: ParticipateAssetIssueModelRepository, - databaseImporter: DatabaseImporter) { + databaseImporter: ContractImporter) { /** * Build block importer that imports the full nodes into the database @@ -44,11 +46,11 @@ class BlockImporter @Inject() ( } // Import Block - queries.append(blockModelRepository.buildInsert(BlockModel.fromProto(block).copy(confirmed = confirmBlocks))) + queries.append(blockModelRepository.buildInsertOrUpdate(BlockModel.fromProto(block).copy(confirmed = confirmBlocks))) // Import Transactions queries.appendAll(block.transactions.map { trx => - transactionModelRepository.buildInsert(ModelUtils.transactionToModel(trx, block).copy(confirmed = confirmBlocks)) + transactionModelRepository.buildInsertOrUpdate(ModelUtils.transactionToModel(trx, block).copy(confirmed = confirmBlocks)) }) // Import Contracts @@ -86,12 +88,12 @@ class BlockImporter @Inject() ( .mapAsync(12) { solidityBlock => for { databaseBlock <- blockModelRepository.findByNumber(solidityBlock.getBlockHeader.getRawData.number) - } yield (solidityBlock, databaseBlock.get) + } yield (solidityBlock, databaseBlock) } // Filter empty or confirmed blocks - .filter(x => x._1.blockHeader.isDefined && !x._2.confirmed) + .filter(x => x._1.blockHeader.isDefined && x._2.nonEmpty && !x._2.get.confirmed) .map { - case (solidityBlock, databaseBlock) => + case (solidityBlock, Some(databaseBlock)) => val queries = ListBuffer[FixedSqlAction[_, NoStream, Effect.Write]]() @@ -136,6 +138,8 @@ class BlockImporter @Inject() ( }) queries.toList + case _ => + List.empty } .flatMapConcat(queries => Source(queries)) .groupedWithin(500, 10.seconds) @@ -157,4 +161,41 @@ class BlockImporter @Inject() ( } } + /** + * Builds the importer of voting rounds + */ + def buildVotingRoundImporter(previousVotingRound: Option[MaintenanceRoundModel] = None) = { + val maintenanceRoundTime = 21600000L + + var currentRound = previousVotingRound + + Sink.foreach[Block] { block => + + currentRound match { + case Some(round) => + // The current block timestamp has exceeded the (previous maintenance timestamp + maintenanceRoundTime) then insert a new voting rond + if ((round.timestamp + maintenanceRoundTime) < block.getBlockHeader.getRawData.timestamp) { + val newRound = MaintenanceRoundModel( + block = block.getBlockHeader.getRawData.number, + number = round.number + 1, + timestamp = block.getBlockHeader.getRawData.timestamp, + dateStart = new DateTime(block.getBlockHeader.getRawData.timestamp), + ) + maintenanceRoundModelRepository.insertAsync(newRound) + Logger.info("Next Round: " + newRound.block + " => " + newRound.number) + currentRound = Some(newRound) + } + case _ => + // If there isn't a previous maintenance round then just use the current block as the start of a round + currentRound = Some(MaintenanceRoundModel( + block = block.getBlockHeader.getRawData.number, + number = 1, + timestamp = block.getBlockHeader.getRawData.timestamp, + dateStart = new DateTime(block.getBlockHeader.getRawData.timestamp), + )) + maintenanceRoundModelRepository.insertAsync(currentRound.get) + } + } + } + } diff --git a/app/org/tronscan/importer/DatabaseImporter.scala b/app/org/tronscan/importer/ContractImporter.scala similarity index 96% rename from app/org/tronscan/importer/DatabaseImporter.scala rename to app/org/tronscan/importer/ContractImporter.scala index 46a30b3..9744aa1 100644 --- a/app/org/tronscan/importer/DatabaseImporter.scala +++ b/app/org/tronscan/importer/ContractImporter.scala @@ -12,7 +12,7 @@ import org.tronscan.Extensions._ /** * Builds queries from transaction contracts */ -class DatabaseImporter @Inject() ( +class ContractImporter @Inject()( blockModelRepository: BlockModelRepository, transactionModelRepository: TransactionModelRepository, transferRepository: TransferModelRepository, @@ -36,7 +36,7 @@ class DatabaseImporter @Inject() ( def importWitnessVote: ContractQueryBuilder = { case (VoteWitnessContract, _, votes: VoteWitnessList) => - voteWitnessContractModelRepository.buildUpdateVotes(votes.voterAddress, votes.votes) + voteWitnessContractModelRepository.buildInsertVotes(votes.votes) } def importAssetIssue: ContractQueryBuilder = { diff --git a/app/org/tronscan/importer/FullNodeImporter.scala b/app/org/tronscan/importer/FullNodeImporter.scala index b3f2095..fcff3a9 100644 --- a/app/org/tronscan/importer/FullNodeImporter.scala +++ b/app/org/tronscan/importer/FullNodeImporter.scala @@ -33,11 +33,11 @@ class FullNodeImporter @Inject()( def buildStream(implicit actorSystem: ActorSystem) = { async { val nodeState = await(synchronisationService.nodeState) - Logger.info("BuildStream::nodeState -> " + nodeState) +// Logger.info("BuildStream::nodeState -> " + nodeState) val importAction = await(importStreamFactory.buildImportActionFromImportStatus(nodeState)) - Logger.info("BuildStream::importAction -> " + importAction) - val importers = importersFactory.buildFullNodeImporters(importAction) - Logger.info("BuildStream::importers -> " + importers.debug) +// Logger.info("BuildStream::importAction -> " + importAction) + val importers = await(importersFactory.buildFullNodeImporters(importAction)) +// Logger.info("BuildStream::importers -> " + importers.debug) val synchronisationChecker = importStreamFactory.fullNodePreSynchronisationChecker val blockSource = importStreamFactory.buildBlockSource(walletClient) val blockSink = importStreamFactory.buildBlockSink(importers) @@ -46,6 +46,7 @@ class FullNodeImporter @Inject()( .single(nodeState) .via(synchronisationChecker) .via(blockSource) + .via(importStreamFactory.buildBlockSequenceChecker) .toMat(blockSink)(Keep.right) } } diff --git a/app/org/tronscan/importer/ImportManager.scala b/app/org/tronscan/importer/ImportManager.scala index 0398afd..ececece 100644 --- a/app/org/tronscan/importer/ImportManager.scala +++ b/app/org/tronscan/importer/ImportManager.scala @@ -27,6 +27,7 @@ class ImportManager @Inject() ( fullNodeImporter: FullNodeImporter, solidityNodeImporter: SolidityNodeImporter, walletClient: WalletClient, + voteRoundImporter: VoteRoundImporter, accountImporter: AccountImporter) extends Actor { val config = configurationProvider.get @@ -65,6 +66,14 @@ class ImportManager @Inject() ( val syncSolidity = config.get[Boolean]("sync.solidity") val syncFull = config.get[Boolean]("sync.full") val syncAddresses = config.get[Boolean]("sync.addresses") + val syncVoteRounds = config.get[Boolean]("sync.votes") + + if (syncVoteRounds) { + startImporter("ROUNDS") { + Source.tick(3.seconds, 30.minutes, "") + .mapAsync(1)(_ => voteRoundImporter.importRounds().run()) + } + } if (syncFull) { startImporter("FULL") { diff --git a/app/org/tronscan/importer/ImportStreamFactory.scala b/app/org/tronscan/importer/ImportStreamFactory.scala index ec958b5..21084b9 100644 --- a/app/org/tronscan/importer/ImportStreamFactory.scala +++ b/app/org/tronscan/importer/ImportStreamFactory.scala @@ -1,7 +1,7 @@ package org.tronscan.importer import akka.stream.SinkShape -import akka.stream.scaladsl.{Broadcast, Flow, GraphDSL, Merge, Sink, Source} +import akka.stream.scaladsl.{Broadcast, Flow, GraphDSL, Merge, Sink} import akka.{Done, NotUsed} import javax.inject.Inject import org.tron.protos.Tron.{Block, Transaction} @@ -9,18 +9,12 @@ import org.tronscan.Extensions._ import org.tronscan.domain.Types.Address import org.tronscan.grpc.{FullNodeBlockChain, SolidityBlockChain, WalletClient} import org.tronscan.importer.StreamTypes._ -import org.tronscan.models._ +import org.tronscan.models.MaintenanceRoundModelRepository import org.tronscan.service.SynchronisationService -import org.tronscan.utils.{ModelUtils, StreamUtils} +import org.tronscan.utils.StreamUtils import play.api.Logger -import play.api.cache.NamedCache -import play.api.cache.redis.CacheAsyncApi -import slick.dbio.{Effect, NoStream} -import slick.sql.FixedSqlAction import scala.async.Async._ -import scala.collection.mutable.ListBuffer -import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} /** @@ -88,6 +82,11 @@ case class ImportAction( * If db should be reset */ resetDB: Boolean = false, + + /** + * If every block should be logged + */ + logAllBlocks: Boolean = true, ) class ImportStreamFactory @Inject()( @@ -103,6 +102,7 @@ class ImportStreamFactory @Inject()( var redisCleaner = true var asyncAddressImport = true var publishEvents = true + var logAllBlocks = true val fullNodeBlockHash = await(syncService.getFullNodeHashByNum(importStatus.solidityBlock)) val resetDB = !await(syncService.isSameChain()) @@ -122,6 +122,7 @@ class ImportStreamFactory @Inject()( // Don't publish events when there is lots to sync if (importStatus.dbLatestBlock < (importStatus.fullNodeBlock - 250)) { publishEvents = false + logAllBlocks = false } // No need to clean cache when starting a clean sync @@ -135,7 +136,8 @@ class ImportStreamFactory @Inject()( cleanRedisCache = redisCleaner, asyncAddressImport = asyncAddressImport, publishEvents = publishEvents, - resetDB = resetDB + resetDB = resetDB, + logAllBlocks = logAllBlocks, ) } @@ -185,6 +187,30 @@ class ImportStreamFactory @Inject()( }) } + /** + * Verifies that blocks are properly sequential + */ + def buildBlockSequenceChecker = { + Flow[Block] + .statefulMapConcat { () => + var number = -1L + block => { + val currentNumber = block.getBlockHeader.getRawData.number + if (number == -1) { + number = currentNumber + List(block) + } else if (number + 1 == currentNumber) { + number = block.getBlockHeader.getRawData.number + List(block) + } else if (number == currentNumber) { + List.empty + } else { + throw new Exception(s"Incorrect block number sequence, $number => $currentNumber") + } + } + } + } + /** * Build a stream of blocks from a solidity node */ @@ -193,12 +219,12 @@ class ImportStreamFactory @Inject()( .mapAsync(1) { status => walletClient.full.map { walletFull => val fullNodeBlockChain = new FullNodeBlockChain(walletFull) - val fromBlock = status.dbLatestBlock + 1 - val toBlock = status.fullNodeBlock - 2 + val fromBlock = if (status.dbLatestBlock <= 0) 0 else status.dbLatestBlock + 1 + val toBlock = status.fullNodeBlock - 1 // Switch between batch or single depending how far the sync is behind if (status.fullNodeBlocksToSync < 100) blockChainBuilder.readFullNodeBlocks(fromBlock, toBlock)(fullNodeBlockChain.client) - else blockChainBuilder.readFullNodeBlocksBatched(fromBlock, toBlock, 100)(walletClient) + else blockChainBuilder.readFullNodeBlocksBatched(fromBlock, toBlock, 90)(walletClient) } } .flatMapConcat(blockStream => blockStream) @@ -227,7 +253,7 @@ class ImportStreamFactory @Inject()( .filter { // Stop if there are more then 100 blocks to sync for full node case status if status.fullNodeBlocksToSync > 0 => - Logger.info(s"START SYNC FROM ${status.dbLatestBlock} TO ${status.fullNodeBlock}. " + status.toString) + Logger.info(s"START SYNC FROM ${status.dbLatestBlock} TO ${status.fullNodeBlock}.") true case status => Logger.info("IGNORE FULL NODE SYNC: " + status.toString) diff --git a/app/org/tronscan/importer/ImportersFactory.scala b/app/org/tronscan/importer/ImportersFactory.scala index 0a23feb..16e77d3 100644 --- a/app/org/tronscan/importer/ImportersFactory.scala +++ b/app/org/tronscan/importer/ImportersFactory.scala @@ -13,7 +13,8 @@ import org.tronscan.service.SynchronisationService import play.api.cache.NamedCache import play.api.cache.redis.CacheAsyncApi import org.tronscan.Extensions._ - +import org.tronscan.models.MaintenanceRoundModelRepository +import async.Async._ import scala.concurrent.{ExecutionContext, Future} class ImportersFactory @Inject() ( @@ -22,16 +23,19 @@ class ImportersFactory @Inject() ( @NamedCache("redis") redisCache: CacheAsyncApi, accountImporter: AccountImporter, walletClient: WalletClient, + maintenanceRoundModelRepository: MaintenanceRoundModelRepository, blockImporter: BlockImporter) { /** * Build importers for Full Node * @return */ - def buildFullNodeImporters(importAction: ImportAction)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) = { + def buildFullNodeImporters(importAction: ImportAction)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) = async { val redisCleaner = if (importAction.cleanRedisCache) Flow[Address].alsoTo(redisCacheCleaner) else Flow[Address] + val maintenanceRound = blockImporter.buildVotingRoundImporter(await(maintenanceRoundModelRepository.findLatest)).toFlow + val accountUpdaterFlow: Flow[Address, Address, NotUsed] = { if (importAction.updateAccounts) { if (importAction.asyncAddressImport) { @@ -64,8 +68,12 @@ class ImportersFactory @Inject() ( .addAddress(redisCleaner) .addContract(eventsPublisher) .addBlock(blockFlow) + .addBlock(maintenanceRound) } + /** + * Build Solidity Blockchain importers + */ def buildSolidityImporters(importAction: ImportAction)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) = { val eventsPublisher = if (importAction.publishEvents) { diff --git a/app/org/tronscan/importer/SolidityNodeImporter.scala b/app/org/tronscan/importer/SolidityNodeImporter.scala index 4f688c5..56cf143 100644 --- a/app/org/tronscan/importer/SolidityNodeImporter.scala +++ b/app/org/tronscan/importer/SolidityNodeImporter.scala @@ -32,11 +32,11 @@ class SolidityNodeImporter @Inject()( async { val nodeState = await(synchronisationService.nodeState) - Logger.info("BuildStream::nodeState -> " + nodeState) +// Logger.info("BuildStream::nodeState -> " + nodeState) val importAction = await(importStreamFactory.buildImportActionFromImportStatus(nodeState)) - Logger.info("BuildStream::importAction -> " + importAction) +// Logger.info("BuildStream::importAction -> " + importAction) val importers = importersFactory.buildSolidityImporters(importAction) - Logger.info("BuildStream::importers -> " + importers.debug) +// Logger.info("BuildStream::importers -> " + importers.debug) val synchronisationChecker = importStreamFactory.solidityNodePreSynchronisationChecker val blockSource = importStreamFactory.buildSolidityBlockSource(walletClient) val blockSink = importStreamFactory.buildBlockSink(importers) diff --git a/app/org/tronscan/importer/VoteRoundImporter.scala b/app/org/tronscan/importer/VoteRoundImporter.scala new file mode 100644 index 0000000..d0d51e1 --- /dev/null +++ b/app/org/tronscan/importer/VoteRoundImporter.scala @@ -0,0 +1,62 @@ +package org.tronscan.importer + +import akka.stream.scaladsl.{Keep, Sink, Source} +import io.circe.Json +import javax.inject.Inject +import org.tron.protos.Tron.Transaction.Contract.ContractType +import org.tronscan.Extensions._ +import org.tronscan.api.models.TransactionSerializer._ +import org.tronscan.models.{MaintenanceRoundModelRepository, RoundVoteModelRepository} +import play.api.Logger + +import scala.concurrent.ExecutionContext + +class VoteRoundImporter @Inject() ( + maintenanceRepository: MaintenanceRoundModelRepository, + roundVoteModelRepository: RoundVoteModelRepository) { + + /** + * Import all vote rounds + */ + def importRounds()(implicit executionContext: ExecutionContext) = { + + Source + .single(0) + // Load rounds + .mapAsync(1)(_ => maintenanceRepository.findAllRounds) + // Combine rounds + .mapConcat(_.sliding(2).toList) + // Iterate all votes + .foldAsync(Map[String, Map[String, Long]]()) { case (previousVotes, Seq(currentRound, nextRound)) => + Logger.info(s"Importing round ${currentRound.number}") + for { + votes <- maintenanceRepository.getVotesBetweenBlocks(currentRound.block, nextRound.block) + newMap = buildVotes(previousVotes, votes) + _ <- roundVoteModelRepository.insertVoteRounds(newMap, currentRound.number) + } yield newMap + } + .toMat(Sink.ignore)(Keep.right) + } + + /** + * Build the vote map based on the round votes + */ + def buildVotes(voteMap: Map[String, Map[String, Long]] = Map.empty, roundVotes: Vector[(String, Int, Json)]) = { + + roundVotes.foldLeft(voteMap) { + case (votes, (address, contractType, contractData)) => + if (contractType == ContractType.UnfreezeBalanceContract.value) { + // Reset votes when unfreezing balance + votes - address + } else if (contractType == ContractType.VoteWitnessContract.value) { + // Read votes from the contract data and add them to the total votes + val contractVotes = contractData.as[org.tron.protos.Contract.VoteWitnessContract].toOption.get + votes ++ Map(address -> contractVotes.votes.map(x => (x.voteAddress.encodeAddress, x.voteCount)).toMap) + } else { + // Do nothing + votes + } + } + } + +} diff --git a/app/org/tronscan/models/AccountBandwidthCapsule.scala b/app/org/tronscan/models/AccountBandwidthCapsule.scala index 408be74..8334a5f 100644 --- a/app/org/tronscan/models/AccountBandwidthCapsule.scala +++ b/app/org/tronscan/models/AccountBandwidthCapsule.scala @@ -1,5 +1,6 @@ package org.tronscan.models +import com.google.inject.Guice import org.tron.api.api.AccountNetMessage case class AccountBandwidthCapsule(accountNetMessage: AccountNetMessage) { diff --git a/app/org/tronscan/models/MaintenanceRound.scala b/app/org/tronscan/models/MaintenanceRound.scala new file mode 100644 index 0000000..bc8d9ac --- /dev/null +++ b/app/org/tronscan/models/MaintenanceRound.scala @@ -0,0 +1,80 @@ +package org.tronscan.models + +import com.google.inject.{Inject, Singleton} +import io.circe.parser.parse +import org.joda.time.DateTime +import org.tronscan.db.PgProfile.api._ +import org.tronscan.db.TableRepository +import play.api.db.slick.DatabaseConfigProvider + +import scala.concurrent.ExecutionContext + +case class MaintenanceRoundModel( + block: Long, + number: Int, + timestamp: Long, + dateStart: DateTime = DateTime.now, + dateEnd: Option[DateTime] = None, +) + +class MaintenanceRoundModelTable(tag: Tag) extends Table[MaintenanceRoundModel](tag, "maintenance_round") { + def block = column[Long]("block", O.PrimaryKey) + def number = column[Int]("number") + def timestamp = column[Long]("timestamp") + def dateStart = column[DateTime]("date_start") + def dateEnd = column[DateTime]("date_end") + def * = (block, number, timestamp, dateStart, dateEnd.?) <> (MaintenanceRoundModel.tupled, MaintenanceRoundModel.unapply) +} + +@Singleton() +class MaintenanceRoundModelRepository @Inject() (val dbConfig: DatabaseConfigProvider) extends TableRepository[MaintenanceRoundModelTable, MaintenanceRoundModel] { + + lazy val table = TableQuery[MaintenanceRoundModelTable] + lazy val transactionTable = TableQuery[TransactionModelTable] + lazy val transferTable = TableQuery[TransferModelTable] + + def findAll = run { + table.result + } + + def findAllRounds = run { + table.sortBy(_.number.asc).result + } + + def findLatest = run { + table.sortBy(_.number.desc).result.headOption + } + + /** + * Retrieve the votes for the given blocks + */ + def getVotesBetweenBlocks(fromBlock: Long, toBlock: Long)(implicit executionContext: ExecutionContext) = run { + sql""" + SELECT + t.owner_address, + t.contract_type, + t.contract_data + FROM ( + SELECT + owner_address, + max(date_created) as ts + FROM transactions + WHERE (block > $fromBlock AND block <= $toBlock) + AND (contract_type = 12 OR contract_type = 4) + GROUP BY owner_address + ) r + INNER JOIN transactions t + ON (t.owner_address = r.owner_address AND t.date_created = r.ts) + """.as[(String, Int, String)] + .map { result => + result.map { case (owner, contractType, contract) => + (owner, contractType, parse(contract).toOption.get) + } + } + } + + + def findByNumber(number: Int) = run { + table.filter(_.number === number).result.headOption + } +} \ No newline at end of file diff --git a/app/org/tronscan/models/RoundVote.scala b/app/org/tronscan/models/RoundVote.scala new file mode 100644 index 0000000..4f64c7f --- /dev/null +++ b/app/org/tronscan/models/RoundVote.scala @@ -0,0 +1,50 @@ +package org.tronscan.models + +import com.google.inject.{Inject, Singleton} +import io.circe.parser.parse +import org.joda.time.DateTime +import org.tronscan.db.PgProfile.api._ +import org.tronscan.db.TableRepository +import play.api.db.slick.DatabaseConfigProvider + +import scala.concurrent.ExecutionContext + +case class RoundVoteModel( + address: String, + round: Int, + candidate: String, + votes: Long, +) + +class RoundVoteModelTable(tag: Tag) extends Table[RoundVoteModel](tag, "round_votes") { + def address = column[String]("address", O.PrimaryKey) + def round = column[Int]("round", O.PrimaryKey) + def candidate = column[String]("candidate", O.PrimaryKey) + def votes = column[Long]("votes") + def * = (address, round, candidate, votes) <> (RoundVoteModel.tupled, RoundVoteModel.unapply) +} + +@Singleton() +class RoundVoteModelRepository @Inject() (val dbConfig: DatabaseConfigProvider) extends TableRepository[RoundVoteModelTable, RoundVoteModel] { + + lazy val table = TableQuery[RoundVoteModelTable] + lazy val transactionTable = TableQuery[TransactionModelTable] + lazy val transferTable = TableQuery[TransferModelTable] + + def findAll = run { + table.result + } + + def findByNumber(number: Int) = run { + table.filter(_.round === number).result.headOption + } + + def insertVoteRounds(votes: Map[String, Map[String, Long]], round: Int) = run { + val models = for { + (address, candidateVotes) <- votes + (candidate, voteCount) <- candidateVotes + } yield RoundVoteModel(address, round, candidate, voteCount) + DBIO.seq(Seq(table.filter(_.round === round).delete) ++ models.map(m => table += m): _*).transactionally.withPinnedSession + } + +} \ No newline at end of file diff --git a/app/org/tronscan/models/VoteWitnessContractModel.scala b/app/org/tronscan/models/VoteWitnessContractModel.scala index 124bcc3..bcb22f7 100644 --- a/app/org/tronscan/models/VoteWitnessContractModel.scala +++ b/app/org/tronscan/models/VoteWitnessContractModel.scala @@ -3,11 +3,16 @@ package org.tronscan.models import java.util.UUID import com.google.inject.{Inject, Singleton} +import io.circe.Json import org.joda.time.DateTime import org.tronscan.db.PgProfile.api._ import org.tronscan.db.TableRepository import play.api.db.slick.DatabaseConfigProvider +import slick.dbio.Effect +import slick.sql.FixedSqlAction +import io.circe.parser._ +import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits.global case class VoteWitnessList( @@ -15,7 +20,7 @@ case class VoteWitnessList( votes: List[VoteWitnessContractModel] = List.empty) case class VoteWitnessContractModel( - id: UUID = UUID.randomUUID(), + id: String, block: Long, transaction: String, timestamp: DateTime, @@ -24,12 +29,12 @@ case class VoteWitnessContractModel( votes: Long = 0L) class VoteWitnessContractModelTable(tag: Tag) extends Table[VoteWitnessContractModel](tag, "vote_witness_contract") { - def id = column[UUID]("id") + def id = column[String]("id", O.PrimaryKey) + def candidateAddress = column[String]("candidate_address", O.PrimaryKey) def block = column[Long]("block") def transaction = column[String]("transaction") def timestamp = column[DateTime]("date_created") def voterAddress = column[String]("voter_address") - def candidateAddress = column[String]("candidate_address") def votes = column[Long]("votes") def * = (id, block, transaction, timestamp, voterAddress, candidateAddress, votes) <> (VoteWitnessContractModel.tupled, VoteWitnessContractModel.unapply) } @@ -57,10 +62,14 @@ class VoteWitnessContractModelRepository @Inject() (val dbConfig: DatabaseConfig DBIO.seq(Seq(table.filter(_.voterAddress === address).delete) ++ votes.map(x => table += x): _*).transactionally } - def buildUpdateVotes(address: String, votes: Seq[VoteWitnessContractModel]) = { + def buildUpdateVotes(address: String, votes: Seq[VoteWitnessContractModel]): Seq[FixedSqlAction[Int, NoStream, Effect.Write]] = { Seq(table.filter(_.voterAddress === address).delete) ++ votes.map(x => table += x) } + def buildInsertVotes(votes: Seq[VoteWitnessContractModel]) = { + votes.map(x => table.insertOrUpdate(x)) + } + def buildDeleteVotesForAddress(address: String) = { table.filter(_.voterAddress === address).delete } diff --git a/app/org/tronscan/network/NetworkNode.scala b/app/org/tronscan/network/NetworkNode.scala index 04ffbe2..0ca43b9 100644 --- a/app/org/tronscan/network/NetworkNode.scala +++ b/app/org/tronscan/network/NetworkNode.scala @@ -16,6 +16,9 @@ case class NetworkNode( grpcResponseTime: Long = 0, pingOnline: Boolean = false, pingResponseTime: Long = 0, + httpEnabled: Boolean = false, + httpResponseTime: Long = 0, + httpUrl: String = "", country: String = "", city: String = "", lat: Double = 0, diff --git a/app/org/tronscan/network/NetworkScanner.scala b/app/org/tronscan/network/NetworkScanner.scala index 1c5ea11..6bd3431 100644 --- a/app/org/tronscan/network/NetworkScanner.scala +++ b/app/org/tronscan/network/NetworkScanner.scala @@ -18,6 +18,7 @@ import org.tronscan.utils.StreamUtils import play.api.Logger import play.api.inject.ConfigurationProvider import play.api.libs.concurrent.Futures +import play.api.libs.ws.WSClient import scala.concurrent.Future import scala.concurrent.duration._ @@ -42,7 +43,8 @@ class NetworkScanner @Inject()( configurationProvider: ConfigurationProvider, @Named("grpc-pool") actorRef: ActorRef, geoIPService: GeoIPService, - implicit val futures: Futures) extends Actor { + implicit val futures: Futures, + implicit val WSClient: WSClient) extends Actor { val workContext = context.system.dispatchers.lookup("contexts.node-watcher") val debugEnabled = configurationProvider.get.get[Boolean]("network.scanner.debug") @@ -124,6 +126,17 @@ class NetworkScanner @Inject()( } node } + .via(NetworkStreams.httpPinger(12)) + .map { node => + if (debugEnabled) { + if (node.httpEnabled) { + Logger.debug("HTTP Online: " + node.ip) + } else { + Logger.debug("HTTP Offline: " + node.ip) + } + } + node + } } def seedNodes = { @@ -244,7 +257,7 @@ class NetworkScanner @Inject()( case None => networkNodes = networkNodes + (node.ip -> node) case x => - println("ignoring update", x) + } } diff --git a/app/org/tronscan/network/NetworkStreams.scala b/app/org/tronscan/network/NetworkStreams.scala index 6c4bda3..84d776b 100644 --- a/app/org/tronscan/network/NetworkStreams.scala +++ b/app/org/tronscan/network/NetworkStreams.scala @@ -4,15 +4,16 @@ import java.net.InetAddress import java.util.concurrent.TimeUnit import akka.NotUsed -import akka.stream.scaladsl.{Flow, Source} +import akka.stream.scaladsl.Flow import org.tron.api.api.EmptyMessage +import org.tronscan.Extensions._ +import org.tronscan.utils.NetworkUtils import play.api.libs.concurrent.Futures import play.api.libs.concurrent.Futures._ +import play.api.libs.ws.WSClient import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import org.tronscan.Extensions._ -import org.tronscan.utils.NetworkUtils object NetworkStreams { @@ -108,4 +109,42 @@ object NetworkStreams { } } } + + /** + * Ping the given IPS and returns a node + * + * @param parallel parallel number of processes + */ + def httpPinger(parallel: Int = 4)(implicit executionContext: ExecutionContext, wsClient: WSClient): Flow[NetworkNode, NetworkNode, NotUsed] = { + Flow[NetworkNode] + .mapAsyncUnordered(parallel) { networkNode => + + val ia = InetAddress.getByName(networkNode.ip) + val startPing = System.currentTimeMillis() + + (for { + online <- if (networkNode.nodeType == NetworkScanner.full) { + NetworkUtils.pingHttp(s"http://${networkNode.ip}:8090/wallet/getnowblock") + } else { + NetworkUtils.pingHttp(s"http://${networkNode.ip}:8091/walletsolidity/getnowblock") + } + response = System.currentTimeMillis() - startPing + } yield { + if (online) { + val httpPort = if (networkNode.nodeType == NetworkScanner.full) 8090 else 8091 + + networkNode.copy( + httpEnabled = online, + httpResponseTime = response, + httpUrl = s"http://${networkNode.ip}:$httpPort", + ) + } else { + networkNode.copy(httpEnabled = false) + } + }).recover { + case _ => + networkNode.copy(httpEnabled = false) + } + } + } } diff --git a/app/org/tronscan/utils/CatsUtils.scala b/app/org/tronscan/utils/CatsUtils.scala new file mode 100644 index 0000000..5df1304 --- /dev/null +++ b/app/org/tronscan/utils/CatsUtils.scala @@ -0,0 +1,15 @@ +package org.tronscan.utils + +import cats.Monoid +import org.tron.protos.Tron.Block + +object CatsUtils { + + implicit val blockSemiGroup = new Monoid[Block] { + def empty = Block() + def combine(x: Block, y: Block): Block = { + x.withTransactions(x.transactions ++ y.transactions) + } + } + +} diff --git a/app/org/tronscan/utils/ContractUtils.scala b/app/org/tronscan/utils/ContractUtils.scala index e3aba87..756a9aa 100644 --- a/app/org/tronscan/utils/ContractUtils.scala +++ b/app/org/tronscan/utils/ContractUtils.scala @@ -27,9 +27,6 @@ object ContractUtils { case c: AssetIssueContract => c.ownerAddress.encodeAddress - case c: DeployContract => - c.ownerAddress.encodeAddress - case c: ParticipateAssetIssueContract => c.ownerAddress.encodeAddress @@ -57,6 +54,42 @@ object ContractUtils { case c: UpdateAssetContract => c.ownerAddress.encodeAddress + case c: ProposalCreateContract => + c.ownerAddress.encodeAddress + + case c: ProposalApproveContract => + c.ownerAddress.encodeAddress + + case c: ProposalDeleteContract => + c.ownerAddress.encodeAddress + + case c: CreateSmartContract => + c.ownerAddress.encodeAddress + + case c: TriggerSmartContract => + c.ownerAddress.encodeAddress + + case c: BuyStorageBytesContract => + c.ownerAddress.encodeAddress + + case c: BuyStorageContract => + c.ownerAddress.encodeAddress + + case c: SellStorageContract => + c.ownerAddress.encodeAddress + + case c: ExchangeCreateContract => + c.ownerAddress.encodeAddress + + case c: ExchangeInjectContract => + c.ownerAddress.encodeAddress + + case c: ExchangeWithdrawContract => + c.ownerAddress.encodeAddress + + case c: ExchangeTransactionContract => + c.ownerAddress.encodeAddress + case _ => "" } diff --git a/app/org/tronscan/utils/ModelUtils.scala b/app/org/tronscan/utils/ModelUtils.scala index fd2cb29..af21280 100644 --- a/app/org/tronscan/utils/ModelUtils.scala +++ b/app/org/tronscan/utils/ModelUtils.scala @@ -62,6 +62,7 @@ object ModelUtils { case c: VoteWitnessContract => val inserts = for (vote <- c.votes) yield { VoteWitnessContractModel( + id = transactionHash, transaction = transactionHash, block = header.number, timestamp = transactionTime, diff --git a/app/org/tronscan/utils/NetworkUtils.scala b/app/org/tronscan/utils/NetworkUtils.scala index ef5ecdf..60ba418 100644 --- a/app/org/tronscan/utils/NetworkUtils.scala +++ b/app/org/tronscan/utils/NetworkUtils.scala @@ -2,6 +2,8 @@ package org.tronscan.utils import java.net.{InetSocketAddress, Socket} +import play.api.libs.ws.WSClient + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ @@ -21,4 +23,15 @@ object NetworkUtils { false } } + + /** + * Try to open a socket for the given ip and port + */ + def pingHttp(url: String, timeout: FiniteDuration = 5.seconds)(implicit executionContext: ExecutionContext, wsClient: WSClient) = { + wsClient + .url(url) + .withRequestTimeout(timeout) + .get() + .map(x => (x.json \ "blockID").isDefined) + } } diff --git a/app/org/tronscan/utils/ProtoUtils.scala b/app/org/tronscan/utils/ProtoUtils.scala index c72994d..95791d5 100644 --- a/app/org/tronscan/utils/ProtoUtils.scala +++ b/app/org/tronscan/utils/ProtoUtils.scala @@ -1,5 +1,6 @@ package org.tronscan.utils +import org.tron.protos.Contract.{BuyStorageBytesContract, BuyStorageContract, SellStorageContract} import org.tron.protos.Tron.Transaction object ProtoUtils { @@ -14,32 +15,82 @@ object ProtoUtils { contract.`type` match { case TransferContract => org.tron.protos.Contract.TransferContract.parseFrom(any.value.toByteArray) + case TransferAssetContract => org.tron.protos.Contract.TransferAssetContract.parseFrom(any.value.toByteArray) + case VoteWitnessContract => org.tron.protos.Contract.VoteWitnessContract.parseFrom(any.value.toByteArray) + case AssetIssueContract => org.tron.protos.Contract.AssetIssueContract.parseFrom(any.value.toByteArray) + case UpdateAssetContract => org.tron.protos.Contract.UpdateAssetContract.parseFrom(any.value.toByteArray) + case ParticipateAssetIssueContract => org.tron.protos.Contract.ParticipateAssetIssueContract.parseFrom(any.value.toByteArray) + case WitnessCreateContract => org.tron.protos.Contract.WitnessCreateContract.parseFrom(any.value.toByteArray) + case WitnessUpdateContract => org.tron.protos.Contract.WitnessUpdateContract.parseFrom(any.value.toByteArray) + case UnfreezeBalanceContract => org.tron.protos.Contract.UnfreezeBalanceContract.parseFrom(any.value.toByteArray) + case FreezeBalanceContract => org.tron.protos.Contract.FreezeBalanceContract.parseFrom(any.value.toByteArray) + case WithdrawBalanceContract => org.tron.protos.Contract.WithdrawBalanceContract.parseFrom(any.value.toByteArray) + case AccountUpdateContract => org.tron.protos.Contract.AccountUpdateContract.parseFrom(any.value.toByteArray) + case UnfreezeAssetContract => org.tron.protos.Contract.UnfreezeAssetContract.parseFrom(any.value.toByteArray) + case AccountCreateContract => org.tron.protos.Contract.AccountCreateContract.parseFrom(any.value.toByteArray) + + case ProposalCreateContract => + org.tron.protos.Contract.ProposalCreateContract.parseFrom(any.value.toByteArray) + + case ProposalApproveContract => + org.tron.protos.Contract.ProposalApproveContract.parseFrom(any.value.toByteArray) + + case ProposalDeleteContract => + org.tron.protos.Contract.ProposalDeleteContract.parseFrom(any.value.toByteArray) + + case CreateSmartContract => + org.tron.protos.Contract.CreateSmartContract.parseFrom(any.value.toByteArray) + + case TriggerSmartContract => + org.tron.protos.Contract.TriggerSmartContract.parseFrom(any.value.toByteArray) + +// case BuyStorageBytesContract => +// org.tron.protos.Contract.BuyStorageBytesContract.parseFrom(any.value.toByteArray) + +// case BuyStorageContract => +// org.tron.protos.Contract.BuyStorageContract.parseFrom(any.value.toByteArray) + +// case SellStorageContract => +// org.tron.protos.Contract.SellStorageContract.parseFrom(any.value.toByteArray) + + case ExchangeCreateContract => + org.tron.protos.Contract.ExchangeCreateContract.parseFrom(any.value.toByteArray) + + case ExchangeInjectContract => + org.tron.protos.Contract.ExchangeInjectContract.parseFrom(any.value.toByteArray) + + case ExchangeWithdrawContract => + org.tron.protos.Contract.ExchangeWithdrawContract.parseFrom(any.value.toByteArray) + + case ExchangeTransactionContract => + org.tron.protos.Contract.ExchangeTransactionContract.parseFrom(any.value.toByteArray) + case _ => throw new Exception("Unknown Contract") } diff --git a/app/protobuf/api/api.proto b/app/protobuf/api/api.proto index 1266a7d..a84e460 100644 --- a/app/protobuf/api/api.proto +++ b/app/protobuf/api/api.proto @@ -22,6 +22,17 @@ service Wallet { }; }; + rpc GetAccountById (Account) returns (Account) { + option (google.api.http) = { + post: "/wallet/getaccountbyid" + body: "*" + additional_bindings { + get: "/wallet/getaccountbyid" + } + }; + }; + + //Please use CreateTransaction2 instead of this function. rpc CreateTransaction (TransferContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/createtransaction" @@ -31,6 +42,9 @@ service Wallet { } }; }; + //Use this function instead of CreateTransaction. + rpc CreateTransaction2 (TransferContract) returns (TransactionExtention) { + }; rpc BroadcastTransaction (Transaction) returns (Return) { option (google.api.http) = { @@ -41,7 +55,7 @@ service Wallet { } }; }; - + //Please use UpdateAccount2 instead of this function. rpc UpdateAccount (AccountUpdateContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/updateaccount" @@ -52,6 +66,22 @@ service Wallet { }; }; + + rpc SetAccountId (SetAccountIdContract) returns (Transaction) { + option (google.api.http) = { + post: "/wallet/setaccountid" + body: "*" + additional_bindings { + get: "/wallet/setaccountid" + } + }; + }; + + //Use this function instead of UpdateAccount. + rpc UpdateAccount2 (AccountUpdateContract) returns (TransactionExtention) { + }; + + //Please use VoteWitnessAccount2 instead of this function. rpc VoteWitnessAccount (VoteWitnessContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/votewitnessaccount" @@ -62,6 +92,14 @@ service Wallet { }; }; + //modify the consume_user_resource_percent + rpc UpdateSetting (UpdateSettingContract) returns (TransactionExtention) { + }; + + //Use this function instead of VoteWitnessAccount. + rpc VoteWitnessAccount2 (VoteWitnessContract) returns (TransactionExtention) { + }; + //Please use CreateAssetIssue2 instead of this function. rpc CreateAssetIssue (AssetIssueContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/createassetissue" @@ -71,7 +109,10 @@ service Wallet { } }; }; - + //Use this function instead of CreateAssetIssue. + rpc CreateAssetIssue2 (AssetIssueContract) returns (TransactionExtention) { + }; + //Please use UpdateWitness2 instead of this function. rpc UpdateWitness (WitnessUpdateContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/updatewitness" @@ -81,7 +122,10 @@ service Wallet { } }; }; - + //Use this function instead of UpdateWitness. + rpc UpdateWitness2 (WitnessUpdateContract) returns (TransactionExtention) { + }; + //Please use CreateAccount2 instead of this function. rpc CreateAccount (AccountCreateContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/createaccount" @@ -91,7 +135,10 @@ service Wallet { } }; }; - + //Use this function instead of CreateAccount. + rpc CreateAccount2 (AccountCreateContract) returns (TransactionExtention) { + } + //Please use CreateWitness2 instead of this function. rpc CreateWitness (WitnessCreateContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/createwitness" @@ -101,7 +148,10 @@ service Wallet { } }; }; - + //Use this function instead of CreateWitness. + rpc CreateWitness2 (WitnessCreateContract) returns (TransactionExtention) { + } + //Please use TransferAsset2 instead of this function. rpc TransferAsset (TransferAssetContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/transferasset" @@ -111,7 +161,10 @@ service Wallet { } }; } - + //Use this function instead of TransferAsset. + rpc TransferAsset2 (TransferAssetContract) returns (TransactionExtention) { + } + //Please use ParticipateAssetIssue2 instead of this function. rpc ParticipateAssetIssue (ParticipateAssetIssueContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/participateassetissue" @@ -121,7 +174,10 @@ service Wallet { } }; } - + //Use this function instead of ParticipateAssetIssue. + rpc ParticipateAssetIssue2 (ParticipateAssetIssueContract) returns (TransactionExtention) { + } + //Please use FreezeBalance2 instead of this function. rpc FreezeBalance (FreezeBalanceContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/freezebalance" @@ -131,7 +187,10 @@ service Wallet { } }; } - + //Use this function instead of FreezeBalance. + rpc FreezeBalance2 (FreezeBalanceContract) returns (TransactionExtention) { + } + //Please use UnfreezeBalance2 instead of this function. rpc UnfreezeBalance (UnfreezeBalanceContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/unfreezebalance" @@ -141,7 +200,10 @@ service Wallet { } }; } - + //Use this function instead of UnfreezeBalance. + rpc UnfreezeBalance2 (UnfreezeBalanceContract) returns (TransactionExtention) { + } + //Please use UnfreezeAsset2 instead of this function. rpc UnfreezeAsset (UnfreezeAssetContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/unfreezeasset" @@ -151,7 +213,10 @@ service Wallet { } }; } - + //Use this function instead of UnfreezeAsset. + rpc UnfreezeAsset2 (UnfreezeAssetContract) returns (TransactionExtention) { + } + //Please use WithdrawBalance2 instead of this function. rpc WithdrawBalance (WithdrawBalanceContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/withdrawbalance" @@ -161,7 +226,10 @@ service Wallet { } }; } - + //Use this function instead of WithdrawBalance. + rpc WithdrawBalance2 (WithdrawBalanceContract) returns (TransactionExtention) { + } + //Please use UpdateAsset2 instead of this function. rpc UpdateAsset (UpdateAssetContract) returns (Transaction) { option (google.api.http) = { post: "/wallet/updateasset" @@ -171,6 +239,39 @@ service Wallet { } }; } + //Use this function instead of UpdateAsset. + rpc UpdateAsset2 (UpdateAssetContract) returns (TransactionExtention) { + } + + rpc ProposalCreate (ProposalCreateContract) returns (TransactionExtention) { + } + + rpc ProposalApprove (ProposalApproveContract) returns (TransactionExtention) { + } + + rpc ProposalDelete (ProposalDeleteContract) returns (TransactionExtention) { + } + + rpc BuyStorage (BuyStorageContract) returns (TransactionExtention) { + } + + rpc BuyStorageBytes (BuyStorageBytesContract) returns (TransactionExtention) { + } + + rpc SellStorage (SellStorageContract) returns (TransactionExtention) { + } + + rpc ExchangeCreate (ExchangeCreateContract) returns (TransactionExtention) { + } + + rpc ExchangeInject (ExchangeInjectContract) returns (TransactionExtention) { + } + + rpc ExchangeWithdraw (ExchangeWithdrawContract) returns (TransactionExtention) { + } + + rpc ExchangeTransaction (ExchangeTransactionContract) returns (TransactionExtention) { + } rpc ListNodes (EmptyMessage) returns (NodeList) { option (google.api.http) = { @@ -200,6 +301,8 @@ service Wallet { } }; }; + rpc GetAccountResource (Account) returns (AccountResourceMessage) { + }; rpc GetAssetIssueByName (BytesMessage) returns (AssetIssueContract) { option (google.api.http) = { post: "/wallet/getassetissuebyname" @@ -209,6 +312,7 @@ service Wallet { } }; } + //Please use GetNowBlock2 instead of this function. rpc GetNowBlock (EmptyMessage) returns (Block) { option (google.api.http) = { post: "/wallet/getnowblock" @@ -218,6 +322,10 @@ service Wallet { } }; } + //Use this function instead of GetNowBlock. + rpc GetNowBlock2 (EmptyMessage) returns (BlockExtention) { + } + //Please use GetBlockByNum2 instead of this function. rpc GetBlockByNum (NumberMessage) returns (Block) { option (google.api.http) = { post: "/wallet/getblockbynum" @@ -227,6 +335,12 @@ service Wallet { } }; } + //Use this function instead of GetBlockByNum. + rpc GetBlockByNum2 (NumberMessage) returns (BlockExtention) { + } + + rpc GetTransactionCountByBlockNum (NumberMessage) returns (NumberMessage) { + } rpc GetBlockById (BytesMessage) returns (Block) { option (google.api.http) = { @@ -237,6 +351,7 @@ service Wallet { } }; } + //Please use GetBlockByLimitNext2 instead of this function. rpc GetBlockByLimitNext (BlockLimit) returns (BlockList) { option (google.api.http) = { post: "/wallet/getblockbylimitnext" @@ -246,6 +361,10 @@ service Wallet { } }; } + //Use this function instead of GetBlockByLimitNext. + rpc GetBlockByLimitNext2 (BlockLimit) returns (BlockListExtention) { + } + //Please use GetBlockByLatestNum2 instead of this function. rpc GetBlockByLatestNum (NumberMessage) returns (BlockList) { option (google.api.http) = { post: "/wallet/getblockbylatestnum" @@ -255,6 +374,9 @@ service Wallet { } }; } + //Use this function instead of GetBlockByLatestNum. + rpc GetBlockByLatestNum2 (NumberMessage) returns (BlockListExtention) { + } rpc GetTransactionById (BytesMessage) returns (Transaction) { option (google.api.http) = { post: "/wallet/gettransactionbyid" @@ -265,6 +387,15 @@ service Wallet { }; } + rpc DeployContract (CreateSmartContract) returns (TransactionExtention) { + } + + rpc GetContract (BytesMessage) returns (SmartContract) { + } + + rpc TriggerContract (TriggerSmartContract) returns (TransactionExtention) { + } + rpc ListWitnesses (EmptyMessage) returns (WitnessList) { option (google.api.http) = { post: "/wallet/listwitnesses" @@ -274,6 +405,57 @@ service Wallet { } }; }; + + rpc ListProposals (EmptyMessage) returns (ProposalList) { + option (google.api.http) = { + post: "/wallet/listproposals" + body: "*" + additional_bindings { + get: "/wallet/listproposals" + } + }; + }; + + rpc GetProposalById (BytesMessage) returns (Proposal) { + option (google.api.http) = { + post: "/wallet/getproposalbyid" + body: "*" + additional_bindings { + get: "/wallet/getproposalbyid" + } + }; + }; + + rpc ListExchanges (EmptyMessage) returns (ExchangeList) { + option (google.api.http) = { + post: "/wallet/listexchanges" + body: "*" + additional_bindings { + get: "/wallet/listexchanges" + } + }; + }; + + rpc GetExchangeById (BytesMessage) returns (Exchange) { + option (google.api.http) = { + post: "/wallet/getexchangebyid" + body: "*" + additional_bindings { + get: "/wallet/getexchangebyid" + } + }; + }; + + rpc GetChainParameters (EmptyMessage) returns (ChainParameters) { + option (google.api.http) = { + post: "/wallet/getchainparameters" + body: "*" + additional_bindings { + get: "/wallet/getchainparameters" + } + }; + }; + rpc GetAssetIssueList (EmptyMessage) returns (AssetIssueList) { option (google.api.http) = { post: "/wallet/getassetissuelist" @@ -311,6 +493,7 @@ service Wallet { }; } //Warning: do not invoke this interface provided by others. + //Please use GetTransactionSign2 instead of this function. rpc GetTransactionSign (TransactionSign) returns (Transaction) { option (google.api.http) = { post: "/wallet/gettransactionsign" @@ -321,12 +504,16 @@ service Wallet { }; }; //Warning: do not invoke this interface provided by others. - rpc CreateAdresss (BytesMessage) returns (BytesMessage) { + //Use this function instead of GetTransactionSign. + rpc GetTransactionSign2 (TransactionSign) returns (TransactionExtention) { + }; + //Warning: do not invoke this interface provided by others. + rpc CreateAddress (BytesMessage) returns (BytesMessage) { option (google.api.http) = { - post: "/wallet/createadresss" + post: "/wallet/createaddress" body: "*" additional_bindings { - get: "/wallet/createadresss" + get: "/wallet/createaddress" } }; }; @@ -340,6 +527,37 @@ service Wallet { } }; }; + //Warning: do not invoke this interface provided by others. + rpc EasyTransferByPrivate (EasyTransferByPrivateMessage) returns (EasyTransferResponse) { + option (google.api.http) = { + post: "/wallet/easytransferbyprivate" + body: "*" + additional_bindings { + get: "/wallet/easytransferbyprivate" + } + }; + }; + //Warning: do not invoke this interface provided by others. + rpc GenerateAddress (EmptyMessage) returns (AddressPrKeyPairMessage) { + + option (google.api.http) = { + post: "/wallet/generateaddress" + body: "*" + additional_bindings { + get: "/wallet/generateaddress" + } + }; + } + + rpc GetTransactionInfoById (BytesMessage) returns (TransactionInfo) { + option (google.api.http) = { + post: "/wallet/gettransactioninfobyid" + body: "*" + additional_bindings { + get: "/wallet/gettransactioninfobyid" + } + }; + } }; @@ -354,6 +572,16 @@ service WalletSolidity { } }; }; + rpc GetAccountById (Account) returns (Account) { + option (google.api.http) = { + post: "/walletsolidity/getaccountbyid" + body: "*" + additional_bindings { + get: "/walletsolidity/getaccountbyid" + } + }; + }; + rpc ListWitnesses (EmptyMessage) returns (WitnessList) { option (google.api.http) = { post: "/walletsolidity/listwitnesses" @@ -381,6 +609,7 @@ service WalletSolidity { } }; } + //Please use GetNowBlock2 instead of this function. rpc GetNowBlock (EmptyMessage) returns (Block) { option (google.api.http) = { post: "/walletsolidity/getnowblock" @@ -390,6 +619,10 @@ service WalletSolidity { } }; } + //Use this function instead of GetNowBlock. + rpc GetNowBlock2 (EmptyMessage) returns (BlockExtention) { + } + //Please use GetBlockByNum2 instead of this function. rpc GetBlockByNum (NumberMessage) returns (Block) { option (google.api.http) = { post: "/walletsolidity/getblockbynum" @@ -399,6 +632,13 @@ service WalletSolidity { } }; } + //Use this function instead of GetBlockByNum. + rpc GetBlockByNum2 (NumberMessage) returns (BlockExtention) { + } + + rpc GetTransactionCountByBlockNum (NumberMessage) returns (NumberMessage) { + } + rpc GetTransactionById (BytesMessage) returns (Transaction) { option (google.api.http) = { post: "/walletsolidity/gettransactionbyid" @@ -408,7 +648,6 @@ service WalletSolidity { } }; } - rpc GetTransactionInfoById (BytesMessage) returns (TransactionInfo) { option (google.api.http) = { post: "/walletsolidity/gettransactioninfobyid" @@ -418,10 +657,20 @@ service WalletSolidity { } }; } + //Warning: do not invoke this interface provided by others. + rpc GenerateAddress (EmptyMessage) returns (AddressPrKeyPairMessage) { + option (google.api.http) = { + post: "/walletsolidity/generateaddress" + body: "*" + additional_bindings { + get: "/walletsolidity/generateaddress" + } + }; + } }; service WalletExtension { - + //Please use GetTransactionsFromThis2 instead of this function. rpc GetTransactionsFromThis (AccountPaginated) returns (TransactionList) { option (google.api.http) = { post: "/walletextension/gettransactionsfromthis" @@ -431,6 +680,10 @@ service WalletExtension { } }; } + //Use this function instead of GetTransactionsFromThis. + rpc GetTransactionsFromThis2 (AccountPaginated) returns (TransactionListExtention) { + } + //Please use GetTransactionsToThis2 instead of this function. rpc GetTransactionsToThis (AccountPaginated) returns (TransactionList) { option (google.api.http) = { post: "/walletextension/gettransactionstothis" @@ -440,6 +693,9 @@ service WalletExtension { } }; } + //Use this function instead of GetTransactionsToThis. + rpc GetTransactionsToThis2 (AccountPaginated) returns (TransactionListExtention) { + } }; // the api of tron's db @@ -492,6 +748,12 @@ service Network { message WitnessList { repeated Witness witnesses = 1; } +message ProposalList { + repeated Proposal proposals = 1; +} +message ExchangeList { + repeated Exchange exchanges = 1; +} message AssetIssueList { repeated AssetIssueContract assetIssue = 1; } @@ -548,6 +810,7 @@ message TimePaginatedMessage { int64 offset = 2; int64 limit = 3; } +//deprecated message AccountNetMessage { int64 freeNetUsed = 1; int64 freeNetLimit = 2; @@ -558,19 +821,70 @@ message AccountNetMessage { int64 TotalNetLimit = 7; int64 TotalNetWeight = 8; } +message AccountResourceMessage { + int64 freeNetUsed = 1; + int64 freeNetLimit = 2; + int64 NetUsed = 3; + int64 NetLimit = 4; + map assetNetUsed = 5; + map assetNetLimit = 6; + int64 TotalNetLimit = 7; + int64 TotalNetWeight = 8; + + int64 EnergyUsed = 13; + int64 EnergyLimit = 14; + int64 TotalEnergyLimit = 15; + int64 TotalEnergyWeight = 16; + + int64 storageUsed = 21; + int64 storageLimit = 22; +} message PaginatedMessage { int64 offset = 1; int64 limit = 2; } -message EasyTransferMessage{ +message EasyTransferMessage { bytes passPhrase = 1; bytes toAddress = 2; int64 amount = 3; } -message EasyTransferResponse{ +message EasyTransferByPrivateMessage { + bytes privateKey = 1; + bytes toAddress = 2; + int64 amount = 3; +} + +message EasyTransferResponse { Transaction transaction = 1; Return result = 2; + bytes txid = 3; //transaction id = sha256(transaction.rowdata) +} + +message AddressPrKeyPairMessage { + string address = 1; + string privateKey = 2; +} + +message TransactionExtention { + Transaction transaction = 1; + bytes txid = 2; //transaction id = sha256(transaction.rowdata) + repeated bytes constant_result = 3; + Return result = 4; +} + +message BlockExtention { + repeated TransactionExtention transactions = 1; + BlockHeader block_header = 2; + bytes blockid = 3; +} + +message BlockListExtention { + repeated BlockExtention block = 1; +} + +message TransactionListExtention { + repeated TransactionExtention transaction = 1; } \ No newline at end of file diff --git a/app/protobuf/core/Contract.proto b/app/protobuf/core/Contract.proto index 2b6160d..18eafde 100644 --- a/app/protobuf/core/Contract.proto +++ b/app/protobuf/core/Contract.proto @@ -29,12 +29,18 @@ message AccountCreateContract { AccountType type = 3; } -// update account name if the account has no name. +// Update account name. Account name is not unique now. message AccountUpdateContract { bytes account_name = 1; bytes owner_address = 2; } +// Set account id if the account has no id. Account id is unique and case insensitive. +message SetAccountIdContract { + bytes account_id = 1; + bytes owner_address = 2; +} + message TransferContract { bytes owner_address = 1; bytes to_address = 2; @@ -66,6 +72,12 @@ message VoteWitnessContract { bool support = 3; } +message UpdateSettingContract { + bytes owner_address = 1; + bytes contract_address = 2; + int64 consume_user_resource_percent = 3; +} + message WitnessCreateContract { bytes owner_address = 1; bytes url = 2; @@ -90,6 +102,7 @@ message AssetIssueContract { int32 num = 8; int64 start_time = 9; int64 end_time = 10; + int64 order = 11; // the order of tokens of the same name int32 vote_score = 16; bytes description = 20; bytes url = 21; @@ -102,23 +115,28 @@ message AssetIssueContract { message ParticipateAssetIssueContract { bytes owner_address = 1; bytes to_address = 2; - bytes asset_name = 3; // the name of target asset + bytes asset_name = 3; // the namekey of target asset, include name and order int64 amount = 4; // the amount of drops } -message DeployContract { - bytes owner_address = 1; - bytes script = 2; + +enum ResourceCode { + BANDWIDTH = 0x00; + ENERGY = 0x01; } message FreezeBalanceContract { bytes owner_address = 1; int64 frozen_balance = 2; int64 frozen_duration = 3; + + ResourceCode resource = 10; } message UnfreezeBalanceContract { bytes owner_address = 1; + + ResourceCode resource = 10; } message UnfreezeAssetContract { @@ -135,4 +153,77 @@ message UpdateAssetContract { bytes url = 3; int64 new_limit = 4; int64 new_public_limit = 5; +} + +message ProposalCreateContract { + bytes owner_address = 1; + map parameters = 2; +} + +message ProposalApproveContract { + bytes owner_address = 1; + int64 proposal_id = 2; + bool is_add_approval = 3; // add or remove approval +} + +message ProposalDeleteContract { + bytes owner_address = 1; + int64 proposal_id = 2; +} + +message CreateSmartContract { + bytes owner_address = 1; + SmartContract new_contract = 2; +} + +message TriggerSmartContract { + bytes owner_address = 1; + bytes contract_address = 2; + int64 call_value = 3; + bytes data = 4; +} + +message BuyStorageContract { + bytes owner_address = 1; + int64 quant = 2; // trx quantity for buy storage (sun) +} + +message BuyStorageBytesContract { + bytes owner_address = 1; + int64 bytes = 2; // storage bytes for buy +} + +message SellStorageContract { + bytes owner_address = 1; + int64 storage_bytes = 2; +} + +message ExchangeCreateContract { + bytes owner_address = 1; + bytes first_token_id = 2; + int64 first_token_balance = 3; + bytes second_token_id = 4; + int64 second_token_balance = 5; +} + +message ExchangeInjectContract { + bytes owner_address = 1; + int64 exchange_id = 2; + bytes token_id = 3; + int64 quant = 4; +} + +message ExchangeWithdrawContract { + bytes owner_address = 1; + int64 exchange_id = 2; + bytes token_id = 3; + int64 quant = 4; +} + +message ExchangeTransactionContract { + bytes owner_address = 1; + int64 exchange_id = 2; + bytes token_id = 3; + int64 quant = 4; + int64 expected = 5; } \ No newline at end of file diff --git a/app/protobuf/core/Tron.proto b/app/protobuf/core/Tron.proto index a9d4160..38c4f3a 100644 --- a/app/protobuf/core/Tron.proto +++ b/app/protobuf/core/Tron.proto @@ -30,16 +30,50 @@ message Vote { int64 vote_count = 2; } -// Account +// Proposal +message Proposal { + int64 proposal_id = 1; + bytes proposer_address = 2; + map parameters = 3; + int64 expiration_time = 4; + int64 create_time = 5; + repeated bytes approvals = 6; + enum State { + PENDING = 0; + DISAPPROVED = 1; + APPROVED = 2; + CANCELED = 3; + } + State state = 7; +} + +// Exchange +message Exchange { + int64 exchange_id = 1; + bytes creator_address = 2; + int64 create_time = 3; + bytes first_token_id = 6; + int64 first_token_balance = 7; + bytes second_token_id = 8; + int64 second_token_balance = 9; +} + +message ChainParameters { + repeated ChainParameter chainParameter = 1; + message ChainParameter { + string key = 1; + int64 value = 2; + } +} + +/* Account */ message Account { - // frozen balance + /* frozen balance */ message Frozen { - // the frozen trx balance - int64 frozen_balance = 1; - // the expire time - int64 expire_time = 2; + int64 frozen_balance = 1; // the frozen trx balance + int64 expire_time = 2; // the expire time } - + // account nick name bytes account_name = 1; AccountType type = 2; // the create address @@ -50,19 +84,20 @@ message Account { repeated Vote votes = 5; // the other asset owned by this account map asset = 6; - // latest asset operation time + // the frozen balance repeated Frozen frozen = 7; // bandwidth, get from frozen int64 net_usage = 8; + // this account create time - int64 create_time = 9; + int64 create_time = 0x09; // this last operation time, including transfer, voting and so on. //FIXME fix grammar int64 latest_opration_time = 10; // witness block producing allowance - int64 allowance = 11; + int64 allowance = 0x0B; // last withdraw time - int64 latest_withdraw_time = 12; + int64 latest_withdraw_time = 0x0C; // not used so far bytes code = 13; bool is_witness = 14; @@ -72,20 +107,38 @@ message Account { // asset_issued_name bytes asset_issued_name = 17; map latest_asset_operation_time = 18; + int64 free_net_usage = 19; map free_asset_net_usage = 20; int64 latest_consume_time = 21; int64 latest_consume_free_time = 22; + + // the identity of this account, case insensitive + bytes account_id = 23; + + message AccountResource { + // energy resource, get from frozen + int64 energy_usage = 1; + // the frozen balance for energy + Frozen frozen_balance_for_energy = 2; + int64 latest_consume_time_for_energy = 3; + + // storage resource, get from market + int64 storage_limit = 6; + int64 storage_usage = 7; + int64 latest_exchange_storage_time = 8; + } + AccountResource account_resource = 26; + + bytes codeHash = 30; } -//FIXME authority? -message acuthrity { +message authority { AccountId account = 1; bytes permission_name = 2; } -//FIXME permission -message permision { +message permission { AccountId account = 1; } @@ -102,7 +155,6 @@ message Witness { bool isJobs = 9; } - // Vote Change message Votes { bytes address = 1; @@ -131,6 +183,15 @@ message TXOutputs { repeated TXOutput outputs = 1; } +message ResourceReceipt { + int64 energy_usage = 1; + int64 energy_fee = 2; + int64 origin_energy_usage = 3; + int64 energy_usage_total = 4; + int64 net_usage = 5; + int64 net_fee = 6; + Transaction.Result.contractResult result = 7; +} message Transaction { message Contract { @@ -142,7 +203,6 @@ message Transaction { VoteWitnessContract = 4; WitnessCreateContract = 5; AssetIssueContract = 6; - DeployContract = 7; WitnessUpdateContract = 8; ParticipateAssetIssueContract = 9; AccountUpdateContract = 10; @@ -151,13 +211,27 @@ message Transaction { WithdrawBalanceContract = 13; UnfreezeAssetContract = 14; UpdateAssetContract = 15; + ProposalCreateContract = 16; + ProposalApproveContract = 17; + ProposalDeleteContract = 18; + SetAccountIdContract = 19; CustomContract = 20; + // BuyStorageContract = 21; + // BuyStorageBytesContract = 22; + // SellStorageContract = 23; + CreateSmartContract = 30; + TriggerSmartContract = 31; + GetContract = 32; + UpdateSettingContract = 33; + ExchangeCreateContract = 41; + ExchangeInjectContract = 42; + ExchangeWithdrawContract = 43; + ExchangeTransactionContract = 44; } ContractType type = 1; google.protobuf.Any parameter = 2; bytes provider = 3; bytes ContractName = 4; - } message Result { @@ -165,8 +239,28 @@ message Transaction { SUCESS = 0; FAILED = 1; } + enum contractResult { + DEFAULT = 0; + SUCCESS = 1; + REVERT = 2; + BAD_JUMP_DESTINATION = 3; + OUT_OF_MEMORY = 4; + PRECOMPILED_CONTRACT = 5; + STACK_TOO_SMALL = 6; + STACK_TOO_LARGE = 7; + ILLEGAL_OPERATION = 8; + STACK_OVERFLOW = 9; + OUT_OF_ENERGY = 10; + OUT_OF_TIME = 11; + JVM_STACK_OVER_FLOW = 12; + UNKNOWN = 13; + } int64 fee = 1; code ret = 2; + contractResult contractRet = 3; + + int64 withdraw_amount = 15; + int64 unfreeze_amount = 16; } message raw { @@ -174,7 +268,7 @@ message Transaction { int64 ref_block_num = 3; bytes ref_block_hash = 4; int64 expiration = 8; - repeated acuthrity auths = 9; //FIXME authority + repeated authority auths = 9; // data not used bytes data = 10; //only support size = 1, repeated list here for extension @@ -182,6 +276,7 @@ message Transaction { // scripts not used bytes scripts = 12; int64 timestamp = 14; + int64 fee_limit = 18; } raw raw_data = 1; @@ -191,10 +286,28 @@ message Transaction { } message TransactionInfo { - bytes id = 1; - int64 fee = 2; - int64 blockNumber = 3; - int64 blockTimeStamp = 4; + enum code { + SUCESS = 0; + FAILED = 1; + } + message Log { + bytes address = 1; + repeated bytes topics = 2; + bytes data = 3; + } + bytes id = 1; + int64 fee = 2; + int64 blockNumber = 3; + int64 blockTimeStamp = 4; + repeated bytes contractResult = 5; + bytes contract_address = 6; + ResourceReceipt receipt = 7; + repeated Log log = 8; + code result = 9; + bytes resMessage = 10; + + int64 withdraw_amount = 15; + int64 unfreeze_amount = 16; } message Transactions { @@ -216,6 +329,7 @@ message BlockHeader { int64 number = 7; int64 witness_id = 8; bytes witness_address = 9; + int32 version = 10; } raw raw_data = 1; bytes witness_signature = 2; @@ -324,3 +438,48 @@ message HelloMessage { BlockId solidBlockId = 5; BlockId headBlockId = 6; } + +message SmartContract { + message ABI { + message Entry { + enum EntryType { + UnknownEntryType = 0; + Constructor = 1; + Function = 2; + Event = 3; + Fallback = 4; + } + message Param { + bool indexed = 1; + string name = 2; + string type = 3; + // SolidityType type = 3; + } + enum StateMutabilityType { + UnknownMutabilityType = 0; + Pure = 1; + View = 2; + Nonpayable = 3; + Payable = 4; + } + + bool anonymous = 1; + bool constant = 2; + string name = 3; + repeated Param inputs = 4; + repeated Param outputs = 5; + EntryType type = 6; + bool payable = 7; + StateMutabilityType stateMutability = 8; + } + repeated Entry entrys = 1; + } + bytes origin_address = 1; + bytes contract_address = 2; + ABI abi = 3; + bytes bytecode = 4; + int64 call_value = 5; + int64 consume_user_resource_percent = 6; + string name = 7; + +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index 60951ff..208a70c 100644 --- a/build.sbt +++ b/build.sbt @@ -5,14 +5,12 @@ organization := "org.tronscan" version := "latest" - -scalaVersion := "2.12.4" +scalaVersion := "2.12.6" dependencyOverrides ++= Seq( "com.fasterxml.jackson.module" % "jackson-module-scala_2.12" % "2.9.2" ) - // Library Dependencies libraryDependencies ++= Seq( @@ -82,7 +80,12 @@ libraryDependencies ++= Seq( "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test, "com.fasterxml.jackson.module" % "jackson-module-scala_2.12" % "2.9.2" -) ++ grpcDeps ++ akkaDeps ++ circeDependencies ++ akkaStreamsContribDeps +) ++ + grpcDeps ++ + akkaDeps ++ + circeDependencies ++ + akkaStreamsContribDeps ++ + catsDeps // Disable API Documentation sources in (Compile, doc) := Seq.empty @@ -97,3 +100,6 @@ lazy val root = (project in file(".")) scalapb.gen() -> (sourceManaged in Compile).value ), ) + + +scalacOptions += "-Ypartial-unification" \ No newline at end of file diff --git a/conf/application.conf b/conf/application.conf index 02265df..e6742e5 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -84,8 +84,9 @@ cache { # /api/node cache nodes = true } -} + warmer = true +} grpc { balancer { @@ -93,13 +94,16 @@ grpc { } } - # Synchronisation Settings sync { full = true full = ${?ENABLE_SYNC} solidity = true solidity = ${?ENABLE_SYNC} + addresses = true + addresses = ${?ENABLE_SYNC} + votes = true + votes = ${?ENABLE_SYNC} } network { diff --git a/conf/routes b/conf/routes index 62b62c7..b5908a7 100644 --- a/conf/routes +++ b/conf/routes @@ -47,16 +47,21 @@ GET /api/node org.tronscan.api.NodeApi.status GET /api/system/status org.tronscan.api.SystemApi.status GET /api/system/balancer org.tronscan.api.SystemApi.balancer -GET /api/system/sync-accounts org.tronscan.api.AccountApi.resync +#GET /api/system/test org.tronscan.api.SystemApi.test GET /api/system/sync-account/:address org.tronscan.api.AccountApi.sync(address: String) # Votes -GET /api/vote org.tronscan.api.VoteApi.findAll -GET /api/vote/live org.tronscan.api.VoteApi.currentCycle -GET /api/vote/current-cycle org.tronscan.api.VoteApi.candidateTotals -GET /api/vote/next-cycle org.tronscan.api.VoteApi.nextCycle -GET /api/vote/stats org.tronscan.api.VoteApi.stats +GET /api/vote org.tronscan.api.VoteApi.findAll +GET /api/vote/live org.tronscan.api.VoteApi.currentCycle +GET /api/vote/current-cycle org.tronscan.api.VoteApi.candidateTotals +GET /api/vote/next-cycle org.tronscan.api.VoteApi.nextCycle +GET /api/vote/stats org.tronscan.api.VoteApi.stats + +# Vote Rounds + +GET /api/round/:number org.tronscan.api.VoteApi.round(number: Int) +GET /api/round/:number/votes org.tronscan.api.VoteApi.roundVotes(number: Int) # Tokens @@ -66,8 +71,8 @@ GET /api/token/:name/address org.tronscan.api.TokenApi.getAccounts(na # Witnesses -GET /api/witness org.tronscan.api.WitnessApi.findAll -GET /api/witness/maintenance-statistic org.tronscan.api.WitnessApi.maintenanceStatistic +GET /api/witness org.tronscan.api.WitnessApi.findAll +GET /api/witness/maintenance-statistic org.tronscan.api.WitnessApi.maintenanceStatistic # Nodes @@ -95,14 +100,18 @@ POST /api/slack/command org.tronscan.slack.SlackApi.handleComman # GRPC Full Node Api -GET /api/grpc/full/getnowblock org.tronscan.api.GrpcFullApi.getNowBlock -GET /api/grpc/full/getblockbynum/:number org.tronscan.api.GrpcFullApi.getBlockByNum(number: Long) -GET /api/grpc/full/totaltransaction org.tronscan.api.GrpcFullApi.totalTransaction -GET /api/grpc/full/getaccount/:address org.tronscan.api.GrpcFullApi.getAccount(address: String) -GET /api/grpc/full/gettransactionbyid/:hash org.tronscan.api.GrpcFullApi.getTransactionById(hash: String) -GET /api/grpc/full/getaccountnet/:address org.tronscan.api.GrpcFullApi.getAccountNet(address: String) -GET /api/grpc/full/listnodes org.tronscan.api.GrpcFullApi.listNodes -GET /api/grpc/full/listwitnesses org.tronscan.api.GrpcFullApi.listWitnesses +GET /api/grpc/full/getnowblock org.tronscan.api.GrpcFullApi.getNowBlock +GET /api/grpc/full/getblockbynum/:number org.tronscan.api.GrpcFullApi.getBlockByNum(number: Long) +GET /api/grpc/full/getblockbylimitnext org.tronscan.api.GrpcFullApi.getBlockByLimitNext +GET /api/grpc/full/totaltransaction org.tronscan.api.GrpcFullApi.totalTransaction +GET /api/grpc/full/getaccount/:address org.tronscan.api.GrpcFullApi.getAccount(address: String) +GET /api/grpc/full/gettransactionbyid/:hash org.tronscan.api.GrpcFullApi.getTransactionById(hash: String) +GET /api/grpc/full/getaccountnet/:address org.tronscan.api.GrpcFullApi.getAccountNet(address: String) +GET /api/grpc/full/listnodes org.tronscan.api.GrpcFullApi.listNodes +GET /api/grpc/full/listwitnesses org.tronscan.api.GrpcFullApi.listWitnesses +GET /api/grpc/full/listproposals org.tronscan.api.GrpcFullApi.listProposals +GET /api/grpc/full/listexchanges org.tronscan.api.GrpcFullApi.listExchanges +GET /api/grpc/full/getchainparameters org.tronscan.api.GrpcFullApi.getChainParameters # GRPC Solidity API @@ -113,11 +122,13 @@ GET /api/grpc/solidity/listwitnesses org.tronscan.api.GrpcSol # Transaction Builder -POST /api/transaction-builder/contract/transfer org.tronscan.api.TransactionBuilderApi.transfer -POST /api/transaction-builder/contract/transferasset org.tronscan.api.TransactionBuilderApi.transferAsset -POST /api/transaction-builder/contract/accountcreate org.tronscan.api.TransactionBuilderApi.accountCreate -POST /api/transaction-builder/contract/accountupdate org.tronscan.api.TransactionBuilderApi.accountUpdate -POST /api/transaction-builder/contract/withdrawbalance org.tronscan.api.TransactionBuilderApi.withdrawBalance +POST /api/transaction-builder/contract/transfer org.tronscan.api.TransactionBuilderApi.transfer +POST /api/transaction-builder/contract/transferasset org.tronscan.api.TransactionBuilderApi.transferAsset +POST /api/transaction-builder/contract/accountcreate org.tronscan.api.TransactionBuilderApi.accountCreate +POST /api/transaction-builder/contract/accountupdate org.tronscan.api.TransactionBuilderApi.accountUpdate +POST /api/transaction-builder/contract/withdrawbalance org.tronscan.api.TransactionBuilderApi.withdrawBalance +POST /api/transaction-builder/contract/proposalapprove org.tronscan.api.TransactionBuilderApi.proposalApprove +POST /api/transaction-builder/contract/updateasset org.tronscan.api.TransactionBuilderApi.updateAsset # Socket IO diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 7fc6123..28e55bd 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -5,4 +5,5 @@ ADD ./target/universal/stage /opt/tronscan-api CMD /opt/tronscan-api/bin/tronscan \ -J-Xms128M -J-Xmx2048m \ + -Dpidfile.path=/dev/null \ -Dconfig.resource=docker.conf diff --git a/project/Dependencies.scala b/project/Dependencies.scala index faf9851..ed6d34d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,9 +6,9 @@ object Dependencies { val slickPgVersion = "0.16.1" val monixVersion = "2.3.0" val akkaVersion = "2.5.14" - val catsVersion = "0.9.0" val grpcVersion = "1.9.0" val scaleCubeVersion = "1.0.7" + val catsVersion = "1.3.1" val akkaStreamsContribDeps = Seq( "com.typesafe.akka" %% "akka-stream-contrib" % "0.9" @@ -31,9 +31,6 @@ object Dependencies { "com.typesafe.akka" %% "akka-stream-testkit" ).map(_ % akkaVersion) - val catsDeps = Seq( - "org.typelevel" %% "cats" % catsVersion) - val macroParadise = addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) val scalaAsync = Seq( @@ -52,4 +49,10 @@ object Dependencies { "io.scalecube" % "scalecube-cluster", "io.scalecube" % "scalecube-transport" ).map(_ % scaleCubeVersion) + + val catsDeps = Seq( + "org.typelevel" %% "cats-core" + ).map(_ % catsVersion) ++ Seq( + "org.typelevel" %% "cats-effect" % "1.0.0" + ) } diff --git a/project/build.properties b/project/build.properties index 7c81737..5620cc5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.5 +sbt.version=1.2.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index c2ef3f2..414fdde 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.13") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.18") addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") \ No newline at end of file diff --git a/project/scaffold.sbt b/project/scaffold.sbt deleted file mode 100644 index e1139bf..0000000 --- a/project/scaffold.sbt +++ /dev/null @@ -1,5 +0,0 @@ -// Defines scaffolding (found under .g8 folder) -// http://www.foundweekends.org/giter8/scaffolding.html -// sbt "g8Scaffold form" -// not working on sbt 1.0 -//addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.10.0") diff --git a/project/scalapb.sbt b/project/scalapb.sbt index 93742e9..2a20c38 100644 --- a/project/scalapb.sbt +++ b/project/scalapb.sbt @@ -1,3 +1,3 @@ addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.18") -libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.1" \ No newline at end of file +libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.4" \ No newline at end of file diff --git a/schema/schema.sql b/schema/schema.sql index 0385492..a64c8b4 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -1,5 +1,33 @@ +create database "tron-explorer" +; + +create schema if not exists analytics +; + +create sequence analytics.vote_snapshot_id_seq +; + +create table analytics.vote_snapshot +( + id bigserial not null + constraint vote_snapshot_pkey + primary key, + address text not null, + timestamp timestamp with time zone default now() not null, + votes bigint default 0 not null +) +; + +create table analytics.requests +( + id uuid not null, + timestamp timestamp with time zone default now() not null, + host text default ''::text, + uri text default ''::text +) +; -create table if not exists blocks +create table blocks ( id bigint not null constraint blocks_pkey @@ -16,13 +44,10 @@ create table if not exists blocks ) ; -create table if not exists transactions +create table transactions ( date_created timestamp with time zone, - block bigint - constraint transactions_blocks_id_fk - references blocks - on update cascade on delete cascade, + block bigint, hash text not null constraint transactions_hash_pk primary key, @@ -31,33 +56,42 @@ create table if not exists transactions contract_type integer default '-1'::integer not null, owner_address text default ''::text not null, to_address text default ''::text not null, - data text default ''::text not null + data text default ''::text not null, + fee bigint ) ; -create index if not exists transactions_date_created_index +create index transactions_date_created_index on transactions (date_created desc) ; -create index if not exists transactions_block_hash_index +create index transactions_block_hash_index on transactions (block, hash) ; -create table if not exists vote_witness_contract +create index transactions_date_created_owner_address_contract_type_block_ind + on transactions (owner_address, date_created, block, contract_type) +; + +create index transactions_owner_address_date_created_index + on transactions (owner_address, date_created) +; + +create table vote_witness_contract ( - id uuid not null - constraint vote_witness_contract_id_pk - primary key, + id text not null, transaction text, voter_address text, - candidate_address text, + candidate_address text not null, votes bigint, date_created timestamp with time zone, - block bigint + block bigint, + constraint vote_witness_contract_id_candidate_address_pk + primary key (id, candidate_address) ) ; -create table if not exists participate_asset_issue +create table participate_asset_issue ( id uuid not null constraint participate_asset_issue_id_pk @@ -72,14 +106,14 @@ create table if not exists participate_asset_issue ) ; -create table if not exists accounts +create table accounts ( address text not null constraint accounts_pkey primary key, - name text, - balance bigint, - token_balances jsonb, + name text default ''::text, + balance bigint default 0, + token_balances jsonb default '{}'::jsonb, date_created timestamp with time zone default now() not null, date_updated timestamp with time zone default now() not null, date_synced timestamp with time zone default now() not null, @@ -87,7 +121,7 @@ create table if not exists accounts ) ; -create table if not exists asset_issue_contract +create table asset_issue_contract ( id uuid constraint asset_issue_contract_id_pk @@ -99,7 +133,6 @@ create table if not exists asset_issue_contract num integer, date_end timestamp with time zone, date_start timestamp with time zone, - decay_ratio integer, vote_score integer, description text, url text, @@ -111,7 +144,7 @@ create table if not exists asset_issue_contract ) ; -create table if not exists address_balance +create table address_balance ( address text, token text, @@ -121,7 +154,7 @@ create table if not exists address_balance ) ; -create table if not exists witness_create_contract +create table witness_create_contract ( address text not null constraint witness_create_contract_pkey @@ -130,7 +163,7 @@ create table if not exists witness_create_contract ) ; -create table if not exists ip_geo +create table ip_geo ( ip text, city text, @@ -140,7 +173,7 @@ create table if not exists ip_geo ) ; -create table if not exists sr_account +create table sr_account ( address text not null constraint sr_account_pkey @@ -149,20 +182,7 @@ create table if not exists sr_account ) ; -CREATE SCHEMA IF NOT EXISTS analytics; - -create table if not exists analytics.vote_snapshot -( - id bigserial not null - constraint vote_snapshot_pkey - primary key, - address text not null, - timestamp timestamp with time zone default now() not null, - votes bigint default 0 not null -) -; - -create table if not exists transfers +create table transfers ( id uuid not null constraint transfers_pkey @@ -172,55 +192,41 @@ create table if not exists transfers transfer_to_address text, amount bigint default 0, token_name text default 'TRX'::text, - block bigint - constraint transfers_blocks_id_fk - references blocks - on update cascade on delete cascade, + block bigint, transaction_hash text not null, confirmed boolean default false not null ) ; -create index if not exists transfers_transfer_from_address_transfer_to_address_date_cre +create index transfers_transfer_from_address_transfer_to_address_date_cre on transfers (transfer_from_address asc, transfer_to_address asc, date_created desc) ; -create index if not exists transfers_block_hash_index +create index transfers_block_hash_index on transfers (block, transaction_hash) ; -create index if not exists transfers_date_created_index +create index transfers_date_created_index on transfers (date_created desc) ; -create index if not exists transfers_transfer_from_address_date_created_index +create index transfers_transfer_from_address_date_created_index on transfers (transfer_from_address asc, date_created desc) ; -create index if not exists transfers_transfer_to_address_date_created_index +create index transfers_transfer_to_address_date_created_index on transfers (transfer_to_address asc, date_created desc) ; -create index if not exists transfers_transfer_from_address_index +create index transfers_transfer_from_address_index on transfers (transfer_from_address) ; -create index if not exists transfers_transfer_to_address_index +create index transfers_transfer_to_address_index on transfers (transfer_to_address) ; -create table if not exists analytics.requests -( - id uuid not null, - timestamp timestamp with time zone default now() not null, - host text default ''::text, - uri text default ''::text, - referer text default ''::text not null, - ip text default ''::text not null -) -; - -create table if not exists trx_request +create table trx_request ( address text not null constraint trx_request_pkey @@ -230,9 +236,33 @@ create table if not exists trx_request ) ; -create table if not exists funds +create table funds ( - id int, + id integer, address text ) ; + +create table maintenance_round +( + block bigint not null + constraint maintenance_rounds_pkey + primary key, + number integer default 1, + date_start timestamp with time zone default now(), + date_end timestamp with time zone, + timestamp bigint +) +; + +create table round_votes +( + address text not null, + round integer not null, + candidate text not null, + votes bigint, + constraint round_votes_address_round_candidate_pk + primary key (address, round, candidate) +) +; + diff --git a/test/org/tronscan/grpc/GrpcSpec.scala b/test/org/tronscan/grpc/GrpcSpec.scala new file mode 100644 index 0000000..8be9a3e --- /dev/null +++ b/test/org/tronscan/grpc/GrpcSpec.scala @@ -0,0 +1,44 @@ +package org +package tronscan.grpc + +import io.grpc.ManagedChannelBuilder +import org.specs2.mutable._ +import org.tron.api.api.{BlockLimit, WalletGrpc} + +import scala.concurrent.Future + +class GrpcSpec extends Specification { + + "GRPC" should { + + "Read BlockLimit Next" in { + + import scala.concurrent.ExecutionContext.Implicits.global + + val channel = ManagedChannelBuilder + .forAddress("47.254.146.147", 50051) + .usePlaintext() + .build + + val wallet = WalletGrpc.stub(channel) + + val blocks = for (i <- 1 to 500 by 50) yield i + + val done = Future.sequence(blocks.sliding(2).toList.map { case Seq(now, next) => + wallet.getBlockByLimitNext(BlockLimit(now, next)).map { result => + val resultBlocks = result.block.sortBy(_.getBlockHeader.getRawData.number).map(_.getBlockHeader.getRawData.number) + if (resultBlocks.head != now) { + println(s"INVALID HEAD ${resultBlocks.head} => $now") + } + if (resultBlocks.last != next) { + println(s"INVALID HEAD ${resultBlocks.last} => $next") + } + resultBlocks + } + }) + + awaitSync(done) + ok + } + } +}