Skip to content

Commit

Permalink
refactor (#4)
Browse files Browse the repository at this point in the history
* add implicit helper function

* refactor

* codegen: replace var with val in generated code

* api: auto codec for request/response

* more apis

* request id per connection

* api: filter id replace with BigInt

* doc: fix doc error

* codegen: update version

* update README & LICENSE
  • Loading branch information
Lbqds authored Jul 4, 2020
1 parent a18c877 commit 7c45b91
Show file tree
Hide file tree
Showing 169 changed files with 2,473 additions and 2,049 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: scala
scala:
- 2.12.8
- 2.13.1
script:
- sbt compile
- sbt test
- sbt +compile
- sbt +test
850 changes: 176 additions & 674 deletions LICENSE

Large diffs are not rendered by default.

203 changes: 113 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,106 +1,103 @@
## eth-abi

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.lbqds/eth-abi_2.12/badge.svg)](https://search.maven.org/artifact/com.github.lbqds/eth-abi_2.12/0.1/jar)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.lbqds/ethabi_2.13/badge.svg)](https://search.maven.org/artifact/com.github.lbqds/ethabi_2.13/0.3.0/jar)
[![Build Status](https://travis-ci.com/Lbqds/eth-abi.svg?token=iUBC3d9KBxXjFrs9989Y&branch=master)](https://travis-ci.com/Lbqds/eth-abi)

generate scala code from solidity contract
jdk 11+ because of [http4s-jdk-http-client](https://github.com/http4s/http4s-jdk-http-client)

### Sonatype
`eth-abi` is currently available for scala 2.12 and scala2.13

### Getting Start

to begin using `eth-abi`, add the following to your `build.sbt`:

```scala
libraryDependencies ++= Seq(
"com.github.lbqds" %% "eth-abi" % "0.1",
"com.typesafe.akka" %% "akka-actor" % "2.5.19",
"com.typesafe.akka" %% "akka-stream" % "2.5.19"
"com.github.lbqds" %% "ethabi" % "0.3.0",
"org.typelevel" %% "cats-core" % "2.1.1",
"org.typelevel" %% "cats-effect" % "2.1.3"
)
```

### codegen
### Code Generator

`eth-abi` have a tool which can generate scala code by solidity contract abi and bin code.

download latest version `abi-codegen.tar.gz` from the [release](https://github.com/Lbqds/eth-abi/releases) page, then execute:
download latest version `abi-codegen-0.3.0` from the [release](https://github.com/Lbqds/eth-abi/releases) page, and execute:

```shell
$ tar -xf abi-codegen.tar.gz
$ scala abi-codegen.jar --help
$ abi-codegen-0.3.0 --help
```

it will show usage as follow:

```text
abi-codegen 0.1
Usage: abi-codegen [options]
use [KVStore](https://github.com/Lbqds/eth-abi/blob/master/examples/src/main/resources/KVStore.abi) contract as an example, execute:

-a, --abi <abiFile> contract abi file
-b, --bin <binFile> contract bin file
-p, --package <packages> package name e.g. "examples.token"
-c, --className <className> class name
-o, --output <output dir> output directory
-h, --help show usage
```shell
$ abi-codegen-0.3.0 gen -a KVStore.abi -b KVStore.bin -p "examples.kvstore" -c "KVStore" -o ./
```

a trivial example as follow:

```solidity
pragma solidity ^0.4.24;
pragma experimental ABIEncoderV2;
contract Trivial {
event TestEvent(uint256 indexed a, bytes b, uint256 indexed c, bytes d);
struct T { uint256 a; bytes b; uint256 c; bytes d; }
function Trivial() public {}
function trigger(T t) public {
emit TestEvent(t.a, t.b, t.c, t.d);
}
}
```
would generate scala code at the current directory. you can also dive into generated code at [here](https://github.com/Lbqds/eth-abi/blob/master/examples/src/main/scala/examples/kvstore/KVStore.scala).

this contract use solidity [experimental ABIEncoderV2](https://solidity.readthedocs.io/en/latest/abi-spec.html#handling-tuple-types) feature,
when we call `trigger` method, it just emit a log, the generated code at [here](https://github.com/Lbqds/eth-abi/blob/master/examples/src/main/scala/examples/trivial/Trivial.scala).
now you can interact with ethereum use the generated scala code:
now we can call generated scala method instead of execute contract method by `eth_sendTransaction` or `eth_sendRawTransaction`:

```scala
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
import system.dispatcher

// creator of contract
val sender = Address("0xe538b17ebf20efcf1c426cf1480e8a2a4b87cb1b")
val contract = new Trivial("ws://127.0.0.1:8546")
val opt = TransactionOpt(Some(BigInt(1000000)), Some(BigInt(10000)), None, None)

contract.deploy(sender, opt)

// waiting to contract deployed
while (!contract.isDeployed) {
Thread.sleep(1000)
}
println("deploy succeed, address is: " + contract.contractAddress)

// subscribe TestEvent
contract.subscribeTestEvent.runForeach(println)

val a = Uint256(BigInt(1000))
val b = DynamicBytes(Array[Byte](0x01, 0x02, 0x03, 0x04))
val c = Uint256(BigInt(3333))
val d = DynamicBytes(Array[Byte](0x11, 0x22, 0x33, 0x44))
val t = TupleType4[Uint256, DynamicBytes, Uint256, DynamicBytes](a, b, c, d)

// call contract trigger method
contract.trigger(t, sender, opt) onComplete {
case Success(txHash) =>
println("call trigger succeed: " + txHash)
system.terminate()
case Failure(exception) => println("call trigger failed: " + exception)
object Main extends IOApp {

private def log(str: String): IO[Unit] = IO.delay(println(s"${Thread.currentThread.getName}, $str"))

override def run(args: List[String]): IO[ExitCode] = {
val sender = Address("60f7947aef8bbc9bc314a9b8db8096099345fba3")
val transactionOpt = TransactionOpt(Some(400000), Some(1000), None, None)
val retryPolicy = retry.RetryPolicies.limitRetries[IO](5).join(RetryPolicies.constantDelay[IO](5 seconds))
KVStore[IO]("ws://127.0.0.1:8546").use { kvStore =>
val task = for {
client <- kvStore.client
peerCount <- client.peerCount.flatMap(_.get)
_ <- log(s"peer count: $peerCount")
cliVersion <- client.clientVersion.flatMap(_.get)
_ <- log(s"client version: $cliVersion")
work <- client.getWork.flatMap(_.get)
_ <- log(s"work response: $work")
protocolV <- client.protocolVersion.flatMap(_.get)
_ <- log(s"protocol version: $protocolV")
coinbase <- client.coinbase.flatMap(_.get)
_ <- log(s"coinbase address: $coinbase")
syncStatus <- client.syncing.flatMap(_.get)
_ <- log(s"sync status: $syncStatus")
deployHash <- kvStore.deploy(sender, transactionOpt)
address <- retryUntil[IO, Option[Address]]("wait contract deployed", retryPolicy, kvStore.address, _.isDefined).map(_.get)
_ <- log(s"contract deploy succeed, address: $address")
contractTx <- deployHash.get.flatMap(client.getTransactionByHash).flatMap(_.get)
_ <- log(s"contract deploy tx: ${contractTx.get}")
result <- kvStore.subscribeRecord
_ <- log(s"subscription id: ${result.id}")
fiber <- result.stream.forall { event =>
println(event)
true
}.compile.drain.start
txHash <- kvStore.set(Uint16(12), DynamicBytes.from("0x010203040506070809"), sender, transactionOpt).flatMap(_.get)
receipt <- retryUntil[IO, Option[TransactionReceipt]](
"wait tx receipt",
retryPolicy,
client.getTransactionReceipt(txHash).flatMap(_.get),
_.isDefined
).map(_.get)
_ <- log(s"tx receipt: $receipt")
result <- kvStore.get(Uint16(12), sender, transactionOpt)
_ <- log(s"key: 12, value: $result")
_ <- fiber.cancel
_ <- log("quit now")
} yield ()
task.handleErrorWith(exp => IO.delay(exp.printStackTrace())) *> IO.delay(ExitCode.Success)
}
}
}
```

**NOTE**:

* the generated code use websocket client rather than http, because it will subscribe solidity event with [ethereum RPC PUB/SUB](https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB)
* you need to assure the account have been unlocked before deploy and call contract method
* every `event` will have a generated `subscribeEventName` method, which just return `Source[EventValue, NotUsed]`, the subscription start only after have been [materialized](https://doc.akka.io/docs/akka/2.5.3/scala/stream/stream-flows-and-basics.html#defining-and-running-streams)
* every `event` will have a generated `subscribeEventName` method, which return `F[Stream[F, Event]]`

### ABIEncoderV2

Expand All @@ -113,25 +110,51 @@ use this feature heavily.
`eth-abi` can also be used to interact directly with ethereum:

```scala
import ethabi.protocol.http.Client
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import scala.util.{Failure, Success}

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
import system.dispatcher

val client = Client("http://127.0.0.1:8545")
client.blockNumber onComplete {
case Failure(exception) => throw exception
case Success(resp) => resp match {
case Left(responseError) => println(s"response error: ${responseError.message}")
case Right(number) =>
println(s"current block number: $number")
system.terminate()
object HttpClientTest extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
import HttpClient._
import scala.concurrent.duration._

apply[IO]("http://127.0.0.1:8545").use { client =>
for {
cVersion <- client.clientVersion.flatMap(_.get)
_ <- IO.delay(println(s"client version: $cVersion"))
nVersion <- client.netVersion.flatMap(_.get)
_ <- IO.delay(println(s"net version: $nVersion"))
filterId <- client.newBlockFilter.flatMap(_.get)
_ <- IO.delay(println(s"filter id: $filterId"))
changes1 <- IO.sleep(3 seconds) *> client.getFilterChangeHashes(filterId).flatMap(_.get)
_ <- IO.delay(println(s"changes: $changes1"))
changes2 <- IO.sleep(3 seconds) *> client.getFilterChangeHashes(filterId).flatMap(_.get)
_ <- IO.delay(println(s"changes: $changes2"))
} yield ExitCode.Success
}
}
}
```

all supported JSONRPC api list at [here](https://github.com/Lbqds/eth-abi/blob/master/src/main/scala/ethabi/protocol/Service.scala).
all supported JSONRPC api list at [here](https://github.com/Lbqds/eth-abi/blob/master/ethabi/src/main/scala/ethabi/protocol/Client.scala).

### Functional Programming

`eth-abi` use [cats](https://github.com/typelevel/cats) and [cats-effect](https://github.com/typelevel/cats-effect),
although the above example use `cats-effect` IO, you can also choose [ZIO](https://github.com/zio/zio)

and all generated scala code are functional style.

## License

Copyright (c) 2020 Lbqds

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

81 changes: 64 additions & 17 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,56 @@ import sbtassembly.AssemblyPlugin.defaultShellScript

lazy val scala212 = "2.12.8"
lazy val scala213 = "2.13.1"
lazy val ver = "0.2.0"
lazy val ethAbiVersion = "0.3.0"

val commonSettings = Seq(
organization := "com.github.lbqds",
crossScalaVersions := Seq(scala212, scala213),
version := ver,
scalacOptions ++= Seq(
"-encoding", "utf8",
// "-Xfatal-warnings",
def scalacOptionByVersion(version: String): Seq[String] = {
val optional: Seq[String] =
if (priorTo213(version)) Seq("-Ypartial-unification")
else Seq("-Ymacro-annotations")

Seq(
"-encoding",
"utf8",
"-Xfatal-warnings",
"-Xlint",
"-deprecation",
// "-unchecked",
"-unchecked",
"-language:implicitConversions",
"-language:higherKinds",
"-language:existentials",
// "-Xlog-implicits",
// "-Xlog-implicit-conversions",
"-language:postfixOps"),
//"-Xlog-implicits",
//"-Xlog-implicit-conversions",
"-language:postfixOps") ++ optional
}

def priorTo213(version: String): Boolean = {
CrossVersion.partialVersion(version) match {
case Some((2, minor)) if minor < 13 => true
case _ => false
}
}

val commonSettings = Seq(
organization := "com.github.lbqds",
scalaVersion := scala213,
crossScalaVersions := Seq(scala212, scala213),
version := ethAbiVersion,
scalacOptions ++= scalacOptionByVersion(scalaVersion.value),
test in assembly := {}
)

val macroSettings = Seq(
libraryDependencies ++= (Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided,
"org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided
) ++ (
if (priorTo213(scalaVersion.value)) Seq(
compilerPlugin("org.scalamacros" % "paradise" % "2.1.1").cross(CrossVersion.patch)
) else Nil
))

)

import xerial.sbt.Sonatype._
val publishSettings = Seq(
publishTo := sonatypePublishTo.value,
Expand All @@ -43,12 +73,30 @@ val publishSettings = Seq(
publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(true)
)

lazy val root =
Project(id = "root", base = file("."))
.settings(commonSettings)
.settings(macroSettings)
.settings(name := "root")
.settings(publishSettings)
.aggregate(ethabi, codegen, examples)
.disablePlugins(sbtassembly.AssemblyPlugin)

lazy val ethabi =
Project(id = "eth-abi", base = file("."))
Project(id = "ethabi", base = file("ethabi"))
.settings(commonSettings)
.settings(name := "eth-abi")
.settings(macroSettings)
.settings(name := "ethabi")
.settings(Dependencies.deps)
.settings(publishSettings)
.settings(
unmanagedSourceDirectories in Compile += {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n >= 13 => (scalaSource in Compile).value.getParentFile / "scala-2.13+"
case _ => (scalaSource in Compile).value.getParentFile / "scala-2.13-"
}
}
)
.enablePlugins(spray.boilerplate.BoilerplatePlugin)
.disablePlugins(sbtassembly.AssemblyPlugin)

Expand All @@ -60,14 +108,13 @@ lazy val codegen =
.settings(
name := "codegen",
assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)),
assemblyJarName := s"abi-codegen-$ver",
assemblyJarName := s"abi-codegen-$ethAbiVersion",
skip in publish := true
)

lazy val example =
lazy val examples =
Project(id = "examples", base = file("examples"))
.settings(commonSettings)
.settings(Dependencies.examplesDpes)
.dependsOn(ethabi)
.disablePlugins(sbtassembly.AssemblyPlugin)
.settings(
Expand Down
Loading

0 comments on commit 7c45b91

Please sign in to comment.