├── .github └── workflows │ └── scala.yml ├── .gitignore ├── .scalafmt.conf ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── bitcoinGenesisTransaction.json └── scala │ ├── Codecs.scala │ ├── Protocol.scala │ ├── RPCClient.scala │ ├── ZeroMQ.scala │ ├── bitcoin │ ├── Codecs.scala │ ├── Instances.scala │ ├── Protocol.scala │ └── Syntax.scala │ ├── ethereum │ ├── Codecs.scala │ ├── HexTools.scala │ ├── Instances.scala │ ├── Protocol.scala │ ├── Syntax.scala │ └── UInt256.scala │ ├── examples │ ├── bitcoin │ │ ├── CatchupFromZero.scala │ │ ├── GetAllTransactionsForBlock.scala │ │ ├── GetBlockHash.scala │ │ └── SubscribeToBlockUpdates.scala │ ├── ethereum │ │ ├── CatchupFromZero.scala │ │ ├── GetEthereumBestBlock.scala │ │ ├── GetEthereumBlockByHash.scala │ │ ├── GetEthereumBlockByHeight.scala │ │ ├── GetEthereumTransactionByHash.scala │ │ └── GetReceiptByHash.scala │ └── omni │ │ └── OmniGetBlockTransactions.scala │ └── omni │ ├── Codecs.scala │ ├── Instances.scala │ ├── Protocol.scala │ └── Syntax.scala └── test └── scala ├── bitcoin └── ProtocolSpec.scala └── ethereum └── ProtocolSpec.scala /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | - name: Run tests 17 | run: sbt test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | project/target/* 2 | target/** 3 | *.class 4 | *.log 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.1" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.5.2 2 | 3 | * changed Omni schema, propertyid is now Long 4 | 5 | # 2.5.1 6 | 7 | * changed Ethereum syntax got rid of getNextBlockHash (not implemented yet) 8 | 9 | # 2.5.0 10 | 11 | * added Ethereum 12 | * added Ethereum block and transactions codec and syntax 13 | 14 | # 2.4.0 15 | 16 | * added Omni field confirmations 17 | * added Omni field invalidreason 18 | 19 | # 2.3.0 20 | 21 | * removed implicit config 22 | * add optional onRetryError: (hostId, exception) => IO.unit callback 23 | 24 | # 2.2.0 25 | 26 | * handle ConnectException with failover 27 | 28 | # 2.1.0 29 | 30 | * changed env flag for hosts BITCOIN_RPC_HOST -> BITCOIN_RPC_HOSTS 31 | 32 | # 2.0.0 33 | 34 | * added feature to add multiple hosts as fallback (not getNextBlockHash yet) 35 | * interface changed for instantiating Bitcoin etc. provide sequence of fallbacks 36 | 37 | # 1.22 38 | 39 | * added getNextBlockHash for Omni 40 | * added getBlockByHeight for Omni 41 | 42 | # 1.21 43 | 44 | * removed logback.xml from resources 45 | 46 | # 1.20 47 | 48 | * added omni methods 49 | * added new API interface, resources etc. 50 | * added EnvConfig for fast configuration 51 | * added optional parameters to make function 52 | * added various examples for usage 53 | * added more details in README.md 54 | 55 | # 1.7 56 | 57 | * added batching of requests 58 | * added getTransactions method 59 | 60 | # 1.6 61 | 62 | * added logging subsystem, log to DEBUG 63 | 64 | # 1.5 65 | 66 | * added input fields txid, vout, scriptSig for TransactionResponse inputs 67 | * changed coinbase fields to be optional 68 | 69 | # 1.4 70 | 71 | * added version and hash to TransactionResponse 72 | 73 | # 1.3 74 | 75 | * added getBlock by height 76 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Cesar Pantoja 2 | * Jendrik Poloczek -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blockchain-rpc 2 | 3 | Moved to [https://github.com/jpzk/blockchain-rpc](https://github.com/jpzk/blockchain-rpc) 4 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val commonSettings = Seq( 2 | organization := "io.tokenanalyst", 3 | version := "2.5.2", 4 | scalaVersion := "2.13.1", 5 | crossScalaVersions := Seq("2.13.1", "2.12.10"), 6 | organizationHomepage := Some( 7 | url("https://github.com/tokenanalyst/blockchain-rpc") 8 | ), 9 | description := "JSON RPC client for Bitcoin, Bitcoin-based, and Ethereum nodes" 10 | ) 11 | 12 | lazy val `blockchain-rpc` = (project in file(".")) 13 | .settings(commonSettings: _*) 14 | .settings( 15 | assemblyJarName in assembly := "blockchain-rpc.jar", 16 | publishMavenStyle := false, 17 | publishTo := { 18 | val nexus = "https://oss.sonatype.org/" 19 | if (isSnapshot.value) 20 | Some("snapshots" at nexus + "content/repositories/snapshots") 21 | else 22 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 23 | } 24 | ) 25 | .settings( 26 | libraryDependencies ++= Seq( 27 | "commons-codec" % "commons-codec" % "1.13", 28 | "com.typesafe.akka" %% "akka-actor" % "2.6.1" 29 | ) ++ http4s ++ json ++ zmq ++ cats ++ scalaTest 30 | ) 31 | 32 | publishMavenStyle := true 33 | 34 | publishArtifact in Test := false 35 | 36 | pomIncludeRepository := { _ => 37 | false 38 | } 39 | 40 | scmInfo := Some( 41 | ScmInfo( 42 | url("https://github.com/tokenanalyst/blockchain-rpc"), 43 | "scm:git@github.com:tokenanalyst/blockchain-rpc.git" 44 | ) 45 | ) 46 | 47 | pomExtra := 48 | https://github.com/tokenanalyst/blockchain-rpc 49 | 50 | 51 | Apache License Version 2.0 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | repo 54 | 55 | 56 | 57 | 58 | jpzk 59 | Jendrik Poloczek 60 | https://www.madewithtea.com 61 | 62 | 63 | CesarPantoja 64 | Cesar Pantoja 65 | https://twitter.com/chpanto 66 | 67 | 68 | 69 | val http4sVersion = "0.21.0-M5" 70 | val circeVersion = "0.12.0-M4" 71 | val scalaTestVersion = "3.1.0" 72 | 73 | lazy val http4s = Seq( 74 | "org.http4s" %% "http4s-dsl" % http4sVersion, 75 | "org.http4s" %% "http4s-blaze-server" % http4sVersion, 76 | "org.http4s" %% "http4s-blaze-client" % http4sVersion 77 | ) 78 | 79 | lazy val json = Seq( 80 | "org.http4s" %% "http4s-circe" % http4sVersion, 81 | "io.circe" %% "circe-generic" % circeVersion, 82 | "io.circe" %% "circe-literal" % circeVersion, 83 | "io.circe" %% "circe-parser" % circeVersion 84 | ) 85 | lazy val scalaTest = Seq( 86 | "org.scalatest" %% "scalatest" % scalaTestVersion % "test", 87 | ) 88 | 89 | lazy val zmq = Seq( 90 | "org.zeromq" % "jeromq" % "0.5.1" 91 | ) 92 | 93 | lazy val cats = Seq( 94 | "org.typelevel" %% "cats-effect" % "2.0.0" 95 | ) 96 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.4 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 3 | addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.5.0") 4 | addSbtPlugin("com.frugalmechanic" % "fm-sbt-s3-resolver" % "0.19.0") 5 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.8") 6 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") -------------------------------------------------------------------------------- /src/main/resources/bitcoinGenesisTransaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 3 | "hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 4 | "version": 1, 5 | "locktime": 0, 6 | "size": 204, 7 | "vsize": 204, 8 | "weight": 816, 9 | "time": 1231006505, 10 | "blocktime": 1231006505, 11 | "blockhash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", 12 | "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", 13 | "vin": [ 14 | { 15 | "coinbase": "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73", 16 | "sequence": 4294967295 17 | } 18 | ], 19 | "vout": [ 20 | { 21 | "value": 50.00000000, 22 | "n": 0, 23 | "scriptPubKey": { 24 | "asm": "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG", 25 | "hex": "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac", 26 | "reqSigs": 1, 27 | "type": "pubkey", 28 | "addresses": [ 29 | "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" 30 | ] 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/Codecs.scala: -------------------------------------------------------------------------------- 1 | package io.tokenanalyst.blockchainrpc 2 | 3 | import io.circe.{Decoder, Encoder, HCursor, Json} 4 | 5 | object Codecs { 6 | implicit def batchResponse[A <: RPCResponse: Decoder] = 7 | new Decoder[BatchResponse[A]] { 8 | def apply(a: HCursor): Decoder.Result[BatchResponse[A]] = 9 | a.as[Seq[A]].map(s => BatchResponse(s)) 10 | } 11 | 12 | implicit def deriveCirceDecoder[A <: RPCResponse: Decoder] = new Decoder[A] { 13 | def apply(a: HCursor): Decoder.Result[A] = a.downField("result").as[A] 14 | } 15 | 16 | def requestFields( 17 | method: String, 18 | params: Iterable[Json] 19 | ): List[(String, Json)] = List( 20 | ("jsonrpc", Json.fromString("2.0")), 21 | ("id", Json.fromString("0")), 22 | ("method", Json.fromString(method)), 23 | ("params", Json.fromValues(params)) 24 | ) 25 | 26 | implicit def batchRequest[A <: RPCRequest](implicit encoder: RPCEncoder[A]) = 27 | new RPCEncoder[BatchRequest[A]] { 28 | final def apply(req: BatchRequest[A]): Json = { 29 | val jsons = req.seq.map { i => 30 | encoder.apply(i) 31 | } 32 | Json.arr(jsons: _*) 33 | } 34 | } 35 | 36 | implicit def deriveCirceEncoder[A <: RPCRequest]( 37 | implicit e: RPCEncoder[A] 38 | ) = new Encoder[A] { 39 | def apply(a: A): Json = e.apply(a) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/Protocol.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc 18 | 19 | import cats.effect.IO 20 | import io.circe.Json 21 | 22 | trait RPCResponse 23 | trait RPCRequest 24 | 25 | case class BatchResponse[A](seq: Seq[A]) extends RPCResponse 26 | case class BatchRequest[A](seq: Seq[A]) extends RPCRequest 27 | 28 | trait RPCEncoder[A] { 29 | def apply(a: A): Json 30 | } 31 | 32 | trait RPCDecoder[A] { 33 | def apply(a: A): Json 34 | } 35 | 36 | case class Config( 37 | hosts: Seq[String], 38 | port: Option[Int], 39 | username: Option[String], 40 | password: Option[String], 41 | zmqPort: Option[Int] 42 | ) 43 | 44 | object Config { 45 | val PasswordEnv = "BLOCKCHAIN_RPC_PASSWORD" 46 | val UsernameEnv = "BLOCKCHAIN_RPC_USERNAME" 47 | val HostEnv = "BLOCKCHAIN_RPC_HOSTS" 48 | val PortEnv = "BLOCKCHAIN_RPC_PORT" 49 | val ZMQPortEnv = "BLOCKCHAIN_RPC_ZEROMQ_PORT" 50 | 51 | val fromEnv: Config = { 52 | Seq(HostEnv, PortEnv, UsernameEnv, PasswordEnv, ZMQPortEnv) 53 | .map(sys.env.get(_)) match { 54 | case Seq(None, _, _, _, _) => 55 | throw new Exception("Pass at least BLOCKCHAIN_RPC_HOSTS.") 56 | case Seq(Some(h), port, user, pass, zmqPort) => 57 | Config( 58 | h.split(",").toIndexedSeq, 59 | port.map(_.toInt), 60 | user, 61 | pass, 62 | zmqPort.map(_.toInt) 63 | ) 64 | } 65 | } 66 | } 67 | 68 | sealed trait Blockchain 69 | case class Ethereum(client: RPCClient) extends Blockchain 70 | case class Bitcoin(client: RPCClient) extends Blockchain 71 | case class Omni(client: RPCClient) extends Blockchain 72 | 73 | object OmniMethods { 74 | trait ListBlockTransactions { 75 | def listBlockTransactions(omni: Omni, height: Long): IO[Seq[String]] 76 | } 77 | } 78 | 79 | object BasicMethods { 80 | trait GetNextBlockHash[A <: Blockchain] { 81 | def getNextBlockHash(a: A): IO[String] 82 | } 83 | 84 | trait GetBlockByHash[A <: Blockchain, B] { 85 | def getBlockByHash(a: A, hash: String): IO[B] 86 | } 87 | 88 | trait GetBlockByHeight[A <: Blockchain, B] { 89 | def getBlockByHeight(a: A, height: Long): IO[B] 90 | } 91 | 92 | trait GetBlockHash[A <: Blockchain] { 93 | def getBlockHash(a: A, height: Long): IO[String] 94 | } 95 | 96 | trait GetBestBlockHash[A <: Blockchain] { 97 | def getBestBlockHash(a: A): IO[String] 98 | } 99 | 100 | trait GetBestBlockHeight[A <: Blockchain] { 101 | def getBestBlockHeight(a: A): IO[Long] 102 | } 103 | 104 | trait GetTransactions[A <: Blockchain, B] { 105 | def getTransactions(a: A, hashes: Seq[String]): IO[B] 106 | } 107 | 108 | trait GetTransaction[A <: Blockchain, B] { 109 | def getTransaction(a: A, hash: String): IO[B] 110 | } 111 | 112 | trait EstimateSmartFee[A <: Blockchain, B] { 113 | def estimateSmartFee(a: A, height: Long): IO[B] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/scala/RPCClient.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc 18 | 19 | import cats.effect.{ContextShift, IO, Resource} 20 | import io.circe.{Decoder, Encoder, Json} 21 | import org.http4s.circe.CirceEntityDecoder._ 22 | import org.http4s.circe.CirceEntityEncoder._ 23 | import org.http4s.client.Client 24 | import org.http4s.client.blaze.BlazeClientBuilder 25 | import org.http4s.client.dsl.Http4sClientDsl 26 | import org.http4s.dsl.io._ 27 | import org.http4s.headers.{Authorization, _} 28 | import org.http4s.{BasicCredentials, MediaType, Request, Uri} 29 | 30 | import java.net.{ConnectException, SocketTimeoutException} 31 | import scala.concurrent.ExecutionContext 32 | import scala.concurrent.duration._ 33 | 34 | object RPCClient { 35 | 36 | def bitcoin( 37 | hosts: Seq[String], 38 | port: Option[Int] = None, 39 | username: Option[String] = None, 40 | password: Option[String] = None, 41 | zmqPort: Option[Int] = None, 42 | onErrorRetry: (Int, Throwable) => IO[Unit] = (_,_) => IO.unit 43 | )( 44 | implicit ec: ExecutionContext, 45 | cs: ContextShift[IO] 46 | ): Resource[IO, Bitcoin] = { 47 | val config = Config(hosts, port, username, password, zmqPort) 48 | for (client <- make(config, onErrorRetry)) yield Bitcoin(client) 49 | } 50 | 51 | def ethereum( 52 | hosts: Seq[String], 53 | port: Option[Int] = None, 54 | username: Option[String] = None, 55 | password: Option[String] = None, 56 | zmqPort: Option[Int] = None, 57 | onErrorRetry: (Int, Throwable) => IO[Unit] = (_,_) => IO.unit 58 | )( 59 | implicit ec: ExecutionContext, 60 | cs: ContextShift[IO] 61 | ): Resource[IO, Ethereum] = { 62 | val config = Config(hosts, port, username, password, zmqPort) 63 | for (client <- make(config, onErrorRetry)) yield Ethereum(client) 64 | } 65 | 66 | def omni( 67 | hosts: Seq[String], 68 | port: Option[Int] = None, 69 | username: Option[String] = None, 70 | password: Option[String] = None, 71 | zmqPort: Option[Int] = None, 72 | onErrorRetry: (Int, Throwable) => IO[Unit] = (_,_) => IO.unit 73 | )( 74 | implicit ec: ExecutionContext, 75 | cs: ContextShift[IO] 76 | ): Resource[IO, Omni] = { 77 | val config = Config(hosts, port, username, password, zmqPort) 78 | for (client <- make(config, onErrorRetry)) yield Omni(client) 79 | } 80 | 81 | def make(config: Config, onErrorRetry: (Int, Throwable) => IO[Unit])( 82 | implicit ec: ExecutionContext, 83 | cs: ContextShift[IO] 84 | ): Resource[IO, RPCClient] = { 85 | for { 86 | client <- BlazeClientBuilder[IO](ec) 87 | .withConnectTimeout(5.seconds) 88 | .withRequestTimeout(2.minutes) 89 | .resource 90 | socket <- ZeroMQ.socket( 91 | config.hosts.head, 92 | config.zmqPort.getOrElse(28332) 93 | ) 94 | } yield new RPCClient(client, socket, config, onErrorRetry) 95 | } 96 | } 97 | 98 | class RPCClient ( 99 | client: Client[IO], 100 | zmq: ZeroMQ.Socket, 101 | config: Config, 102 | onErrorRetry: (Int, Throwable) => IO[Unit] 103 | ) extends Http4sClientDsl[IO] { 104 | 105 | // is blocking 106 | def nextBlockHash(): IO[String] = zmq.nextBlock() 107 | 108 | def request[A <: RPCRequest: Encoder, B <: RPCResponse: Decoder]( 109 | request: A 110 | ): IO[B] = retry(config.hosts) { host => 111 | for { 112 | req <- post(host, request) 113 | res <- client.expect[B](req) 114 | } yield res 115 | } 116 | 117 | def requestJson[A <: RPCRequest: Encoder](request: A): IO[Json] = 118 | retry(config.hosts) { host => 119 | for { 120 | req <- post(host, request) 121 | res <- client.expect[Json](req) 122 | } yield res 123 | } 124 | 125 | private def post[A <: RPCRequest: Encoder]( 126 | host: String, 127 | request: A 128 | ): IO[Request[IO]] = { 129 | val uri = Uri 130 | .fromString(s"http://${host}:${config.port.getOrElse(8332)}") 131 | .getOrElse(throw new Exception("Could not parse URL")) 132 | (config.username, config.password) match { 133 | case (Some(user), Some(pass)) => 134 | POST( 135 | request, 136 | uri, 137 | Authorization(BasicCredentials(user, pass)), 138 | Accept(MediaType.application.json) 139 | ) 140 | case _ => 141 | POST( 142 | request, 143 | uri, 144 | Accept(MediaType.application.json) 145 | ) 146 | } 147 | } 148 | 149 | def retry[A](fallbacks: Seq[String], current: Int = 0, max: Int = 10)( 150 | f: String => IO[A] 151 | ): IO[A] = { 152 | val hostId = current % fallbacks.size 153 | val handle = (e: Exception) => { 154 | if (current <= max) for { 155 | _ <- onErrorRetry(hostId, e) 156 | r <- retry(fallbacks, current + 1, max)(f) 157 | } yield r 158 | else 159 | IO.raiseError(new Exception(s"Running out of retries for: ${e}")) 160 | } 161 | f(fallbacks(hostId)).handleErrorWith { 162 | case e: org.http4s.client.UnexpectedStatus => handle(e) 163 | case e: ConnectException => handle(e) 164 | case e: SocketTimeoutException => handle(e) 165 | case e => IO.raiseError(e) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/scala/ZeroMQ.scala: -------------------------------------------------------------------------------- 1 | package io.tokenanalyst.blockchainrpc 2 | 3 | import java.io.Closeable 4 | import java.nio.ByteBuffer 5 | 6 | import cats.effect.{IO, Resource} 7 | import org.zeromq._ 8 | 9 | object ZeroMQ { 10 | case class message(topic: String, body: String, sequence: Int) 11 | 12 | def messageFromZMsg(zMsg: ZMsg) = { 13 | val topic = zMsg.popString() 14 | val body = zMsg.popString() 15 | val seq = ByteBuffer.wrap(zMsg.pop().getData.reverse).getInt 16 | message(topic, body, seq) 17 | } 18 | 19 | class Socket(host: String, port: Int) extends Closeable { 20 | val context = new ZContext() 21 | val socket: ZMQ.Socket = context.createSocket(SocketType.SUB) 22 | 23 | //http://api.zeromq.org/2-1:zmq-setsockopt 24 | socket.setHWM(0) 25 | socket.subscribe("hashblock".map(_.toByte).toArray) 26 | socket.connect(f"tcp://$host:$port") 27 | 28 | def nextBlock(): IO[String] = IO { 29 | val msg = ZMsg.recvMsg(socket) 30 | messageFromZMsg(msg).body 31 | } 32 | 33 | override def close() = { 34 | println("closing...") 35 | context.close() 36 | } 37 | } 38 | 39 | def socket(host: String, port: Int): Resource[IO, Socket] = 40 | Resource.fromAutoCloseable(IO(new Socket(host, port))) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/bitcoin/Codecs.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.bitcoin 18 | 19 | import io.circe.Json 20 | import io.tokenanalyst.blockchainrpc.Codecs._ 21 | import io.tokenanalyst.blockchainrpc.RPCEncoder 22 | import io.tokenanalyst.blockchainrpc.bitcoin.Protocol._ 23 | 24 | object Codecs { 25 | 26 | implicit val transactionRequest = new RPCEncoder[TransactionRequest] { 27 | final def apply(a: TransactionRequest): Json = 28 | Json.obj( 29 | requestFields( 30 | "getrawtransaction", 31 | Array(Json.fromString(a.hash), Json.fromInt(1)) 32 | ): _* 33 | ) 34 | } 35 | 36 | implicit val feeRequest = new RPCEncoder[FeeRequest] { 37 | final def apply(a: FeeRequest): Json = 38 | Json.obj( 39 | requestFields("estimatesmartfee", Array(Json.fromLong(a.block))): _* 40 | ) 41 | } 42 | 43 | implicit val blockHashByHeightRequest = 44 | new RPCEncoder[BlockHashByHeightRequest] { 45 | override def apply(a: BlockHashByHeightRequest): Json = 46 | Json.obj( 47 | requestFields("getblockhash", Array(Json.fromLong(a.height))): _* 48 | ) 49 | } 50 | 51 | implicit val bestBlockHashRequest = new RPCEncoder[BestBlockHashRequest] { 52 | final def apply(a: BestBlockHashRequest): Json = 53 | Json.obj(requestFields("getbestblockhash", Array[Json]()): _*) 54 | } 55 | 56 | implicit val blockHashRequest = new RPCEncoder[BlockHashRequest] { 57 | final def apply(a: BlockHashRequest): Json = 58 | Json.obj( 59 | requestFields("getblockhash", Array[Json](Json.fromLong(a.height))): _* 60 | ) 61 | } 62 | 63 | implicit val blockRequest = new RPCEncoder[BlockRequest] { 64 | final def apply(a: BlockRequest): Json = 65 | Json.obj(requestFields("getblock", Array(Json.fromString(a.hash))): _*) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/bitcoin/Instances.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.bitcoin 18 | 19 | import cats.effect.IO 20 | import io.circe.generic.auto._ 21 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 22 | import io.tokenanalyst.blockchainrpc.Codecs._ 23 | import io.tokenanalyst.blockchainrpc.bitcoin.Protocol._ 24 | import io.tokenanalyst.blockchainrpc.bitcoin.Codecs._ 25 | import io.tokenanalyst.blockchainrpc.{BatchRequest, BatchResponse, Bitcoin} 26 | 27 | import scala.collection.mutable.ListBuffer 28 | 29 | object Instances { 30 | 31 | implicit val getNextBlockHashInstance = 32 | new GetNextBlockHash[Bitcoin] { 33 | override def getNextBlockHash(a: Bitcoin): IO[String] = 34 | a.client.nextBlockHash() 35 | } 36 | 37 | implicit val getBlockByHashInstance = 38 | new GetBlockByHash[Bitcoin, BlockResponse] { 39 | override def getBlockByHash( 40 | a: Bitcoin, 41 | hash: String 42 | ): IO[BlockResponse] = { 43 | a.client.request[BlockRequest, BlockResponse](BlockRequest(hash)) 44 | } 45 | } 46 | 47 | implicit val getBlockHashInstance = new GetBlockHash[Bitcoin] { 48 | override def getBlockHash(a: Bitcoin, height: Long): IO[String] = 49 | for { 50 | json <- a.client 51 | .requestJson[BlockHashRequest](BlockHashRequest(height)) 52 | } yield json.asObject.get("result").get.asString.get 53 | } 54 | 55 | implicit val getBlockByHeightInstance = 56 | new GetBlockByHeight[Bitcoin, BlockResponse] { 57 | override def getBlockByHeight( 58 | a: Bitcoin, 59 | height: Long 60 | ): IO[BlockResponse] = 61 | for { 62 | hash <- getBlockHashInstance.getBlockHash(a, height) 63 | data <- getBlockByHashInstance.getBlockByHash(a, hash) 64 | } yield data 65 | } 66 | 67 | implicit val getBestBlockHashInstance = 68 | new GetBestBlockHash[Bitcoin] { 69 | override def getBestBlockHash(a: Bitcoin): IO[String] = 70 | for { 71 | json <- a.client 72 | .requestJson[BestBlockHashRequest](new BestBlockHashRequest) 73 | } yield json.asObject.get("result").get.asString.get 74 | } 75 | 76 | implicit val getBestBlockHeightInstance = 77 | new GetBestBlockHeight[Bitcoin] { 78 | override def getBestBlockHeight(a: Bitcoin): IO[Long] = 79 | for { 80 | hash <- getBestBlockHashInstance.getBestBlockHash(a) 81 | block <- getBlockByHashInstance.getBlockByHash(a, hash) 82 | } yield block.height 83 | } 84 | 85 | implicit val getTransactionsInstance = 86 | new GetTransactions[Bitcoin, BatchResponse[ 87 | TransactionResponse 88 | ]] { 89 | override def getTransactions( 90 | a: Bitcoin, 91 | hashes: Seq[String] 92 | ): IO[BatchResponse[TransactionResponse]] = { 93 | val list = ListBuffer(hashes: _*) 94 | val genesisTransactionIndex = 95 | list.indexOf(Transactions.GenesisTransactionHash) 96 | 97 | if (genesisTransactionIndex >= 0) { 98 | list.remove(genesisTransactionIndex) 99 | } 100 | 101 | val result = 102 | a.client.request[BatchRequest[TransactionRequest], BatchResponse[ 103 | TransactionResponse 104 | ]]( 105 | BatchRequest[TransactionRequest](list.map(TransactionRequest.apply).toSeq) 106 | ) 107 | 108 | if (genesisTransactionIndex >= 0) { 109 | for { 110 | batcResponse <- result 111 | listResult <- IO(ListBuffer(batcResponse.seq: _*)) 112 | _ <- IO( 113 | listResult 114 | .insert( 115 | genesisTransactionIndex, 116 | Transactions.GenesisTransaction 117 | ) 118 | ) 119 | } yield BatchResponse(listResult.toSeq) 120 | } else { 121 | result 122 | } 123 | } 124 | } 125 | 126 | implicit val getTransactionInstance = 127 | new GetTransaction[Bitcoin, TransactionResponse] { 128 | override def getTransaction( 129 | a: Bitcoin, 130 | hash: String 131 | ): IO[TransactionResponse] = 132 | if (hash != Transactions.GenesisTransactionHash) { 133 | a.client.request[TransactionRequest, TransactionResponse]( 134 | TransactionRequest(hash) 135 | ) 136 | } else { 137 | IO(Transactions.GenesisTransaction) 138 | } 139 | } 140 | 141 | implicit val estimateSmartFeeInstance = 142 | new EstimateSmartFee[Bitcoin, FeeResponse] { 143 | override def estimateSmartFee(a: Bitcoin, height: Long): IO[FeeResponse] = 144 | a.client.request[FeeRequest, FeeResponse](FeeRequest(height)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/scala/bitcoin/Protocol.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.bitcoin 18 | 19 | import io.tokenanalyst.blockchainrpc.{RPCRequest, RPCResponse} 20 | import scala.io.Source 21 | 22 | object Protocol { 23 | case class FeeResponse(feerate: Double, blocks: Int) extends RPCResponse 24 | case class BlockHashResponse(hash: String) extends RPCResponse 25 | case class BlockResponse( 26 | height: Long, 27 | hash: String, 28 | previousblockhash: Option[String], 29 | nonce: Long, 30 | strippedsize: Long, 31 | merkleroot: String, 32 | version: Int, 33 | weight: Int, 34 | difficulty: Double, 35 | chainwork: String, 36 | bits: String, 37 | size: Long, 38 | mediantime: Long, 39 | time: Long, 40 | nTx: Int, 41 | tx: List[String] 42 | ) extends RPCResponse 43 | 44 | case class TransactionResponseVin( 45 | txid: Option[String], 46 | vout: Option[Int], 47 | scriptSig: Option[TransactionResponseScriptSig], 48 | coinbase: Option[String], 49 | sequence: Long 50 | ) 51 | 52 | case class TransactionResponseScriptSig(asm: String, hex: String) 53 | 54 | case class TransactionResponseScript( 55 | asm: String, 56 | hex: String, 57 | reqSigs: Option[Int], 58 | `type`: String, 59 | addresses: Option[List[String]] 60 | ) 61 | 62 | case class TransactionResponseVout( 63 | value: Double, 64 | n: Int, 65 | scriptPubKey: TransactionResponseScript 66 | ) 67 | 68 | case class TransactionResponse( 69 | confirmations: Option[Int], 70 | blockhash: String, 71 | blocktime: Long, 72 | hash: String, 73 | hex: String, 74 | txid: String, 75 | time: Long, 76 | vsize: Int, 77 | size: Int, 78 | weight: Int, 79 | version: Int, 80 | vin: List[TransactionResponseVin], 81 | vout: List[TransactionResponseVout], 82 | locktime: Long 83 | ) extends RPCResponse 84 | 85 | case class FeeRequest(block: Long) extends RPCRequest 86 | case class BlockRequest(hash: String) extends RPCRequest 87 | case class BlockHashRequest(height: Long) extends RPCRequest 88 | case class TransactionRequest(hash: String) extends RPCRequest 89 | case class BestBlockHashRequest() extends RPCRequest 90 | case class BlockHashByHeightRequest(height: Long) extends RPCRequest 91 | } 92 | 93 | object Transactions { 94 | import io.circe.generic.auto._ 95 | import io.circe.parser._ 96 | import Protocol._ 97 | 98 | lazy val GenesisTransactionHash = 99 | "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" 100 | 101 | lazy val GenesisTransaction = 102 | parse(Source.fromResource("bitcoinGenesisTransaction.json").mkString) 103 | .flatMap { json => 104 | json.as[TransactionResponse] 105 | } 106 | .getOrElse(throw new Exception("Could not parse genesis")) 107 | } 108 | -------------------------------------------------------------------------------- /src/main/scala/bitcoin/Syntax.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.bitcoin 18 | 19 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 20 | import io.tokenanalyst.blockchainrpc.bitcoin.Instances._ 21 | import io.tokenanalyst.blockchainrpc.bitcoin.Protocol._ 22 | import io.tokenanalyst.blockchainrpc.{BatchResponse, Bitcoin} 23 | 24 | object Syntax { 25 | implicit class BitcoinOps(b: Bitcoin) { 26 | 27 | def getNextBlockHash() = 28 | implicitly[GetNextBlockHash[Bitcoin]].getNextBlockHash(b) 29 | 30 | def getBlockByHash(hash: String) = 31 | implicitly[GetBlockByHash[Bitcoin, BlockResponse]].getBlockByHash(b, hash) 32 | 33 | def getBlockByHeight(height: Long) = 34 | implicitly[GetBlockByHeight[Bitcoin, BlockResponse]] 35 | .getBlockByHeight(b, height) 36 | 37 | def getBlockHash(height: Long) = 38 | implicitly[GetBlockHash[Bitcoin]].getBlockHash(b, height) 39 | 40 | def getBestBlockHash() = 41 | implicitly[GetBestBlockHash[Bitcoin]].getBestBlockHash(b) 42 | 43 | def getBestBlockHeight() = 44 | implicitly[GetBestBlockHeight[Bitcoin]].getBestBlockHeight(b) 45 | 46 | def getTransactions(hashes: Seq[String]) = 47 | implicitly[GetTransactions[Bitcoin, BatchResponse[TransactionResponse]]] 48 | .getTransactions(b, hashes) 49 | 50 | def getTransaction(hash: String) = 51 | implicitly[GetTransaction[Bitcoin, TransactionResponse]] 52 | .getTransaction(b, hash) 53 | 54 | def estimateSmartFee(height: Long) = 55 | implicitly[EstimateSmartFee[Bitcoin, FeeResponse]] 56 | .estimateSmartFee(b, height) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/Codecs.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.ethereum 18 | 19 | import io.circe.Json 20 | import io.tokenanalyst.blockchainrpc.Codecs._ 21 | import io.tokenanalyst.blockchainrpc.RPCEncoder 22 | import io.tokenanalyst.blockchainrpc.ethereum.Protocol._ 23 | 24 | object Codecs { 25 | 26 | implicit val receiptRequest = new RPCEncoder[ReceiptRequest] { 27 | final def apply(a: ReceiptRequest): Json = Json.obj( 28 | requestFields( 29 | "eth_getTransactionReceipt", 30 | Array(Json.fromString(a.hash)) 31 | ): _* 32 | ) 33 | } 34 | 35 | implicit val transactionRequest = new RPCEncoder[TransactionRequest] { 36 | final def apply(a: TransactionRequest): Json = Json.obj( 37 | requestFields( 38 | "eth_getTransactionByHash", 39 | Array(Json.fromString(a.hash)) 40 | ): _* 41 | ) 42 | } 43 | 44 | implicit val bestBlockHeightRequest = new RPCEncoder[BestBlockHeightRequest] { 45 | final def apply(a: BestBlockHeightRequest): Json = 46 | Json.obj(requestFields("eth_blockNumber", Array[Json]()): _*) 47 | } 48 | 49 | implicit val blockByHeightRequest = new RPCEncoder[BlockByHeightRequest] { 50 | final def apply(a: BlockByHeightRequest): Json = 51 | Json.obj( 52 | requestFields( 53 | "eth_getBlockByNumber", 54 | Array[Json]( 55 | Json.fromString( 56 | HexTools.toHexString(a.height) 57 | ), 58 | Json.fromBoolean(a.withTransactions) 59 | ) 60 | ): _* 61 | ) 62 | } 63 | 64 | implicit val blockByHashRequest = new RPCEncoder[BlockByHashRequest] { 65 | final def apply(a: BlockByHashRequest): Json = 66 | Json.obj( 67 | requestFields( 68 | "eth_getBlockByHash", 69 | Array(Json.fromString(a.hash), Json.fromBoolean(a.withTransactions)) 70 | ): _* 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/HexTools.scala: -------------------------------------------------------------------------------- 1 | package io.tokenanalyst.blockchainrpc.ethereum 2 | 3 | import org.apache.commons.codec.binary.Hex 4 | 5 | object HexTools { 6 | 7 | def parseQuantity(in: String): BigInt = { 8 | val cleaned = in.replaceFirst("^0x", in.length % 2 match { 9 | case 0 => "" 10 | case 1 => "0" 11 | }) 12 | UInt256(Hex.decodeHex(cleaned)).toBigInt 13 | } 14 | 15 | def parseData(in: String): Array[Byte] = { 16 | Hex.decodeHex(in.replaceFirst("^0x", "")) 17 | } 18 | 19 | def toHexString(in: Long) = UInt256(in).toHexString 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/Instances.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.ethereum 18 | 19 | import cats.effect.IO 20 | import io.circe.generic.auto._ 21 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 22 | import io.tokenanalyst.blockchainrpc.Codecs._ 23 | import io.tokenanalyst.blockchainrpc.ethereum.Codecs._ 24 | import io.tokenanalyst.blockchainrpc.ethereum.Methods._ 25 | import io.tokenanalyst.blockchainrpc.ethereum.Protocol._ 26 | import io.tokenanalyst.blockchainrpc.{BatchRequest, BatchResponse, Ethereum} 27 | 28 | object Instances { 29 | 30 | implicit val getReceiptInstance = 31 | new GetReceipt[Ethereum, ReceiptResponse] { 32 | override def getReceipt( 33 | a: Ethereum, 34 | hash: String 35 | ): IO[ReceiptResponse] = { 36 | a.client.request[ReceiptRequest, ReceiptResponse]( 37 | ReceiptRequest(hash) 38 | ) 39 | } 40 | } 41 | 42 | implicit val getReceiptsInstance = 43 | new GetReceipts[Ethereum, BatchResponse[ReceiptResponse]] { 44 | override def getReceipts( 45 | a: Ethereum, 46 | hashes: Seq[String] 47 | ): IO[BatchResponse[ReceiptResponse]] = { 48 | a.client.request[ 49 | BatchRequest[ReceiptRequest], 50 | BatchResponse[ReceiptResponse] 51 | ]( 52 | BatchRequest[ReceiptRequest](hashes.map(ReceiptRequest.apply)) 53 | ) 54 | } 55 | } 56 | 57 | implicit val getBlockWithTransactionsByHashInstance = 58 | new GetBlockByHash[Ethereum, BlockWithTransactionsResponse] { 59 | override def getBlockByHash( 60 | a: Ethereum, 61 | hash: String 62 | ): IO[BlockWithTransactionsResponse] = { 63 | a.client.request[BlockByHashRequest, BlockWithTransactionsResponse]( 64 | BlockByHashRequest(hash, true) 65 | ) 66 | } 67 | } 68 | 69 | implicit val getBlockByHashInstance = 70 | new GetBlockByHash[Ethereum, BlockResponse] { 71 | override def getBlockByHash( 72 | a: Ethereum, 73 | hash: String 74 | ): IO[BlockResponse] = { 75 | a.client.request[BlockByHashRequest, BlockResponse]( 76 | BlockByHashRequest(hash, false) 77 | ) 78 | } 79 | } 80 | 81 | implicit val getBlockWithTransactionsByHeightInstance = 82 | new GetBlockByHeight[Ethereum, BlockWithTransactionsResponse] { 83 | override def getBlockByHeight( 84 | a: Ethereum, 85 | height: Long 86 | ): IO[BlockWithTransactionsResponse] = 87 | a.client.request[BlockByHeightRequest, BlockWithTransactionsResponse]( 88 | BlockByHeightRequest(height, true) 89 | ) 90 | } 91 | 92 | implicit val getBlockByHeightInstance = 93 | new GetBlockByHeight[Ethereum, BlockResponse] { 94 | override def getBlockByHeight( 95 | a: Ethereum, 96 | height: Long 97 | ): IO[BlockResponse] = 98 | a.client.request[BlockByHeightRequest, BlockResponse]( 99 | BlockByHeightRequest(height, false) 100 | ) 101 | } 102 | 103 | implicit val getBestBlockHeightInstance = 104 | new GetBestBlockHeight[Ethereum] { 105 | override def getBestBlockHeight(a: Ethereum): IO[Long] = 106 | for { 107 | json <- a.client 108 | .requestJson[BestBlockHeightRequest](new BestBlockHeightRequest) 109 | } yield HexTools 110 | .parseQuantity(json.asObject.get("result").get.asString.get) 111 | .toLong 112 | } 113 | 114 | implicit val getTransactionsInstance = 115 | new GetTransactions[Ethereum, BatchResponse[ 116 | TransactionResponse 117 | ]] { 118 | override def getTransactions( 119 | a: Ethereum, 120 | hashes: Seq[String] 121 | ): IO[BatchResponse[TransactionResponse]] = 122 | a.client.request[ 123 | BatchRequest[TransactionRequest], 124 | BatchResponse[TransactionResponse] 125 | ]( 126 | BatchRequest[TransactionRequest](hashes.map(TransactionRequest.apply)) 127 | ) 128 | } 129 | 130 | implicit val getTransactionInstance = 131 | new GetTransaction[Ethereum, TransactionResponse] { 132 | override def getTransaction( 133 | a: Ethereum, 134 | hash: String 135 | ): IO[TransactionResponse] = 136 | a.client.request[TransactionRequest, TransactionResponse]( 137 | TransactionRequest(hash) 138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/Protocol.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.ethereum 18 | 19 | import cats.effect.IO 20 | import io.tokenanalyst.blockchainrpc.{Ethereum, RPCRequest, RPCResponse} 21 | 22 | object Methods { 23 | 24 | trait GetBlockWithTransactionsByHash[A <: Ethereum, B] { 25 | def getBlockWithTransactionsByHash(a: A, hash: String): IO[B] 26 | } 27 | 28 | trait GetBlockWithTransactionsByHeight[A <: Ethereum, B] { 29 | def getBlockWithTransactionsByHeight(a: A, height: Long): IO[B] 30 | } 31 | 32 | trait GetReceipt[A <: Ethereum, B] { 33 | def getReceipt(a: A, hash: String): IO[B] 34 | } 35 | 36 | trait GetReceipts[A <: Ethereum, B] { 37 | def getReceipts(a: A, hashes: Seq[String]): IO[B] 38 | } 39 | } 40 | 41 | object Protocol { 42 | 43 | type BlockWithTransactionsResponse = 44 | GenericBlockResponse[TransactionResponse] 45 | 46 | type BlockResponse = GenericBlockResponse[String] 47 | 48 | case class GenericBlockResponse[A]( 49 | author: String, 50 | difficulty: String, 51 | extraData: String, 52 | gasLimit: String, 53 | gasUsed: String, 54 | hash: String, 55 | logsBloom: String, 56 | miner: String, 57 | mixHash: String, 58 | nonce: String, 59 | number: String, 60 | parentHash: String, 61 | receiptsRoot: String, 62 | sealFields: List[String], 63 | sha3Uncles: String, 64 | size: String, 65 | stateRoot: String, 66 | timestamp: String, 67 | totalDifficulty: String, 68 | transactions: List[A], 69 | transactionsRoot: String, 70 | uncles: List[String] 71 | ) extends RPCResponse 72 | 73 | case class TransactionResponse( 74 | blockHash: String, 75 | blockNumber: String, 76 | chainId: Option[String], 77 | from: String, 78 | gas: String, 79 | gasPrice: String, 80 | hash: String, 81 | input: String, 82 | nonce: String, 83 | publicKey: String, 84 | r: String, 85 | raw: String, 86 | s: String, 87 | v: String, 88 | standardV: String, 89 | to: Option[String], 90 | transactionIndex: String, 91 | value: String, 92 | condition: Option[String], 93 | creates: Option[String] 94 | ) extends RPCResponse 95 | 96 | case class ReceiptResponse( 97 | blockHash: String, 98 | blockNumber: String, 99 | contractAddress: Option[String], 100 | from: Option[String], 101 | to: Option[String], 102 | cumulativeGasUsed: String, 103 | gasUsed: Option[String], 104 | logs: List[LogResponse], 105 | logsBloom: String, 106 | status: Option[String], 107 | transactionHash: String, 108 | transactionIndex: String 109 | ) extends RPCResponse 110 | 111 | case class LogResponse( 112 | removed: Boolean, 113 | logIndex: Option[String], 114 | transactionIndex: Option[String], 115 | transactionHash: Option[String], 116 | blockHash: Option[String], 117 | blockNumber: Option[String], 118 | address: String, 119 | data: String, 120 | topics: List[String], 121 | transactionLogIndex: Option[String], 122 | `type`: String 123 | ) 124 | 125 | case class BlockByHashRequest(hash: String, withTransactions: Boolean) 126 | extends RPCRequest 127 | case class BlockByHeightRequest(height: Long, withTransactions: Boolean) 128 | extends RPCRequest 129 | case class ReceiptRequest(hash: String) extends RPCRequest 130 | case class TransactionRequest(hash: String) extends RPCRequest 131 | case class BestBlockHeightRequest() extends RPCRequest 132 | } 133 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/Syntax.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.ethereum 18 | 19 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 20 | import io.tokenanalyst.blockchainrpc.{BatchResponse, Ethereum} 21 | import io.tokenanalyst.blockchainrpc.ethereum.Instances._ 22 | import io.tokenanalyst.blockchainrpc.ethereum.Methods._ 23 | import io.tokenanalyst.blockchainrpc.ethereum.Protocol.{BlockResponse, _} 24 | 25 | object Syntax { 26 | implicit class EthereumOps(b: Ethereum) { 27 | 28 | def getReceiptByHash(hash: String) = 29 | implicitly[GetReceipt[Ethereum, ReceiptResponse]].getReceipt(b, hash) 30 | 31 | def getReceiptsByHash(hashes: Seq[String]) = 32 | implicitly[GetReceipts[Ethereum, BatchResponse[ReceiptResponse]]] 33 | .getReceipts(b, hashes) 34 | 35 | def getBlockByHeight(height: Long) = 36 | implicitly[GetBlockByHeight[Ethereum, BlockResponse]] 37 | .getBlockByHeight(b, height) 38 | 39 | def getBlockByHash(hash: String) = 40 | implicitly[GetBlockByHash[Ethereum, BlockResponse]] 41 | .getBlockByHash(b, hash) 42 | 43 | def getBestBlockHeight() = 44 | implicitly[GetBestBlockHeight[Ethereum]].getBestBlockHeight(b) 45 | 46 | def getTransaction(hash: String) = 47 | implicitly[GetTransaction[Ethereum, TransactionResponse]] 48 | .getTransaction(b, hash) 49 | 50 | def getTransactions(hashes: Seq[String]) = 51 | implicitly[GetTransactions[Ethereum, BatchResponse[TransactionResponse]]] 52 | .getTransactions(b, hashes) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/ethereum/UInt256.scala: -------------------------------------------------------------------------------- 1 | package io.tokenanalyst.blockchainrpc.ethereum 2 | 3 | import akka.util.ByteString 4 | 5 | // scalastyle:off number.of.methods 6 | object UInt256 { 7 | 8 | 9 | /** Size of UInt256 byte representation */ 10 | val Size: Int = 32 11 | 12 | private val Modulus: BigInt = BigInt(2).pow(256) 13 | 14 | val MaxValue: UInt256 = new UInt256(Modulus - 1) 15 | 16 | val Zero: UInt256 = new UInt256(0) 17 | 18 | val One: UInt256 = new UInt256(1) 19 | 20 | val Two: UInt256 = new UInt256(2) 21 | 22 | def apply(bytes: ByteString): UInt256 = { 23 | require(bytes.length <= Size, s"Input byte array cannot be longer than $Size: ${bytes.length}") 24 | UInt256(bytes.foldLeft(BigInt(0)){(n, b) => (n << 8) + (b & 0xff)}) 25 | } 26 | 27 | def apply(array: Array[Byte]): UInt256 = 28 | UInt256(ByteString(array)) 29 | 30 | def apply(n: BigInt): UInt256 = 31 | new UInt256(boundBigInt(n)) 32 | 33 | def apply(b: Boolean): UInt256 = 34 | if (b) One else Zero 35 | 36 | def apply(n: Long): UInt256 = 37 | apply(BigInt(n)) 38 | 39 | implicit class BigIntAsUInt256(val bigInt: BigInt) extends AnyVal { 40 | def toUInt256: UInt256 = UInt256(bigInt) 41 | } 42 | 43 | implicit def uint256ToBigInt(uint: UInt256): BigInt = uint.toBigInt 44 | 45 | implicit def byte2UInt256(b: Byte): UInt256 = UInt256(b.longValue()) 46 | 47 | implicit def int2UInt256(i: Int): UInt256 = UInt256(i.longValue()) 48 | 49 | implicit def long2UInt256(l: Long): UInt256 = UInt256(l) 50 | 51 | implicit def bool2UInt256(b: Boolean): UInt256 = UInt256(b) 52 | 53 | 54 | 55 | private val Zeros: ByteString = ByteString(Array.fill[Byte](Size)(0)) 56 | 57 | private def boundBigInt(n: BigInt): BigInt = (n % Modulus + Modulus) % Modulus 58 | 59 | private val MaxSignedValue: BigInt = BigInt(2).pow(Size * 8 - 1) - 1 60 | } 61 | 62 | /** Represents 256 bit unsigned integers with standard arithmetic, byte-wise operation and EVM-specific extensions */ 63 | class UInt256 private (private val n: BigInt) extends Ordered[UInt256] { 64 | 65 | import UInt256._ 66 | require(n >= 0 && n < Modulus, s"Invalid UInt256 value: $n") 67 | 68 | 69 | // byte-wise operations 70 | 71 | /** Converts a BigInt to a ByteString. 72 | * Output ByteString is padded with 0's from the left side up to UInt256.Size bytes. 73 | */ 74 | lazy val bytes: ByteString = { 75 | val bs: ByteString = ByteString(n.toByteArray).takeRight(Size) 76 | val padLength: Int = Size - bs.length 77 | if (padLength > 0) 78 | Zeros.take(padLength) ++ bs 79 | else 80 | bs 81 | } 82 | 83 | /** Used for gas calculation for EXP opcode. See YP Appendix H.1 (220) 84 | * For n > 0: (n.bitLength - 1) / 8 + 1 == 1 + floor(log_256(n)) 85 | * 86 | * @return Size in bytes excluding the leading 0 bytes 87 | */ 88 | def byteSize: Int = if (isZero) 0 else (n.bitLength - 1) / 8 + 1 89 | 90 | def getByte(that: UInt256): UInt256 = 91 | if (that.n > 31) Zero else UInt256(bytes(that.n.toInt).toLong & 0xff) 92 | 93 | // standard arithmetic (note the use of new instead of apply where result is guaranteed to be within bounds) 94 | def &(that: UInt256): UInt256 = new UInt256(this.n & that.n) 95 | 96 | def |(that: UInt256): UInt256 = new UInt256(this.n | that.n) 97 | 98 | def ^(that: UInt256): UInt256 = new UInt256(this.n ^ that.n) 99 | 100 | def unary_- : UInt256 = UInt256(-n) 101 | 102 | def unary_~ : UInt256 = UInt256(~n) 103 | 104 | def +(that: UInt256): UInt256 = UInt256(this.n + that.n) 105 | 106 | def -(that: UInt256): UInt256 = UInt256(this.n - that.n) 107 | 108 | def *(that: UInt256): UInt256 = UInt256(this.n * that.n) 109 | 110 | def /(that: UInt256): UInt256 = new UInt256(this.n / that.n) 111 | 112 | def **(that: UInt256): UInt256 = UInt256(this.n.modPow(that.n, Modulus)) 113 | 114 | def compare(that: UInt256): Int = this.n.compare(that.n) 115 | 116 | def min(that: UInt256): UInt256 = if (compare(that) < 0) this else that 117 | 118 | def max(that: UInt256): UInt256 = if (compare(that) > 0) this else that 119 | 120 | def isZero: Boolean = n == 0 121 | 122 | 123 | 124 | // EVM-specific arithmetic 125 | private lazy val signedN: BigInt = if (n > MaxSignedValue) n - Modulus else n 126 | 127 | private def zeroCheck(x: UInt256)(result: => BigInt): UInt256 = 128 | if (x.isZero) Zero else UInt256(result) 129 | 130 | def div(that: UInt256): UInt256 = zeroCheck(that) { new UInt256(this.n / that.n) } 131 | 132 | def sdiv(that: UInt256): UInt256 = zeroCheck(that) { UInt256(this.signedN / that.signedN) } 133 | 134 | def mod(that: UInt256): UInt256 = zeroCheck(that) { UInt256(this.n mod that.n) } 135 | 136 | def smod(that: UInt256): UInt256 = zeroCheck(that) { UInt256(this.signedN % that.signedN.abs) } 137 | 138 | def addmod(that: UInt256, modulus: UInt256): UInt256 = zeroCheck(modulus) { new UInt256((this.n + that.n) % modulus.n) } 139 | 140 | def mulmod(that: UInt256, modulus: UInt256): UInt256 = zeroCheck(modulus) { new UInt256((this.n * that.n) mod modulus.n) } 141 | 142 | def slt(that: UInt256): Boolean = this.signedN < that.signedN 143 | 144 | def sgt(that: UInt256): Boolean = this.signedN > that.signedN 145 | 146 | def signExtend(that: UInt256): UInt256 = { 147 | if (that.n < 0 || that.n > 31) { 148 | this 149 | } else { 150 | val idx = that.n.toByte 151 | val negative = n.testBit(idx * 8 + 7) 152 | val mask = (BigInt(1) << ((idx + 1) * 8)) - 1 153 | val newN = if (negative) n | (MaxValue ^ mask) else n & mask 154 | new UInt256(newN) 155 | } 156 | } 157 | 158 | 159 | 160 | //standard methods 161 | override def equals(that: Any): Boolean = { 162 | that match { 163 | case that: UInt256 => this.n.equals(that.n) 164 | case other => other == n 165 | } 166 | } 167 | 168 | override def hashCode: Int = n.hashCode() 169 | 170 | override def toString: String = toSignedDecString 171 | 172 | def toDecString: String = 173 | n.toString 174 | 175 | def toSignedDecString: String = 176 | signedN.toString 177 | 178 | def toHexString: String = { 179 | val hex = f"$n%x" 180 | //add zero if odd number of digits 181 | val extraZero = if (hex.length % 2 == 0) "" else "0" 182 | s"0x$extraZero$hex" 183 | } 184 | 185 | // conversions 186 | def toBigInt: BigInt = n 187 | 188 | /** 189 | * @return an Int with MSB=0, thus a value in range [0, Int.MaxValue] 190 | */ 191 | def toInt: Int = n.intValue & Int.MaxValue 192 | 193 | /** 194 | * @return a Long with MSB=0, thus a value in range [0, Long.MaxValue] 195 | */ 196 | def toLong: Long = n.longValue & Long.MaxValue 197 | } 198 | -------------------------------------------------------------------------------- /src/main/scala/examples/bitcoin/CatchupFromZero.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.bitcoin 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.Bitcoin 23 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 24 | import io.tokenanalyst.blockchainrpc.bitcoin.Syntax._ 25 | 26 | object CatchupFromZero extends IOApp { 27 | 28 | def loop(rpc: Bitcoin, current: Long = 0L, until: Long = 10L): IO[Unit] = 29 | for { 30 | block <- rpc.getBlockByHeight(current) 31 | _ <- IO { println(block) } 32 | l <- if (current + 1 < until) loop(rpc, current + 1, until) else IO.unit 33 | } yield l 34 | 35 | def run(args: List[String]): IO[ExitCode] = { 36 | implicit val ec = global 37 | implicit val config = Config.fromEnv 38 | RPCClient 39 | .bitcoin(config.hosts, config.port, config.username, config.password) 40 | .use { rpc => 41 | for { 42 | _ <- loop(rpc) 43 | } yield ExitCode(0) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/examples/bitcoin/GetAllTransactionsForBlock.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.bitcoin 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 23 | import io.tokenanalyst.blockchainrpc.bitcoin.Syntax._ 24 | 25 | object GetAllTransactionsFromBlock extends IOApp { 26 | 27 | def run(args: List[String]): IO[ExitCode] = { 28 | implicit val ec = global 29 | implicit val config = Config.fromEnv 30 | RPCClient 31 | .bitcoin(config.hosts, config.port, config.username, config.password) 32 | .use { rpc => 33 | for { 34 | block <- rpc.getBlockByHash( 35 | "0000000000000000000759de6ab39c2d8fb01e4481ba581761ddc1d50a57358d" 36 | ) 37 | txs <- rpc.getTransactions(block.tx) 38 | _ <- IO(println(s"Fetched ${txs.seq.size} transactions")) 39 | } yield ExitCode(0) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/examples/bitcoin/GetBlockHash.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.bitcoin 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 23 | import io.tokenanalyst.blockchainrpc.bitcoin.Syntax._ 24 | 25 | object GetBlockHash extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | implicit val config = Config.fromEnv 29 | RPCClient 30 | .bitcoin( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | onErrorRetry = { (_, e: Throwable) => IO(println(e)) } 36 | ) 37 | .use { bitcoin => 38 | for { 39 | block <- bitcoin.getBlockByHash( 40 | "0000000000000000000759de6ab39c2d8fb01e4481ba581761ddc1d50a57358d" 41 | ) 42 | _ <- IO { println(block) } 43 | } yield ExitCode(0) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/examples/bitcoin/SubscribeToBlockUpdates.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.bitcoin 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.RPCClient 23 | import io.tokenanalyst.blockchainrpc.bitcoin.Syntax._ 24 | 25 | object SubscribeToBlockUpdates extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | RPCClient 29 | .bitcoin( 30 | hosts = Seq("127.0.0.1"), 31 | username = Some("user"), 32 | password = Some("password") 33 | ) 34 | .use { bitcoin => 35 | for { 36 | hash <- bitcoin.getNextBlockHash() 37 | block <- bitcoin.getBlockByHash(hash) 38 | _ <- IO { println(block) } 39 | } yield ExitCode(0) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/CatchupFromZero.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 21 | import io.tokenanalyst.blockchainrpc.ethereum.HexTools 22 | import io.tokenanalyst.blockchainrpc.ethereum.Protocol.TransactionResponse 23 | import io.tokenanalyst.blockchainrpc.{BatchResponse, Config, Ethereum, RPCClient} 24 | 25 | import scala.concurrent.ExecutionContext.global 26 | 27 | object CatchupFromZero extends IOApp { 28 | 29 | def getReceipts(rpc: Ethereum, txs: Seq[String]) = 30 | for { 31 | receipts <- rpc.getReceiptsByHash(txs) 32 | _ <- IO(println(s"${receipts.seq.size} receipts")) 33 | } yield () 34 | 35 | def loop(rpc: Ethereum, current: Long = 9000000L, until: Long = 9120000): IO[Unit] = 36 | for { 37 | block <- rpc.getBlockByHeight(current) 38 | _ <- IO { println(s"block ${HexTools.parseQuantity(block.number)} - ${block.hash}: ${block.transactions.size} transactions") } 39 | transactions <- if(block.transactions.nonEmpty) rpc.getTransactions(block.transactions) else IO.pure(BatchResponse[TransactionResponse](List())) 40 | _ <- IO(println(s"transactions: ${transactions.seq.size}")) 41 | _ <- if(block.transactions.nonEmpty) getReceipts(rpc, block.transactions) else IO.unit 42 | l <- if (current + 1 < until) loop(rpc, current + 1, until) else IO.unit 43 | } yield l 44 | 45 | def run(args: List[String]): IO[ExitCode] = { 46 | implicit val ec = global 47 | implicit val config = Config.fromEnv 48 | RPCClient 49 | .ethereum( 50 | config.hosts, 51 | config.port, 52 | config.username, 53 | config.password, 54 | onErrorRetry = { (_, e: Throwable) => 55 | IO(println(e)) 56 | } 57 | ) 58 | .use { ethereum => 59 | for { 60 | _ <- loop(ethereum) 61 | } yield ExitCode(0) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/GetEthereumBestBlock.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 23 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 24 | 25 | object GetEthereumBestBlock extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | implicit val config = Config.fromEnv 29 | RPCClient 30 | .ethereum( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | onErrorRetry = { (_, e: Throwable) => 36 | IO(println(e)) 37 | } 38 | ) 39 | .use { ethereum => 40 | for { 41 | height <- ethereum.getBestBlockHeight() 42 | _ <- IO { println(height) } 43 | } yield ExitCode(0) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/GetEthereumBlockByHash.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 23 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 24 | import io.tokenanalyst.blockchainrpc.ethereum.HexTools 25 | 26 | object GetEthereumBlockByHash extends IOApp { 27 | def run(args: List[String]): IO[ExitCode] = { 28 | 29 | 30 | implicit val ec = global 31 | implicit val config = Config.fromEnv 32 | RPCClient 33 | .ethereum( 34 | config.hosts, 35 | config.port, 36 | config.username, 37 | config.password, 38 | onErrorRetry = { (_, e: Throwable) => 39 | IO(println(e)) 40 | } 41 | ) 42 | .use { ethereum => 43 | for { 44 | block <- ethereum.getBlockByHash( 45 | "0x3bad41c70c9efac92490e8a74ab816558bbdada0984f2bcfa4cb1522ddb3ca16" 46 | ) 47 | _ <- IO { println(s"block ${HexTools.parseQuantity(block.number)}: $block") } 48 | } yield ExitCode(0) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/GetEthereumBlockByHeight.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import scala.concurrent.ExecutionContext.global 21 | 22 | import io.tokenanalyst.blockchainrpc.{RPCClient, Config} 23 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 24 | 25 | object GetEthereumBlockByHeight extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | implicit val config = Config.fromEnv 29 | RPCClient 30 | .ethereum( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | onErrorRetry = { (_, e: Throwable) => 36 | IO(println(e)) 37 | } 38 | ) 39 | .use { ethereum => 40 | for { 41 | block <- ethereum.getBlockByHeight( 42 | 14 43 | ) 44 | _ <- IO { println(block) } 45 | } yield ExitCode(0) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/GetEthereumTransactionByHash.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 21 | import io.tokenanalyst.blockchainrpc.{Config, RPCClient} 22 | 23 | import scala.concurrent.ExecutionContext.global 24 | 25 | object GetEthereumTransactionByHash extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | implicit val config = Config.fromEnv 29 | RPCClient 30 | .ethereum( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | onErrorRetry = { (_, e: Throwable) => 36 | IO(println(e)) 37 | } 38 | ) 39 | .use { ethereum => 40 | for { 41 | tx <- ethereum.getTransaction( 42 | "0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e" 43 | ) 44 | _ <- IO { println(tx) } 45 | } yield ExitCode(0) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/examples/ethereum/GetReceiptByHash.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.examples.ethereum 18 | 19 | import cats.effect.{ExitCode, IO, IOApp} 20 | import io.tokenanalyst.blockchainrpc.ethereum.Syntax._ 21 | import io.tokenanalyst.blockchainrpc.{Config, RPCClient} 22 | 23 | import scala.concurrent.ExecutionContext.global 24 | 25 | object GetReceiptByHash extends IOApp { 26 | def run(args: List[String]): IO[ExitCode] = { 27 | implicit val ec = global 28 | implicit val config = Config.fromEnv 29 | RPCClient 30 | .ethereum( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | onErrorRetry = { (_, e: Throwable) => 36 | IO(println(e)) 37 | } 38 | ) 39 | .use { ethereum => 40 | for { 41 | tx <- ethereum.getReceiptByHash( 42 | "0x218b632d932371478d1ae5a01620ebab1a2030f9dad6f8fba4a044ea6335a57e" 43 | ) 44 | _ <- IO { println(tx) } 45 | } yield ExitCode(0) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/examples/omni/OmniGetBlockTransactions.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package io.tokenanalyst.blockchainrpc.examples.omni 19 | 20 | import cats.effect.{ExitCode, IO, IOApp} 21 | import io.tokenanalyst.blockchainrpc.{Config, RPCClient} 22 | import io.tokenanalyst.blockchainrpc.omni.Syntax._ 23 | 24 | import scala.concurrent.ExecutionContext.global 25 | 26 | object OmniGetBlockTransactions extends IOApp { 27 | override def run(args: List[String]): IO[ExitCode] = { 28 | implicit val ec = global 29 | val config = Config.fromEnv 30 | RPCClient.omni( 31 | config.hosts, 32 | config.port, 33 | config.username, 34 | config.password, 35 | ).use { omni => 36 | for { 37 | bestBlockHeight <- omni.getBestBlockHeight() 38 | _ <- IO(println(s"best block: $bestBlockHeight")) 39 | txs <- omni.listBlockTransactions(bestBlockHeight) 40 | values <- omni.getTransactions(txs) 41 | _ <- IO {println(s"sending addresses: ${values.seq.map(_.sendingaddress)}")} 42 | } yield ExitCode.Success 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/omni/Codecs.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.omni 18 | 19 | import io.circe.Json 20 | import io.tokenanalyst.blockchainrpc.Codecs._ 21 | import io.tokenanalyst.blockchainrpc.RPCEncoder 22 | import io.tokenanalyst.blockchainrpc.omni.Protocol.BlockRequest 23 | import io.tokenanalyst.blockchainrpc.omni.Protocol.{ 24 | BestBlockHashRequest, 25 | BlockTransactionsRequest, 26 | TransactionRequest, 27 | BlockHashRequest 28 | } 29 | 30 | object Codecs { 31 | 32 | implicit val listBlockTransactionsRequest = 33 | new RPCEncoder[BlockTransactionsRequest] { 34 | final def apply(a: BlockTransactionsRequest): Json = { 35 | Json.obj( 36 | requestFields( 37 | "omni_listblocktransactions", 38 | Array(Json.fromLong(a.height)) 39 | ): _* 40 | ) 41 | } 42 | } 43 | 44 | implicit val blockHashRequest = new RPCEncoder[BlockHashRequest] { 45 | final def apply(a: BlockHashRequest): Json = 46 | Json.obj( 47 | requestFields("getblockhash", Array[Json](Json.fromLong(a.height))): _* 48 | ) 49 | } 50 | 51 | implicit val getTransactionRequest = new RPCEncoder[TransactionRequest] { 52 | final def apply(a: TransactionRequest): Json = 53 | Json.obj( 54 | requestFields( 55 | "omni_gettransaction", 56 | Array(Json.fromString(a.hash)) 57 | ): _* 58 | ) 59 | } 60 | 61 | implicit val bestBlockHashRequest = new RPCEncoder[BestBlockHashRequest] { 62 | final def apply(a: BestBlockHashRequest): Json = 63 | Json.obj(requestFields("getbestblockhash", Array[Json]()): _*) 64 | } 65 | 66 | implicit val blockRequest = new RPCEncoder[BlockRequest] { 67 | final def apply(a: BlockRequest): Json = 68 | Json.obj(requestFields("getblock", Array(Json.fromString(a.hash))): _*) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/scala/omni/Instances.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.omni 18 | 19 | import cats.effect.IO 20 | import io.circe.generic.auto._ 21 | import io.tokenanalyst.blockchainrpc.Codecs._ 22 | import io.tokenanalyst.blockchainrpc.OmniMethods._ 23 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 24 | import io.tokenanalyst.blockchainrpc.omni.Codecs._ 25 | import io.tokenanalyst.blockchainrpc.omni.Protocol._ 26 | import io.tokenanalyst.blockchainrpc.{BatchRequest, BatchResponse, Omni} 27 | 28 | object Instances { 29 | implicit val getNextBlockHashInstance = 30 | new GetNextBlockHash[Omni] { 31 | override def getNextBlockHash(a: Omni): IO[String] = 32 | a.client.nextBlockHash() 33 | } 34 | 35 | implicit val listBlockTransactionsInstance = 36 | new ListBlockTransactions { 37 | override def listBlockTransactions( 38 | omni: Omni, 39 | height: Long 40 | ): IO[Seq[String]] = 41 | for { 42 | json <- omni.client.requestJson[BlockTransactionsRequest]( 43 | BlockTransactionsRequest(height) 44 | ) 45 | } yield json.asObject.get("result").get.asArray.get.map(_.asString.get) 46 | } 47 | 48 | implicit val getTransactionInstance = 49 | new GetTransaction[Omni, TransactionResponse] { 50 | override def getTransaction( 51 | omni: Omni, 52 | hash: String 53 | ): IO[TransactionResponse] = { 54 | for { 55 | res <- omni.client.request[TransactionRequest, TransactionResponse]( 56 | TransactionRequest(hash) 57 | ) 58 | } yield res 59 | } 60 | } 61 | 62 | implicit val getTransactionsInstance = 63 | new GetTransactions[Omni, BatchResponse[TransactionResponse]] { 64 | override def getTransactions( 65 | omni: Omni, 66 | hashes: Seq[String] 67 | ): IO[BatchResponse[TransactionResponse]] = 68 | for { 69 | res <- omni.client 70 | .request[BatchRequest[TransactionRequest], BatchResponse[ 71 | TransactionResponse 72 | ]]( 73 | BatchRequest[TransactionRequest]( 74 | hashes.map(TransactionRequest.apply) 75 | ) 76 | ) 77 | } yield res 78 | } 79 | 80 | implicit val getBlockHashInstance = new GetBlockHash[Omni] { 81 | override def getBlockHash(a: Omni, height: Long): IO[String] = 82 | for { 83 | json <- a.client 84 | .requestJson[BlockHashRequest](BlockHashRequest(height)) 85 | } yield json.asObject.get("result").get.asString.get 86 | } 87 | 88 | implicit val getBestBlockHashInstance = new GetBestBlockHash[Omni] { 89 | override def getBestBlockHash(omni: Omni): IO[String] = 90 | for { 91 | json <- omni.client 92 | .requestJson[BestBlockHashRequest](new BestBlockHashRequest) 93 | } yield json.asObject.get("result").get.asString.get 94 | } 95 | 96 | implicit val getBlockByHashInstance = 97 | new GetBlockByHash[Omni, BlockResponse] { 98 | override def getBlockByHash( 99 | a: Omni, 100 | hash: String 101 | ): IO[BlockResponse] = { 102 | a.client.request[BlockRequest, BlockResponse](BlockRequest(hash)) 103 | } 104 | } 105 | 106 | implicit val getBlockByHeightInstance = 107 | new GetBlockByHeight[Omni, BlockResponse] { 108 | override def getBlockByHeight( 109 | a: Omni, 110 | height: Long 111 | ): IO[BlockResponse] = 112 | for { 113 | hash <- getBlockHashInstance.getBlockHash(a, height) 114 | data <- getBlockByHashInstance.getBlockByHash(a, hash) 115 | } yield data 116 | } 117 | 118 | implicit val getBestBlockHeightInstance = 119 | new GetBestBlockHeight[Omni] { 120 | override def getBestBlockHeight(a: Omni): IO[Long] = 121 | for { 122 | hash <- getBestBlockHashInstance.getBestBlockHash(a) 123 | block <- getBlockByHashInstance.getBlockByHash(a, hash) 124 | } yield block.height 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/scala/omni/Protocol.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.omni 18 | 19 | import io.tokenanalyst.blockchainrpc._ 20 | 21 | object Protocol { 22 | case class TransactionResponse( 23 | txid: String, 24 | block: Long, 25 | blockhash: String, 26 | confirmations: Int, 27 | positioninblock: Option[Int], 28 | version: Option[Int], 29 | blocktime: Long, 30 | valid: Option[Boolean], 31 | invalidreason: Option[String], 32 | type_int: Option[Int], 33 | `type`: Option[String], 34 | propertyid: Option[Long], 35 | amount: Option[Double], 36 | fee: Option[Double], 37 | sendingaddress: String, 38 | referenceaddress: Option[String] 39 | ) extends RPCResponse 40 | 41 | case class BlockResponse( 42 | height: Long, 43 | hash: String, 44 | previousblockhash: Option[String], 45 | nonce: Long, 46 | strippedsize: Long, 47 | merkleroot: String, 48 | version: Int, 49 | weight: Int, 50 | difficulty: Double, 51 | chainwork: String, 52 | bits: String, 53 | size: Long, 54 | mediantime: Long, 55 | time: Long, 56 | tx: List[String] 57 | ) extends RPCResponse 58 | 59 | case class BlockTransactionsRequest(height: Long) extends RPCRequest 60 | case class TransactionRequest(hash: String) extends RPCRequest 61 | case class BestBlockHashRequest() extends RPCRequest 62 | case class BlockRequest(hash: String) extends RPCRequest 63 | case class BlockHashRequest(height: Long) extends RPCRequest 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/omni/Syntax.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.omni 18 | 19 | import io.tokenanalyst.blockchainrpc.BatchResponse 20 | import io.tokenanalyst.blockchainrpc.Omni 21 | import io.tokenanalyst.blockchainrpc.BasicMethods._ 22 | import io.tokenanalyst.blockchainrpc.OmniMethods._ 23 | import io.tokenanalyst.blockchainrpc.omni.Instances._ 24 | 25 | import Protocol._ 26 | 27 | object Syntax { 28 | implicit class OmniOps(omni: Omni) { 29 | def listBlockTransactions(height: Long) = 30 | implicitly[ListBlockTransactions].listBlockTransactions(omni, height) 31 | 32 | def getTransaction(hash: String) = 33 | implicitly[GetTransaction[Omni, TransactionResponse]].getTransaction(omni, hash) 34 | 35 | def getTransactions(hashes: Seq[String]) = 36 | implicitly[GetTransactions[Omni, BatchResponse[TransactionResponse]]] 37 | .getTransactions(omni, hashes) 38 | 39 | def getNextBlockHash() = 40 | implicitly[GetNextBlockHash[Omni]].getNextBlockHash(omni) 41 | 42 | def getBlockByHeight(height: Long) = 43 | implicitly[GetBlockByHeight[Omni, BlockResponse]] 44 | .getBlockByHeight(omni, height) 45 | 46 | def getBestBlockHash() = 47 | implicitly[GetBestBlockHash[Omni]].getBestBlockHash(omni) 48 | 49 | def getBestBlockHeight() = 50 | implicitly[GetBestBlockHeight[Omni]].getBestBlockHeight(omni) 51 | 52 | def getBlockByHash(hash: String) = 53 | implicitly[GetBlockByHash[Omni, BlockResponse]].getBlockByHash(omni, hash) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/scala/bitcoin/ProtocolSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.test.bitcoin 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import io.tokenanalyst.blockchainrpc.bitcoin.Protocol._ 22 | import io.tokenanalyst.blockchainrpc.Codecs._ 23 | import io.circe.generic.auto._ 24 | import io.circe.parser.decode 25 | 26 | class ProtocolSpec extends AnyFlatSpec with Matchers { 27 | 28 | behavior of "Bitcoin protocol" 29 | 30 | it should "decode BlockResponse" in { 31 | val response = 32 | """ 33 | {"result":{"hash":"00000000000000000009db93b0bd627158b665cd954cc274c056ad8b257c3f35","confirmations":1,"strippedsize":779713,"size":1654295,"weight":3993434,"height":607523,"version":536928256,"versionHex":"2000e000","merkleroot":"e1c20883d44eead043c4d8bf118520e61c72183145067d212af8fba319d67ba5","tx":["26de2820d5e15884a18c24426c6fafa6f527cca0f67a0266aa9737690b0bf3bc"],"time":1575992821,"mediantime":1575988911,"nonce":3271658646,"bits":"1715dbd2","difficulty":12876842089682.48,"chainwork":"00000000000000000000000000000000000000000ac014e49df992a6caa4cd1c","nTx":2273,"previousblockhash":"0000000000000000000280301806aa6a1e74ff64139f2dd03c3ac30802739c7a"},"error":null,"id":"curltest"} 34 | """ 35 | val decoded = decode[BlockResponse](response) 36 | decoded shouldEqual (Right( 37 | BlockResponse( 38 | 607523, 39 | "00000000000000000009db93b0bd627158b665cd954cc274c056ad8b257c3f35", 40 | Some( 41 | "0000000000000000000280301806aa6a1e74ff64139f2dd03c3ac30802739c7a" 42 | ), 43 | 3271658646L, 44 | 779713, 45 | "e1c20883d44eead043c4d8bf118520e61c72183145067d212af8fba319d67ba5", 46 | 536928256, 47 | 3993434, 48 | 12876842089682.48, 49 | "00000000000000000000000000000000000000000ac014e49df992a6caa4cd1c", 50 | "1715dbd2", 51 | 1654295, 52 | 1575988911, 53 | 1575992821, 54 | 2273, 55 | List("26de2820d5e15884a18c24426c6fafa6f527cca0f67a0266aa9737690b0bf3bc") 56 | ) 57 | )) 58 | } 59 | 60 | it should "decode TransactionResponse" in { 61 | val response = 62 | """ 63 | {"result":{"txid":"3246fa6d081b32223a4097052b15594acc00372a9ce293b29f92086e19655888","hash":"20ed27407730078e01478e375910cd92ec4c99c0e1763a893df877e36a63f24f","version":1,"size":193,"vsize":111,"weight":442,"locktime":0,"vin":[{"txid":"eee52abb8ef949f0eaba5b331b4aa6de13601b00a93d99ed0cc75189be3d2a24","vout":1,"scriptSig":{"asm":"","hex":""},"txinwitness":["3045022100dc5b8d3643750a3b96defa6e452b268d25bdcaba9d75e3614aaa8f59c285bd3902206b44833a75111ce4bd487be86972def80db4ef8c2865e8b1c19cda5f9f7ee09101","035d6e3719f26bcc5381eca4e5c33ff3f9fd5b4e84450f863abbd83a584ccdc204"],"sequence":4294967295}],"vout":[{"value":0.00803580,"n":0,"scriptPubKey":{"asm":"OP_HASH160 ab974139080e159ac8af9fc0d9e2ef4885a1339d OP_EQUAL","hex":"a914ab974139080e159ac8af9fc0d9e2ef4885a1339d87","reqSigs":1,"type":"scripthash","addresses":["3HLJfiJLa5CnKQoAF3YeteidRjDMvY5upH"]}}],"hex":"01000000000101242a3dbe8951c70ced993da9001b6013dea64a1b335bbaeaf049f98ebb2ae5ee0100000000ffffffff01fc420c000000000017a914ab974139080e159ac8af9fc0d9e2ef4885a1339d8702483045022100dc5b8d3643750a3b96defa6e452b268d25bdcaba9d75e3614aaa8f59c285bd3902206b44833a75111ce4bd487be86972def80db4ef8c2865e8b1c19cda5f9f7ee0910121035d6e3719f26bcc5381eca4e5c33ff3f9fd5b4e84450f863abbd83a584ccdc20400000000","blockhash":"0000000000000000000e924f2fc7105362ce640d0865d10d314086c795d2cfde","confirmations":1,"time":1575996190,"blocktime":1575996190},"error":null,"id":"curltest"} 64 | """ 65 | val decoded = decode[TransactionResponse](response) 66 | decoded shouldEqual Right( 67 | TransactionResponse( 68 | Some(1), 69 | "0000000000000000000e924f2fc7105362ce640d0865d10d314086c795d2cfde", 70 | 1575996190L, 71 | "20ed27407730078e01478e375910cd92ec4c99c0e1763a893df877e36a63f24f", 72 | "01000000000101242a3dbe8951c70ced993da9001b6013dea64a1b335bbaeaf049f98ebb2ae5ee0100000000ffffffff01fc420c000000000017a914ab974139080e159ac8af9fc0d9e2ef4885a1339d8702483045022100dc5b8d3643750a3b96defa6e452b268d25bdcaba9d75e3614aaa8f59c285bd3902206b44833a75111ce4bd487be86972def80db4ef8c2865e8b1c19cda5f9f7ee0910121035d6e3719f26bcc5381eca4e5c33ff3f9fd5b4e84450f863abbd83a584ccdc20400000000", 73 | "3246fa6d081b32223a4097052b15594acc00372a9ce293b29f92086e19655888", 74 | 1575996190L, 75 | 111, 76 | 193, 77 | 442, 78 | 1, 79 | List( 80 | TransactionResponseVin( 81 | Some( 82 | "eee52abb8ef949f0eaba5b331b4aa6de13601b00a93d99ed0cc75189be3d2a24" 83 | ), 84 | Some(1), 85 | Some(TransactionResponseScriptSig("", "")), 86 | None, 87 | 4294967295L 88 | ) 89 | ), 90 | List( 91 | TransactionResponseVout( 92 | 0.00803580, 93 | 0, 94 | TransactionResponseScript( 95 | "OP_HASH160 ab974139080e159ac8af9fc0d9e2ef4885a1339d OP_EQUAL", 96 | "a914ab974139080e159ac8af9fc0d9e2ef4885a1339d87", 97 | Some(1), 98 | "scripthash", 99 | Some(List("3HLJfiJLa5CnKQoAF3YeteidRjDMvY5upH")) 100 | ) 101 | ) 102 | ), 103 | 0 104 | ) 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/scala/ethereum/ProtocolSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.tokenanalyst.blockchainrpc.test.ethereum 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import io.tokenanalyst.blockchainrpc.ethereum.Protocol.{BlockResponse, _} 22 | import io.tokenanalyst.blockchainrpc.Codecs._ 23 | import io.circe.generic.auto._ 24 | import io.circe.parser.decode 25 | 26 | class ProtocolSpec extends AnyFlatSpec with Matchers { 27 | 28 | behavior of "Ethereum protocol" 29 | 30 | it should """decode ReceiptResponse UA to UA Pre-byzantinium""" in { 31 | val response = 32 | """ 33 | { 34 | "jsonrpc": "2.0", 35 | "result": { 36 | "blockHash": "0x3a1fba5abd9d41457944e91ed097e039b7b12d3d7ba324a3f422db2277a48e28", 37 | "blockNumber": "0xcb3d", 38 | "contractAddress": null, 39 | "cumulativeGasUsed": "0xab4d", 40 | "from": null, 41 | "gasUsed": null, 42 | "logs": [ 43 | { 44 | "address": "0x5564886ca2c518d1964e5fcea4f423b41db9f561", 45 | "blockHash": null, 46 | "blockNumber": null, 47 | "data": "0x", 48 | "logIndex": null, 49 | "removed": false, 50 | "topics": [ 51 | "0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc", 52 | "0x4d794669727374436f696e000000000000000000000000000000000000000000" 53 | ], 54 | "transactionHash": null, 55 | "transactionIndex": null, 56 | "transactionLogIndex": null, 57 | "type": "pending" 58 | } 59 | ], 60 | "logsBloom": "0x00000000000000000100000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000400000000000000000010", 61 | "root": "0x31ebb0ae686c616ea261dd67c1b433a78a8238add284d7b9e8edf3d0e4789ad5", 62 | "to": null, 63 | "transactionHash": "0x218b632d932371478d1ae5a01620ebab1a2030f9dad6f8fba4a044ea6335a57e", 64 | "transactionIndex": "0x0" 65 | }, 66 | "id": 1 67 | } 68 | """ 69 | val decoded = decode[ReceiptResponse](response) 70 | decoded shouldEqual Right( 71 | ReceiptResponse( 72 | "0x3a1fba5abd9d41457944e91ed097e039b7b12d3d7ba324a3f422db2277a48e28", 73 | "0xcb3d", 74 | None, 75 | None, 76 | None, 77 | "0xab4d", 78 | None, 79 | List( 80 | LogResponse( 81 | false, 82 | None, 83 | None, 84 | None, 85 | None, 86 | None, 87 | "0x5564886ca2c518d1964e5fcea4f423b41db9f561", 88 | "0x", 89 | List( 90 | "0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc", 91 | "0x4d794669727374436f696e000000000000000000000000000000000000000000" 92 | ), 93 | None, 94 | "pending" 95 | ) 96 | ), 97 | "0x00000000000000000100000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000400000000000000000010", 98 | None, 99 | "0x218b632d932371478d1ae5a01620ebab1a2030f9dad6f8fba4a044ea6335a57e", 100 | "0x0" 101 | ) 102 | ) 103 | } 104 | 105 | it should """decode ReceiptResponse on contract Pre-byzantinium""" in { 106 | val response = 107 | """ 108 | { 109 | "id": 1, 110 | "jsonrpc": "2.0", 111 | "result": { 112 | "blockHash": "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 113 | "blockNumber": "0x6914b0", 114 | "contractAddress": "0x471a8bf3fd0dfbe20658a97155388cec674190bf", 115 | "from": "0xc931d93e97ab07fe42d923478ba2465f2", 116 | "to": null, 117 | "cumulativeGasUsed": "0x158e33", 118 | "gasUsed": "0xba2e6", 119 | "logs": [], 120 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 121 | "root": null, 122 | "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 123 | "transactionIndex": "0x4" 124 | } 125 | } 126 | """ 127 | 128 | val decoded = decode[ReceiptResponse](response) 129 | decoded shouldEqual Right( 130 | ReceiptResponse( 131 | "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 132 | "0x6914b0", 133 | Some("0x471a8bf3fd0dfbe20658a97155388cec674190bf"), 134 | Some("0xc931d93e97ab07fe42d923478ba2465f2"), 135 | None, 136 | "0x158e33", 137 | Some("0xba2e6"), 138 | List(), 139 | "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 140 | None, 141 | "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 142 | "0x4" 143 | ) 144 | ) 145 | } 146 | 147 | it should "decode ReceiptResponse UA to UA Post-byzantinium" in { 148 | val response = 149 | """ 150 | { 151 | "id": 1, 152 | "jsonrpc": "2.0", 153 | "result": { 154 | "blockHash": "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 155 | "blockNumber": "0x6914b0", 156 | "contractAddress": null, 157 | "from": "0xc931d93e97ab07fe42d923478ba2465f2", 158 | "to": "0xc931d93e97ab07fe42d923478ba2465f2", 159 | "cumulativeGasUsed": "0x158e33", 160 | "gasUsed": "0xba2e6", 161 | "logs": [], 162 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 163 | "root": null, 164 | "status": "0x1", 165 | "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 166 | "transactionIndex": "0x4" 167 | } 168 | } 169 | """ 170 | 171 | val decoded = decode[ReceiptResponse](response) 172 | decoded shouldEqual Right( 173 | ReceiptResponse( 174 | "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 175 | "0x6914b0", 176 | None, 177 | Some("0xc931d93e97ab07fe42d923478ba2465f2"), 178 | Some("0xc931d93e97ab07fe42d923478ba2465f2"), 179 | "0x158e33", 180 | Some("0xba2e6"), 181 | List(), 182 | "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 183 | Some("0x1"), 184 | "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 185 | "0x4" 186 | ) 187 | ) 188 | } 189 | 190 | it should "decode ReceiptResponse on contract Post-byzantinium" in { 191 | val response = 192 | """ 193 | { 194 | "id": 1, 195 | "jsonrpc": "2.0", 196 | "result": { 197 | "blockHash": "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 198 | "blockNumber": "0x6914b0", 199 | "contractAddress": "0x471a8bf3fd0dfbe20658a97155388cec674190bf", 200 | "from": "0xc931d93e97ab07fe42d923478ba2465f2", 201 | "to": null, 202 | "cumulativeGasUsed": "0x158e33", 203 | "gasUsed": "0xba2e6", 204 | "logs": [], 205 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 206 | "root": null, 207 | "status": "0x1", 208 | "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 209 | "transactionIndex": "0x4" 210 | } 211 | } 212 | """ 213 | 214 | val decoded = decode[ReceiptResponse](response) 215 | decoded shouldEqual Right( 216 | ReceiptResponse( 217 | "0x67c0303244ae4beeec329e0c66198e8db8938a94d15a366c7514626528abfc8c", 218 | "0x6914b0", 219 | Some("0x471a8bf3fd0dfbe20658a97155388cec674190bf"), 220 | Some("0xc931d93e97ab07fe42d923478ba2465f2"), 221 | None, 222 | "0x158e33", 223 | Some("0xba2e6"), 224 | List(), 225 | "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 226 | Some("0x1"), 227 | "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", 228 | "0x4" 229 | ) 230 | ) 231 | } 232 | 233 | it should "decode TransactionResponse" in { 234 | val response = 235 | """ 236 | {"jsonrpc":"2.0","result":{"blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2","blockNumber":"0x5daf3b","chainId":"0x1","condition":null,"creates":null,"from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b","input":"0x68656c6c6f21","nonce":"0x15","publicKey":"0xd200bc91709a525ba0b5ce99053f239d890a78327eb038a87bcd4f8166a30b825a4122f0998946a4b7c7996b93e2d4aff8cf9a3fcdaffffae0badf90a3f9a4df","r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea","raw":"0xf871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c","s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c","standardV":"0x0","to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb","transactionIndex":"0x41","v":"0x25","value":"0xf3dbb76162000"},"id":1} 237 | """ 238 | 239 | val decoded = decode[TransactionResponse](response) 240 | decoded shouldEqual Right( 241 | TransactionResponse( 242 | "0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", 243 | "0x5daf3b", 244 | Some("0x1"), 245 | "0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", 246 | "0xc350", 247 | "0x4a817c800", 248 | "0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", 249 | "0x68656c6c6f21", 250 | "0x15", 251 | "0xd200bc91709a525ba0b5ce99053f239d890a78327eb038a87bcd4f8166a30b825a4122f0998946a4b7c7996b93e2d4aff8cf9a3fcdaffffae0badf90a3f9a4df", 252 | "0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", 253 | "0xf871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c", 254 | "0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c", 255 | "0x25", 256 | "0x0", 257 | Some("0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb"), 258 | "0x41", 259 | "0xf3dbb76162000", 260 | None, 261 | None 262 | ) 263 | ) 264 | } 265 | 266 | it should "decode BlockResponse" in { 267 | val response = 268 | """ 269 | {"jsonrpc":"2.0","result":{"author":"0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c","difficulty":"0x89e4c5695f464","extraData":"0x5050594520737061726b706f6f6c2d6574682d636e2d687a32","gasLimit":"0x9879a1","gasUsed":"0x0","hash":"0xd5e3eff1778c4735fb2a51c5c4e92bec32f9363ba23285d6d94e62c26b7c0884","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c","mixHash":"0x1c0540069d772ce8fe02415a5988ad5b90b389a13d1a0643087ea75487007aef","nonce":"0xdf2bd7300001a5e8","number":"0x865801","parentHash":"0x65144a20fd4d1b2faa86d90d35b9cfcc87b67f48b21ad439167d80429b719258","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa01c0540069d772ce8fe02415a5988ad5b90b389a13d1a0643087ea75487007aef","0x88df2bd7300001a5e8"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x21f","stateRoot":"0x4b909ee750967307134206db327f361b1877bffbdbd2c1100449dd4759c350c9","timestamp":"0x5db1e4c3","totalDifficulty":"0x2a7666ad70f09d1728f","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]},"id":1} 270 | """ 271 | val decoded = decode[BlockResponse](response) 272 | decoded shouldEqual Right( 273 | new BlockResponse( 274 | "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", 275 | "0x89e4c5695f464", 276 | "0x5050594520737061726b706f6f6c2d6574682d636e2d687a32", 277 | "0x9879a1", 278 | "0x0", 279 | "0xd5e3eff1778c4735fb2a51c5c4e92bec32f9363ba23285d6d94e62c26b7c0884", 280 | "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 281 | "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", 282 | "0x1c0540069d772ce8fe02415a5988ad5b90b389a13d1a0643087ea75487007aef", 283 | "0xdf2bd7300001a5e8", 284 | "0x865801", 285 | "0x65144a20fd4d1b2faa86d90d35b9cfcc87b67f48b21ad439167d80429b719258", 286 | "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", 287 | List( 288 | "0xa01c0540069d772ce8fe02415a5988ad5b90b389a13d1a0643087ea75487007aef", 289 | "0x88df2bd7300001a5e8" 290 | ), 291 | "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", 292 | "0x21f", 293 | "0x4b909ee750967307134206db327f361b1877bffbdbd2c1100449dd4759c350c9", 294 | "0x5db1e4c3", 295 | "0x2a7666ad70f09d1728f", 296 | List(), 297 | "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", 298 | List() 299 | ) 300 | ) 301 | } 302 | } 303 | --------------------------------------------------------------------------------