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:
Chris Stewart 2021-04-01 10:43:10 -05:00 committed by GitHub
parent a275668734
commit 49b6d39ab4
14 changed files with 423 additions and 2 deletions

View file

@ -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

View file

@ -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

View file

@ -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)(

View file

@ -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)
*/

View file

@ -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)
}
}

View 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")
}
}
}

View file

@ -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
}
}
}

View file

@ -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}"
}
}

View file

@ -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])

View file

@ -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]
}

View file

@ -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)
}
}
}

View 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 == '/')
}
}
}

View file

@ -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)
}
}

View file

@ -538,4 +538,11 @@ object Deps {
Compile.slf4j,
Compile.grizzledSlf4j
)
val oracleExplorerClient = Vector(
Compile.akkaActor,
Compile.akkaHttp,
Compile.akkaStream,
Compile.playJson
)
}