Skip to content

Commit

Permalink
Merge pull request #540 from alephium/address-public-key
Browse files Browse the repository at this point in the history
Add `/addresses/<address>/public-key` endpoint
  • Loading branch information
tdroxler authored May 21, 2024
2 parents 336efb7 + 634b8f3 commit 9efbd57
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 4 deletions.
99 changes: 99 additions & 0 deletions app/src/main/resources/explorer-backend-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2536,6 +2536,105 @@
}
}
},
"/addresses/{address}/public-key": {
"get": {
"tags": [
"Addresses"
],
"description": "Get public key of p2pkh addresses, the address needs to have at least one input.",
"operationId": "getAddressesAddressPublic-key",
"parameters": [
{
"name": "address",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "address"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string",
"format": "public-key"
},
"example": "d1b70d2226308b46da297486adb6b4f1a8c1842cb159ac5ec04f384fe2d6f5da28"
}
}
},
"400": {
"description": "BadRequest",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequest"
},
"example": {
"detail": "Something bad in the request"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Unauthorized"
},
"example": {
"detail": "You shall not pass"
}
}
}
},
"404": {
"description": "NotFound",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFound"
},
"example": {
"resource": "wallet-name",
"detail": "wallet-name not found"
}
}
}
},
"500": {
"description": "InternalServerError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InternalServerError"
},
"example": {
"detail": "Ouch"
}
}
}
},
"503": {
"description": "ServiceUnavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailable"
},
"example": {
"detail": "Self clique unsynced"
}
}
}
}
}
}
},
"/addresses/{address}/tokens-balance": {
"get": {
"tags": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.alephium.api.Endpoints.jsonBody
import org.alephium.api.model.TimeInterval
import org.alephium.explorer.api.EndpointExamples._
import org.alephium.explorer.api.model._
import org.alephium.protocol.PublicKey
import org.alephium.protocol.model.{Address, TokenId}
import org.alephium.util.Duration

Expand Down Expand Up @@ -174,6 +175,14 @@ trait AddressesEndpoints extends BaseEndpoint with QueryParams {
.in(intervalTypeQuery)
.out(jsonBody[AmountHistory])

val getPublicKey: BaseEndpoint[Address, PublicKey] =
addressesEndpoint.get
.in("public-key")
.out(jsonBody[PublicKey])
.description(
"Get public key of p2pkh addresses, the address needs to have at least one input."
)

private case class TextCsv() extends CodecFormat {
override val mediaType: MediaType = MediaType.TextCsv
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.alephium.api.EndpointsExamples
import org.alephium.api.model.{Amount, ValBool}
import org.alephium.explorer.api.model._
import org.alephium.explorer.persistence.queries.ExplainResult
import org.alephium.protocol.ALPH
import org.alephium.protocol.{ALPH, PublicKey}
import org.alephium.protocol.mining.HashRate
import org.alephium.protocol.model.{Address, BlockHash, ContractId, GroupIndex, TokenId}
import org.alephium.util.{Hex, U256}
Expand All @@ -47,8 +47,9 @@ object EndpointExamples extends EndpointsExamples {
private val outputRef: OutputRef =
OutputRef(hint = 23412, key = hash)

private val publicKey = "d1b70d2226308b46da297486adb6b4f1a8c1842cb159ac5ec04f384fe2d6f5da28"
private val unlockScript: ByteString =
Hex.unsafe("d1b70d2226308b46da297486adb6b4f1a8c1842cb159ac5ec04f384fe2d6f5da28")
Hex.unsafe(publicKey)

private val address1: Address =
Address.fromBase58("1AujpupFP4KWeZvqA7itsHY9cLJmx4qTzojVZrg8W9y9n").get
Expand Down Expand Up @@ -416,4 +417,7 @@ object EndpointExamples extends EndpointsExamples {

implicit val priceExample: List[Example[ArraySeq[Option[Double]]]] =
simpleExample(ArraySeq(Option(0.01)))

implicit val publicKeyExample: List[Example[PublicKey]] =
simpleExample(PublicKey.unsafe(Hex.unsafe(publicKey)))
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ trait Documentation
listAddressTokens,
listAddressTokenTransactions,
getAddressTokenBalance,
getPublicKey,
listAddressTokensBalance,
areAddressesActive,
exportTransactionsCsvByAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.alephium.explorer.persistence.queries
import scala.collection.immutable.ArraySeq
import scala.concurrent.ExecutionContext

import akka.util.ByteString
import slick.dbio.DBIOAction
import slick.jdbc._
import slick.jdbc.PostgresProfile.api._
Expand All @@ -30,7 +31,7 @@ import org.alephium.explorer.persistence.schema.CustomGetResult._
import org.alephium.explorer.persistence.schema.CustomSetParameter._
import org.alephium.explorer.util.SlickExplainUtil._
import org.alephium.explorer.util.SlickUtil._
import org.alephium.protocol.model.{BlockHash, TransactionId}
import org.alephium.protocol.model.{Address, BlockHash, TransactionId}

object InputQueries {

Expand Down Expand Up @@ -160,6 +161,17 @@ object InputQueries {
ORDER BY block_timestamp #${if (ascendingOrder) "" else "DESC"}
""".asASE[InputEntity](inputGetResult)

def getUnlockScript(address: Address)(implicit
ec: ExecutionContext
): DBActionR[Option[ByteString]] = {
sql"""
select unlock_script
FROM inputs
WHERE output_ref_address = $address
LIMIT 1
""".asAS[ByteString].headOrNone
}

/** Runs explain on query `inputsFromTxs` and checks the index `inputs_tx_hash_block_hash_idx` is
* being used
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._
import scala.jdk.FutureConverters._

import akka.util.ByteString
import io.reactivex.rxjava3.core.Flowable
import io.vertx.core.buffer.Buffer
import slick.basic.DatabaseConfig
Expand All @@ -33,6 +34,7 @@ import org.alephium.explorer.api.model._
import org.alephium.explorer.cache.TransactionCache
import org.alephium.explorer.persistence.DBRunner._
import org.alephium.explorer.persistence.dao.{MempoolDao, TransactionDao}
import org.alephium.explorer.persistence.queries.InputQueries
import org.alephium.explorer.persistence.queries.TransactionQueries._
import org.alephium.explorer.util.TimeUtil
import org.alephium.protocol.ALPH
Expand Down Expand Up @@ -96,6 +98,10 @@ trait TransactionService {
dc: DatabaseConfig[PostgresProfile]
): Future[Boolean]

def getUnlockScript(
address: Address
)(implicit ec: ExecutionContext, dc: DatabaseConfig[PostgresProfile]): Future[Option[ByteString]]

def exportTransactionsByAddress(
address: Address,
fromTime: TimeStamp,
Expand Down Expand Up @@ -225,6 +231,13 @@ object TransactionService extends TransactionService {
)
}

def getUnlockScript(address: Address)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[Option[ByteString]] = {
run(InputQueries.getUnlockScript(address))
}

@SuppressWarnings(Array("org.wartremover.warts.OptionPartial"))
def getAmountHistoryDEPRECATED(
address: Address,
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/scala/org/alephium/explorer/web/AddressServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.alephium.explorer.web
import scala.collection.immutable.ArraySeq
import scala.concurrent.{ExecutionContext, Future}

import akka.util.ByteString
import io.vertx.core.buffer.Buffer
import io.vertx.core.streams.ReadStream
import io.vertx.ext.web._
Expand All @@ -34,7 +35,10 @@ import org.alephium.explorer.api.AddressesEndpoints
import org.alephium.explorer.api.model._
import org.alephium.explorer.config.ExplorerConfig
import org.alephium.explorer.service.{TokenService, TransactionService}
import org.alephium.protocol.PublicKey
import org.alephium.protocol.model.Address
import org.alephium.protocol.vm.{LockupScript, UnlockScript}
import org.alephium.serde._
import org.alephium.util.Duration

class AddressServer(
Expand Down Expand Up @@ -157,6 +161,19 @@ class AddressServer(
})
}
}
}),
route(getPublicKey.serverLogic[Future] { address =>
address match {
case Address.Asset(LockupScript.P2PKH(_)) =>
transactionService.getUnlockScript(address).map {
case None =>
Left(ApiError.NotFound(s"No input found for address $address"))
case Some(unlockScript) =>
AddressServer.bytesToPublicKey(unlockScript)
}
case _ =>
Future.successful(Left(ApiError.BadRequest(s"Only P2PKH addresses supported")))
}
})
)

Expand Down Expand Up @@ -206,4 +223,18 @@ object AddressServer {
def amountHistoryFileNameHeader(address: Address, timeInterval: TimeInterval): String = {
s"""attachment;filename="$address-amount-history-${timeInterval.from.millis}-${timeInterval.to.millis}.json""""
}

def bytesToPublicKey(
unlockScript: ByteString
): Either[ApiError[_ <: StatusCode], PublicKey] =
deserialize[UnlockScript](unlockScript).left
.map { error =>
ApiError.InternalServerError(s"Failed to deserialize unlock script: $error")
}
.flatMap {
case UnlockScript.P2PKH(publickKey) =>
Right(publickKey)
case _ =>
Left(ApiError.BadRequest(s"Invalid unlock script, require P2PKH"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import java.math.BigInteger
import scala.collection.immutable.ArraySeq
import scala.concurrent.{ExecutionContext, Future}

import akka.util.ByteString
import io.reactivex.rxjava3.core.Flowable
import io.vertx.core.buffer.Buffer
import slick.basic.DatabaseConfig
Expand Down Expand Up @@ -99,6 +100,13 @@ trait EmptyTransactionService extends TransactionService {
dc: DatabaseConfig[PostgresProfile]
): Future[Boolean] = ???

def getUnlockScript(
address: Address
)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[Option[ByteString]] = Future.successful(None)

def exportTransactionsByAddress(
address: Address,
from: TimeStamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,25 @@ class TransactionServiceSpec extends AlephiumActorSpecLike with DatabaseFixtureF
}
}

"get unlock script" in new Fixture {

val blocks = Gen
.listOfN(20, blockEntityGen(chainIndex))
.sample
.get

BlockDao.insertAll(blocks).futureValue

val addresses = blocks.flatMap(_.inputs.flatMap(_.outputRefAddress)).distinct

addresses.foreach { address =>
TransactionService
.getUnlockScript(address)
.futureValue
.isDefined is true
}
}

trait Fixture {
implicit val blockCache: BlockCache = TestBlockCache()

Expand Down
Loading

0 comments on commit 9efbd57

Please sign in to comment.