mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
Implement Oracle Explorer Client (#2838)
* WIP * Get POSt working for creating an announcement * Add ability POST oracle attestations * Add docs * Remove ExplorerMain * Update workflows to add oracleExplorerClient/test * Switch to test so it passes on CI * Add unit test, switch env to ExplorerEnv.Test * Remove extra comments * Add StringFactory to ExplorerEnv
This commit is contained in:
parent
a275668734
commit
49b6d39ab4
14 changed files with 423 additions and 2 deletions
|
@ -25,4 +25,4 @@ jobs:
|
|||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.12.12 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test asyncUtilsTestJVM/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
|
||||
run: sbt ++2.12.12 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test asyncUtilsTestJVM/test oracleExplorerClient/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
|
||||
|
|
|
@ -25,4 +25,4 @@ jobs:
|
|||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.5 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test asyncUtilsTestJVM/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
|
||||
run: sbt ++2.13.5 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test asyncUtilsTestJVM/test oracleExplorerClient/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
|
||||
|
|
|
@ -21,6 +21,10 @@ import org.bitcoins.core.protocol.script.{
|
|||
WitnessVersion,
|
||||
WitnessVersion0
|
||||
}
|
||||
import org.bitcoins.core.protocol.tlv.{
|
||||
OracleAnnouncementV0TLV,
|
||||
OracleAttestmentV0TLV
|
||||
}
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.protocol.{
|
||||
Address,
|
||||
|
@ -652,6 +656,44 @@ object JsonReaders {
|
|||
}
|
||||
}
|
||||
|
||||
implicit object OracleAnnouncementV0TLVReads
|
||||
extends Reads[OracleAnnouncementV0TLV] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[OracleAnnouncementV0TLV] =
|
||||
json match {
|
||||
case JsString(s) =>
|
||||
OracleAnnouncementV0TLV.fromHexT(s) match {
|
||||
case Success(ann) => JsSuccess(ann)
|
||||
case Failure(err) =>
|
||||
SerializerUtil.buildJsErrorMsg(
|
||||
s"Unexpected Service Identifier: $err",
|
||||
json)
|
||||
}
|
||||
case err @ (JsNull | _: JsBoolean | _: JsNumber | _: JsArray |
|
||||
_: JsObject) =>
|
||||
SerializerUtil.buildJsErrorMsg("jsstring", err)
|
||||
}
|
||||
}
|
||||
|
||||
implicit object OracleAttestmentV0TLVReads
|
||||
extends Reads[OracleAttestmentV0TLV] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[OracleAttestmentV0TLV] =
|
||||
json match {
|
||||
case JsString(s) =>
|
||||
OracleAttestmentV0TLV.fromHexT(s) match {
|
||||
case Success(att) => JsSuccess(att)
|
||||
case Failure(err) =>
|
||||
SerializerUtil.buildJsErrorMsg(
|
||||
s"Unexpected Service Identifier: $err",
|
||||
json)
|
||||
}
|
||||
case err @ (JsNull | _: JsBoolean | _: JsNumber | _: JsArray |
|
||||
_: JsObject) =>
|
||||
SerializerUtil.buildJsErrorMsg("jsstring", err)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val feeProportionalMillionthsReads: Reads[
|
||||
FeeProportionalMillionths] = Reads { js =>
|
||||
SerializerUtil.processJsNumberBigInt(FeeProportionalMillionths.fromBigInt)(
|
||||
|
|
11
build.sbt
11
build.sbt
|
@ -198,6 +198,7 @@ lazy val `bitcoin-s` = project
|
|||
testkitCoreJS,
|
||||
testkit,
|
||||
zmq,
|
||||
oracleExplorerClient,
|
||||
oracleServer,
|
||||
oracleServerTest,
|
||||
serverRoutes
|
||||
|
@ -241,6 +242,7 @@ lazy val `bitcoin-s` = project
|
|||
appCommonsTest,
|
||||
testkit,
|
||||
zmq,
|
||||
oracleExplorerClient,
|
||||
oracleServer,
|
||||
oracleServerTest,
|
||||
serverRoutes
|
||||
|
@ -741,6 +743,15 @@ lazy val dlcOracleTest = project
|
|||
)
|
||||
.dependsOn(coreJVM % testAndCompile, dlcOracle, testkit)
|
||||
|
||||
lazy val oracleExplorerClient = project
|
||||
.in(file("oracle-explorer-client"))
|
||||
.settings(CommonSettings.settings: _*)
|
||||
.settings(
|
||||
name := "bitcoin-s-oracle-explorer-client",
|
||||
libraryDependencies ++= Deps.oracleExplorerClient
|
||||
)
|
||||
.dependsOn(coreJVM, appCommons, testkit % "test->test")
|
||||
|
||||
/** Given a database name, returns the appropriate
|
||||
* Flyway settings we apply to a project (chain, node, wallet)
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package org.bitcoins.explorer.client
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.model.{
|
||||
ContentTypes,
|
||||
HttpEntity,
|
||||
HttpMethods,
|
||||
HttpRequest,
|
||||
Uri
|
||||
}
|
||||
import akka.http.scaladsl.{Http, HttpExt}
|
||||
import akka.util.ByteString
|
||||
import org.bitcoins.crypto.Sha256Digest
|
||||
import org.bitcoins.explorer.env.ExplorerEnv
|
||||
import org.bitcoins.explorer.model.{
|
||||
CreateAnnouncementExplorer,
|
||||
CreateAttestations,
|
||||
SbAnnouncementEvent
|
||||
}
|
||||
import org.bitcoins.explorer.picklers.ExplorerPicklers
|
||||
import play.api.libs.json.{JsError, JsSuccess, JsValue, Json}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/** A class that implements the Suredbits oracle explorer API */
|
||||
case class SbExplorerClient(env: ExplorerEnv)(implicit system: ActorSystem) {
|
||||
import ExplorerPicklers._
|
||||
import system.dispatcher
|
||||
private val httpClient: HttpExt = Http(system)
|
||||
|
||||
/** Lists all events on oracle explorer
|
||||
* @see https://gist.github.com/Christewart/a9e55d9ba582ac9a5ceffa96db9d7e1f#list-all-events
|
||||
* @return
|
||||
*/
|
||||
def listEvents(): Future[Vector[SbAnnouncementEvent]] = {
|
||||
val base = env.baseUri
|
||||
val uri = Uri(base + "events")
|
||||
val httpReq = HttpRequest(uri = uri)
|
||||
val responseF = sendRequest(httpReq)
|
||||
responseF.flatMap { response =>
|
||||
val result = response.validate[Vector[SbAnnouncementEvent]]
|
||||
result match {
|
||||
case success: JsSuccess[Vector[SbAnnouncementEvent]] =>
|
||||
Future.successful(success.value)
|
||||
case err: JsError =>
|
||||
Future.failed(
|
||||
new RuntimeException(
|
||||
s"Failed to parse response for listevents, err=$err"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets an announcement from the oracle explorer
|
||||
* @see https://gist.github.com/Christewart/a9e55d9ba582ac9a5ceffa96db9d7e1f#get-event
|
||||
*/
|
||||
def getEvent(announcementHash: Sha256Digest): Future[SbAnnouncementEvent] = {
|
||||
val base = env.baseUri
|
||||
val uri = Uri(base + s"events/${announcementHash.hex}")
|
||||
val httpReq = HttpRequest(uri = uri)
|
||||
val responseF = sendRequest(httpReq)
|
||||
responseF.flatMap { response =>
|
||||
val result = response.validate[SbAnnouncementEvent]
|
||||
result match {
|
||||
case success: JsSuccess[SbAnnouncementEvent] =>
|
||||
Future.successful(success.value)
|
||||
case err: JsError =>
|
||||
Future.failed(
|
||||
new RuntimeException(
|
||||
s"Failed to parse response for listevents, err=$err"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an announcement on the oracle explorer
|
||||
* @see https://gist.github.com/Christewart/a9e55d9ba582ac9a5ceffa96db9d7e1f#create-an-event
|
||||
*/
|
||||
def createAnnouncement(
|
||||
oracleEventExplorer: CreateAnnouncementExplorer): Future[Unit] = {
|
||||
val base = env.baseUri
|
||||
val uri = Uri(base + s"events")
|
||||
val string = oracleEventExplorer.toString
|
||||
val httpReq =
|
||||
HttpRequest(
|
||||
uri = uri,
|
||||
method = HttpMethods.POST,
|
||||
entity =
|
||||
HttpEntity(ContentTypes.`application/x-www-form-urlencoded`, string))
|
||||
val responseF = sendRequest(httpReq)
|
||||
responseF.map(_ => ())
|
||||
}
|
||||
|
||||
/** Creates an attestation for an announcement on the oracle explorer
|
||||
* @see https://gist.github.com/Christewart/a9e55d9ba582ac9a5ceffa96db9d7e1f#create-an-events-attestation
|
||||
*/
|
||||
def createAttestations(attestations: CreateAttestations): Future[Unit] = {
|
||||
val base = env.baseUri
|
||||
val uri = Uri(
|
||||
base + s"events/${attestations.announcementHash.hex}/attestations")
|
||||
val string = attestations.toString
|
||||
val httpReq =
|
||||
HttpRequest(
|
||||
uri = uri,
|
||||
method = HttpMethods.POST,
|
||||
entity =
|
||||
HttpEntity(ContentTypes.`application/x-www-form-urlencoded`, string))
|
||||
val responseF = sendRequest(httpReq)
|
||||
responseF.map(_ => ())
|
||||
}
|
||||
|
||||
private def sendRequest(httpReq: HttpRequest): Future[JsValue] = {
|
||||
|
||||
val responsePayloadF: Future[String] = {
|
||||
httpClient
|
||||
.singleRequest(httpReq)
|
||||
.flatMap(response =>
|
||||
response.entity.dataBytes
|
||||
.runFold(ByteString.empty)(_ ++ _)
|
||||
.map(payload => payload.decodeString(ByteString.UTF_8)))
|
||||
}
|
||||
|
||||
responsePayloadF.map(Json.parse)
|
||||
}
|
||||
}
|
34
oracle-explorer-client/src/main/scala/org/bitcoins/explorer/env/ExplorerEnv.scala
vendored
Normal file
34
oracle-explorer-client/src/main/scala/org/bitcoins/explorer/env/ExplorerEnv.scala
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
package org.bitcoins.explorer.env
|
||||
|
||||
import org.bitcoins.crypto.StringFactory
|
||||
|
||||
sealed trait ExplorerEnv {
|
||||
def baseUri: String
|
||||
}
|
||||
|
||||
object ExplorerEnv extends StringFactory[ExplorerEnv] {
|
||||
|
||||
case object Production extends ExplorerEnv {
|
||||
override val baseUri: String = "https://oracle.suredbits.com/v1/"
|
||||
}
|
||||
|
||||
case object Test extends ExplorerEnv {
|
||||
override val baseUri: String = "https://test.oracle.suredbits.com/v1/"
|
||||
}
|
||||
|
||||
/** For local testing purposes */
|
||||
case object Local extends ExplorerEnv {
|
||||
override val baseUri: String = "http://localhost:9000/v1/"
|
||||
}
|
||||
|
||||
val all: Vector[ExplorerEnv] = Vector(Production, Test, Local)
|
||||
|
||||
override def fromString(string: String): ExplorerEnv = {
|
||||
val explorerEnvOpt = all.find(_.toString.toLowerCase == string)
|
||||
explorerEnvOpt match {
|
||||
case Some(env) => env
|
||||
case None =>
|
||||
sys.error(s"Failed to parse explorer env from str=$string")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.bitcoins.explorer.model
|
||||
|
||||
import org.bitcoins.core.protocol.tlv.OracleAnnouncementV0TLV
|
||||
|
||||
case class CreateAnnouncementExplorer(
|
||||
oracleAnnouncementV0: OracleAnnouncementV0TLV,
|
||||
oracleName: String,
|
||||
description: String,
|
||||
eventURI: Option[String]) {
|
||||
|
||||
override def toString: String = {
|
||||
val base =
|
||||
s"oracleAnnouncementV0=${oracleAnnouncementV0.hex}&description=$description&oracleName=$oracleName"
|
||||
eventURI match {
|
||||
case None => base
|
||||
case Some(uri) =>
|
||||
val uriString = s"&uri=$uri"
|
||||
base + uriString
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.bitcoins.explorer.model
|
||||
|
||||
import org.bitcoins.core.protocol.tlv.OracleAttestmentV0TLV
|
||||
import org.bitcoins.crypto.Sha256Digest
|
||||
|
||||
case class CreateAttestations(
|
||||
announcementHash: Sha256Digest,
|
||||
attestment: OracleAttestmentV0TLV) {
|
||||
|
||||
override def toString: String = {
|
||||
s"attestations=${attestment.hex}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.bitcoins.explorer.model
|
||||
|
||||
import org.bitcoins.core.protocol.tlv.{
|
||||
OracleAnnouncementV0TLV,
|
||||
OracleAttestmentV0TLV
|
||||
}
|
||||
import org.bitcoins.crypto.Sha256Digest
|
||||
|
||||
case class SbAnnouncementEvent(
|
||||
id: Sha256Digest,
|
||||
oracleName: String,
|
||||
description: String,
|
||||
uri: Option[String],
|
||||
announcement: OracleAnnouncementV0TLV,
|
||||
attestations: Option[OracleAttestmentV0TLV],
|
||||
outcome: Option[String])
|
|
@ -0,0 +1,10 @@
|
|||
package org.bitcoins.explorer.picklers
|
||||
|
||||
import org.bitcoins.commons.serializers.JsonReaders
|
||||
import org.bitcoins.explorer.model.SbAnnouncementEvent
|
||||
import play.api.libs.json.Json
|
||||
|
||||
object ExplorerPicklers {
|
||||
import JsonReaders._
|
||||
implicit val explorerEventRW = Json.reads[SbAnnouncementEvent]
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package org.bitcoins.explorer.client
|
||||
|
||||
import org.bitcoins.core.protocol.tlv.{
|
||||
OracleAnnouncementV0TLV,
|
||||
OracleAttestmentV0TLV
|
||||
}
|
||||
import org.bitcoins.crypto.Sha256Digest
|
||||
import org.bitcoins.explorer.env.ExplorerEnv
|
||||
import org.bitcoins.explorer.model.{
|
||||
CreateAnnouncementExplorer,
|
||||
CreateAttestations,
|
||||
SbAnnouncementEvent
|
||||
}
|
||||
import org.bitcoins.testkit.util.BitcoinSAsyncTest
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class SbExplorerClientTest extends BitcoinSAsyncTest {
|
||||
|
||||
behavior of "SbExplorerClient"
|
||||
|
||||
val explorerClient = SbExplorerClient(ExplorerEnv.Test)
|
||||
|
||||
//https://test.oracle.suredbits.com/event/57505dcdfe8746d9adf3454df538244a425f302c07642d9dc4a4f635fbf08d30
|
||||
private val announcementHex: String =
|
||||
"fdd824b33cbc4081b947ea9d05e616b010b563bfdbc42a2d20effa6f169f8e4be732b10d5461fa84b5739876a0c8a7bdb717040b8ee5907fe7e60694199ba948ecd505b01d5dcdba2e64cb116cc0c375a0856298f0058b778f46bfe625ac6576204889e4fdd8224f0001efdf735567ae0a00a515e313d20029de5d7525da7b8367bc843d28b672d4db4d605bd280fdd80609000203594553024e4f1b323032312d30332d32342d73756e6e792d696e2d6368696361676f"
|
||||
|
||||
private val announcement: OracleAnnouncementV0TLV =
|
||||
OracleAnnouncementV0TLV.fromHex(announcementHex)
|
||||
|
||||
it must "list events" in {
|
||||
val eventsF: Future[Vector[SbAnnouncementEvent]] =
|
||||
explorerClient.listEvents()
|
||||
for {
|
||||
events <- eventsF
|
||||
} yield {
|
||||
assert(events.nonEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
it must "get an event" in {
|
||||
val hash = announcement.sha256
|
||||
val eventsF = explorerClient.getEvent(hash)
|
||||
for {
|
||||
event <- eventsF
|
||||
} yield {
|
||||
assert(event.announcement.sha256 == hash)
|
||||
}
|
||||
}
|
||||
|
||||
it must "return failure from get an event if the event DNE" in {
|
||||
val hash = Sha256Digest.empty
|
||||
recoverToSucceededIf[RuntimeException] {
|
||||
explorerClient.getEvent(hash)
|
||||
}
|
||||
}
|
||||
|
||||
it must "create an event on the oracle explorer and then get that event" ignore {
|
||||
|
||||
val oracleName = "Chris_Stewart_5"
|
||||
val description = "2021-03-24-sunny-in-chicago"
|
||||
val uriOpt = Some("https://twitter.com/Chris_Stewart_5")
|
||||
|
||||
val event =
|
||||
CreateAnnouncementExplorer(announcement, oracleName, description, uriOpt)
|
||||
|
||||
val createdF = explorerClient.createAnnouncement(event)
|
||||
for {
|
||||
_ <- createdF
|
||||
event <- explorerClient.getEvent(event.oracleAnnouncementV0.sha256)
|
||||
} yield {
|
||||
assert(event.announcement == announcement)
|
||||
assert(event.attestations.isEmpty)
|
||||
assert(event.uri == uriOpt)
|
||||
assert(event.oracleName == oracleName)
|
||||
assert(event.description == description)
|
||||
}
|
||||
}
|
||||
|
||||
it must "post attestations for an event to the oracle explorer" ignore {
|
||||
//the announcement is posted in the test case above
|
||||
//which means the test case above must be run before this test case
|
||||
val attestationsHex =
|
||||
"fdd868821b323032312d30332d32342d73756e6e792d696e2d6368696361676f1d5dcdba2e64cb116cc0c375a0856298f0058b778f46bfe625ac6576204889e40001efdf735567ae0a00a515e313d20029de5d7525da7b8367bc843d28b672d4db4db5de4dbff689f3b742be634a9c92c615dbcf2eadbdd470f514b1ac250a30db6d03594553"
|
||||
|
||||
val attestations = OracleAttestmentV0TLV.fromHex(attestationsHex)
|
||||
|
||||
val announcementHash = announcement.sha256
|
||||
val create = CreateAttestations(announcementHash, attestations)
|
||||
val createdF = explorerClient.createAttestations(create)
|
||||
|
||||
for {
|
||||
_ <- createdF
|
||||
//now we must have the attesations
|
||||
event <- explorerClient.getEvent(announcementHash)
|
||||
} yield {
|
||||
assert(event.attestations.isDefined)
|
||||
assert(event.attestations.get == attestations)
|
||||
}
|
||||
}
|
||||
}
|
14
oracle-explorer-client/src/test/scala/org/bitcoins/explorer/env/ExplorerEnvTest.scala
vendored
Normal file
14
oracle-explorer-client/src/test/scala/org/bitcoins/explorer/env/ExplorerEnvTest.scala
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.bitcoins.explorer.env
|
||||
|
||||
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
|
||||
|
||||
class ExplorerEnvTest extends BitcoinSUnitTest {
|
||||
|
||||
behavior of "ExplorerEnv"
|
||||
|
||||
it must "have all base uris envs end with a '/'" in {
|
||||
ExplorerEnv.all.foreach { e =>
|
||||
assert(e.baseUri.last == '/')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.bitcoins.explorer.model
|
||||
|
||||
import org.bitcoins.core.protocol.tlv.OracleAnnouncementV0TLV
|
||||
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
|
||||
|
||||
class SbOracleEventExplorerTest extends BitcoinSUnitTest {
|
||||
|
||||
behavior of "SbOracleEvent"
|
||||
|
||||
it must "encode the event to POST for a form" in {
|
||||
|
||||
val announcementHex =
|
||||
"fdd824b33cbc4081b947ea9d05e616b010b563bfdbc42a2d20effa6f169f8e4be732b10d5461fa84b5739876a0c8a7bdb717040b8ee5907fe7e60694199ba948ecd505b01d5dcdba2e64cb116cc0c375a0856298f0058b778f46bfe625ac6576204889e4fdd8224f0001efdf735567ae0a00a515e313d20029de5d7525da7b8367bc843d28b672d4db4d605bd280fdd80609000203594553024e4f1b323032312d30332d32342d73756e6e792d696e2d6368696361676f"
|
||||
val announcement = OracleAnnouncementV0TLV.fromHex(announcementHex)
|
||||
val oracleName = "Chris_Stewart_5"
|
||||
val description = "2021-03-24-sunny-in-chicago"
|
||||
val uriOpt = Some("https://twitter.com/Chris_Stewart_5")
|
||||
|
||||
val event =
|
||||
CreateAnnouncementExplorer(announcement, oracleName, description, uriOpt)
|
||||
|
||||
val expected = s"oracleAnnouncementV0=${announcementHex}&" +
|
||||
s"description=$description&" +
|
||||
s"oracleName=${oracleName}&" +
|
||||
s"uri=${uriOpt.get}"
|
||||
|
||||
assert(event.toString == expected)
|
||||
}
|
||||
}
|
|
@ -538,4 +538,11 @@ object Deps {
|
|||
Compile.slf4j,
|
||||
Compile.grizzledSlf4j
|
||||
)
|
||||
|
||||
val oracleExplorerClient = Vector(
|
||||
Compile.akkaActor,
|
||||
Compile.akkaHttp,
|
||||
Compile.akkaStream,
|
||||
Compile.playJson
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue