You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2020/12/15 06:30:07 UTC

[incubator-tuweni] branch master updated: Add faucet app (#184)

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git


The following commit(s) were added to refs/heads/master by this push:
     new d268dc2  Add faucet app (#184)
d268dc2 is described below

commit d268dc268c49a7c22b03e3770a0e047b4b7faa78
Author: Antoine Toulme <at...@users.noreply.github.com>
AuthorDate: Mon Dec 14 22:24:08 2020 -0800

    Add faucet app (#184)
    
    * Add faucet app
    
    * fix jackson upgrade issue
    
    * Fix weirdness with spring boot main class
    
    * Fix tests and add headers
    
    * spotless
    
    * One more jackson upgrade issue
    
    * Move faucet to top-level
    
    * remove examples folder
---
 .idea/codeStyles/Project.xml                       |   7 ++
 .idea/codeStyles/codeStyleConfig.xml               |   1 -
 build.gradle                                       |  10 ++
 dependency-versions.gradle                         |   7 +-
 eth-faucet/README.md                               |  48 ++++++++
 eth-faucet/build.gradle                            |  62 +++++++++++
 .../org/apache/tuweni/faucet/FaucetApplication.kt  | 123 +++++++++++++++++++++
 .../tuweni/faucet/controller/FaucetController.kt   | 119 ++++++++++++++++++++
 .../tuweni/faucet/controller/FaucetRequest.kt      |  19 ++++
 eth-faucet/src/main/resources/application.yml      |  57 ++++++++++
 .../src/main/resources/static/error/4xx.html       |  25 +++++
 .../src/main/resources/static/error/5xx.html       |  25 +++++
 eth-faucet/src/main/resources/templates/index.html |  43 +++++++
 jsonrpc/build.gradle                               |   3 +
 .../tuweni/jsonrpc/ClientRequestException.kt       |  22 ++++
 .../org/apache/tuweni/jsonrpc/JSONRPCClient.kt     |  79 ++++++++++++-
 .../org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt |   2 +-
 .../tuweni/scuttlebutt/lib/KeyFileLoader.java      |   3 +-
 .../org/apache/tuweni/scuttlebutt/rpc/Utils.java   |   3 +-
 settings.gradle                                    |   1 +
 .../java/org/apache/tuweni/units/ethereum/Wei.java |  11 ++
 .../org/apache/tuweni/units/ethereum/WeiTest.java  |   5 +
 .../main/kotlin/org/apache/tuweni/wallet/Wallet.kt |   4 +
 23 files changed, 666 insertions(+), 13 deletions(-)

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index d441e85..0a95645 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -21,6 +21,13 @@
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
     <codeStyleSettings language="kotlin">
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </codeStyleSettings>
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 6e6eec1..79ee123 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,6 +1,5 @@
 <component name="ProjectCodeStyleConfiguration">
   <state>
     <option name="USE_PER_PROJECT_SETTINGS" value="true" />
-    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
   </state>
 </component>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 77f4726..a9ef9b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,10 +29,12 @@ buildscript {
 plugins {
   id 'com.diffplug.spotless' version '5.8.2'
   id 'net.ltgt.errorprone' version '1.2.1'
+  id 'org.springframework.boot' version '2.4.1'
   id 'io.spring.dependency-management' version '1.0.6.RELEASE'
   id 'com.github.hierynomus.license' version '0.15.0'
   id 'org.gradle.crypto.checksum' version '1.1.0'
   id 'org.jetbrains.kotlin.jvm' version '1.4.20'
+  id 'org.jetbrains.kotlin.plugin.spring' version '1.4.20'
   id 'org.jetbrains.dokka' version "0.10.1"
   id 'maven-publish'
   id 'com.jfrog.bintray' version '1.8.3'
@@ -237,6 +239,14 @@ allprojects {
   apply from: "${rootDir}/dependency-versions.gradle"
   apply from: "${rootDir}/gradle/check-licenses.gradle"
 
+  bootJar {
+    enabled = false
+  }
+
+  jar {
+    enabled = true
+  }
+
   version = buildVersion
 
   repositories {
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index 144be1d..1b45efd 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -14,8 +14,8 @@ dependencyManagement {
   dependencies {
     dependency('ch.qos.logback:logback-classic:1.2.3')
     dependency('commons-codec:commons-codec:1.14')
-    dependency('com.fasterxml.jackson.core:jackson-databind:2.9.5')
-    dependency('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.8')
+    dependency('com.fasterxml.jackson.core:jackson-databind:2.11.0')
+    dependency('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0')
     dependency('com.github.jnr:jnr-ffi:2.1.9')
     dependency('com.github.kstyrc:embedded-redis:0.6')
     dependency('com.google.code.findbugs:jsr305:3.0.2')
@@ -35,7 +35,8 @@ dependencyManagement {
     dependency('io.ktor:ktor-network:1.4.2')
     dependency('io.ktor:ktor-server-core:1.4.2')
     dependency('io.ktor:ktor-server-netty:1.4.2')
-    ''
+    dependency('org.springframework.boot:spring-boot-starter-thymeleaf:2.4.1')
+
     dependency('io.lettuce:lettuce-core:5.1.3.RELEASE')
     dependency('io.vertx:vertx-core:3.9.4')
     dependency('io.vertx:vertx-lang-kotlin:3.9.4')
diff --git a/eth-faucet/README.md b/eth-faucet/README.md
new file mode 100644
index 0000000..dab109b
--- /dev/null
+++ b/eth-faucet/README.md
@@ -0,0 +1,48 @@
+# Ethereum Faucet
+
+This example allows you to set up a faucet with Github authentication.
+
+The application is written in Kotlin with Spring Boot, with Spring Web, Spring Security and Thymeleaf templates.
+
+The app is configured with the values in src/main/resources/application.yml.
+
+# Faucet
+
+This web application creates an account on chain and allows folks to request money from it.
+
+The faucet will top up their accounts up to to max balance that is permitted.
+
+## Running locally
+
+Start the faucet with the main method in `FaucetApplication`.
+
+When the system starts, enter the password for your wallet. If it is the first time the application runs, the wallet is created.
+
+Navigate to localhost:8080 and sign in using github.
+
+You will then be greeted to a page where you can ask for funds.
+
+In parallel, start Hyperledger Besu:
+
+`$> besu --network=dev --rpc-http-enabled --host-allowlist=* --rpc-http-cors-origins=* --miner-enabled --miner-coinbase 0xfe3b557e8fb62b89f4916b721be55ceb828dbd73`
+
+This allows to run Besu with just one node.
+
+In the web page, note the faucet account address. Make sure to send money to that faucet account (you can use Metamask for this, and the dev network private keys are documented).
+
+Now you can send money using the faucet. Enter any valid address and press OK.
+
+The second time you ask for money, the faucet will detect the balance of the account matches the max the faucet with top up.
+
+# License
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+to You 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.
diff --git a/eth-faucet/build.gradle b/eth-faucet/build.gradle
new file mode 100644
index 0000000..31eccb8
--- /dev/null
+++ b/eth-faucet/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+
+description = 'Ethereum Faucet'
+apply plugin: 'org.springframework.boot'
+apply plugin: 'org.jetbrains.kotlin.plugin.spring'
+
+bootJar {
+  enabled = true
+  mainClassName = 'org.apache.tuweni.faucet.FaucetApplicationKt'
+}
+
+jar {
+  enabled = false
+}
+
+dependencies {
+  implementation 'com.fasterxml.jackson.core:jackson-databind'
+  implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+  implementation 'io.vertx:vertx-core'
+  implementation 'io.vertx:vertx-lang-kotlin'
+  implementation 'io.vertx:vertx-lang-kotlin-coroutines'
+  implementation 'org.bouncycastle:bcprov-jdk15on'
+  implementation 'org.springframework:spring-webflux'
+  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
+  implementation("org.jetbrains.kotlin:kotlin-reflect")
+  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
+  implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+  implementation("org.springframework.boot:spring-boot-starter")
+  implementation("org.springframework.boot:spring-boot-starter-web")
+  implementation("org.springframework.boot:spring-boot-starter-security")
+  implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
+  implementation project(':wallet')
+  implementation project(':bytes')
+  implementation project(':eth')
+  implementation project(':jsonrpc')
+  implementation project(':units')
+
+  testImplementation project(':junit')
+  testImplementation 'org.junit.jupiter:junit-jupiter-api'
+  testImplementation 'org.junit.jupiter:junit-jupiter-params'
+  testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin'
+  testImplementation 'org.mockito:mockito-junit-jupiter'
+  testImplementation 'org.glassfish.jersey.core:jersey-client'
+  testImplementation("org.springframework.boot:spring-boot-starter-test")
+  testImplementation("org.springframework.security:spring-security-test")
+
+  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+
+  runtimeOnly 'ch.qos.logback:logback-classic'
+}
diff --git a/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/FaucetApplication.kt b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/FaucetApplication.kt
new file mode 100644
index 0000000..8658c4d
--- /dev/null
+++ b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/FaucetApplication.kt
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.faucet
+
+import io.vertx.core.Vertx
+import org.apache.tuweni.wallet.Wallet
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Bean
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService
+import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException
+import org.springframework.security.oauth2.core.OAuth2Error
+import org.springframework.security.oauth2.core.user.OAuth2User
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.client.WebClient
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.security.Security
+import java.util.Scanner
+import javax.annotation.PostConstruct
+import javax.annotation.PreDestroy
+
+@SpringBootApplication
+class FaucetApplication {
+
+  @Value("\${banner}")
+  var banner: String? = null
+
+  @Value("\${auth.org}")
+  var authorizedOrg: String? = null
+
+  @Bean("wallet")
+  fun createWallet(@Value("\${wallet.path}") path: String, @Value("\${wallet.password}") password: String): Wallet {
+    val walletPath = Paths.get(path).toAbsolutePath()
+    if (!Files.exists(walletPath)) {
+      return Wallet.create(walletPath, password)
+    }
+    return Wallet.open(walletPath, password)
+  }
+
+  val vertx = Vertx.vertx()
+
+  @Bean
+  fun createVertx(): Vertx {
+    return vertx
+  }
+
+  @Bean
+  fun oauth2UserService(rest: WebClient): OAuth2UserService<OAuth2UserRequest, OAuth2User>? {
+    val delegate = DefaultOAuth2UserService()
+    return OAuth2UserService { request: OAuth2UserRequest ->
+      val user = delegate.loadUser(request)
+      val client = OAuth2AuthorizedClient(request.clientRegistration, user.name, request.accessToken)
+      val url = user.getAttribute<String>("organizations_url")
+      val orgs = rest
+        .get().uri(url ?: "")
+        .attributes(oauth2AuthorizedClient(client))
+        .retrieve()
+        .bodyToMono(MutableList::class.java)
+        .block()
+      val found = orgs?.stream()?.anyMatch { org ->
+        authorizedOrg == (org as Map<*, *>)["login"]
+      } ?: false
+      if (!found) {
+        throw OAuth2AuthenticationException(OAuth2Error("invalid_token", "Not in authorized team", ""))
+      }
+      user
+    }
+  }
+
+  @PostConstruct
+  fun banner() {
+    banner?.let {
+      println(it)
+    }
+  }
+
+  @PreDestroy
+  fun close() {
+    vertx.close()
+  }
+}
+
+fun main(args: Array<String>) {
+  Security.addProvider(BouncyCastleProvider())
+  println("Please enter your wallet password:")
+  val scanner = Scanner(System.`in`)
+  val password: String = scanner.next()
+  scanner.close()
+  runApplication<FaucetApplication>(*args, "--wallet.password=$password")
+}
+
+@Component("htmlConfig")
+class HtmlConfig() {
+  @Value("\${html.title}")
+  var title: String? = null
+
+  @Value("\${html.request_message}")
+  var requestMessage: String? = null
+
+  @Value("\${faucet.maxETH}")
+  var maxETH: Long? = null
+}
diff --git a/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetController.kt b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetController.kt
new file mode 100644
index 0000000..f5b783e
--- /dev/null
+++ b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetController.kt
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.faucet.controller
+
+import io.vertx.core.Vertx
+import kotlinx.coroutines.runBlocking
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.eth.Address
+import org.apache.tuweni.jsonrpc.ClientRequestException
+import org.apache.tuweni.jsonrpc.JSONRPCClient
+import org.apache.tuweni.units.ethereum.Gas
+import org.apache.tuweni.units.ethereum.Wei
+import org.apache.tuweni.wallet.Wallet
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Controller
+import org.springframework.ui.Model
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.ModelAttribute
+import org.springframework.web.bind.annotation.PostMapping
+import javax.annotation.PostConstruct
+
+val logger = LoggerFactory.getLogger("faucet")
+
+@Controller
+class FaucetController {
+
+  @Autowired
+  val vertx: Vertx? = null
+
+  var jsonrpcClient: JSONRPCClient? = null
+
+  @Value("\${faucet.chainId}")
+  var chainId: Int? = null
+
+  @Autowired
+  var wallet: Wallet? = null
+
+  @Value("\${faucet.maxETH}")
+  var maxETH: Long? = null
+
+  @Value("\${faucet.rpcPort}")
+  var rpcPort: Int? = null
+
+  @Value("\${faucet.rpcHost}")
+  var rpcHost: String? = null
+
+  @PostConstruct
+  fun createClient() {
+    jsonrpcClient = JSONRPCClient(vertx!!, rpcPort!!, rpcHost!!)
+  }
+
+  @GetMapping("/")
+  fun index(model: Model): String {
+    model.addAttribute("faucetRequest", FaucetRequest("", ""))
+    return "index"
+  }
+
+  @PostMapping("/")
+  fun send(@ModelAttribute request: FaucetRequest, model: Model): String {
+    model.addAttribute("faucetRequest", request)
+    val addr: Address
+    try {
+      addr = Address.fromHexString(request.addr ?: "")
+    } catch (e: IllegalArgumentException) {
+      request.message = e.message ?: "Invalid address"
+      return "index"
+    }
+    try {
+      return runBlocking {
+        // check if the address has more than the maxETH. If it does, we don't need to send money there.
+        val balance = jsonrpcClient!!.getBalance_latest(addr)
+        val lessThanMax = Wei.fromEth(maxETH!!).compareTo(balance) == 1
+        if (!lessThanMax) {
+          request.message = "Balance is more than this faucet gives."
+          return@runBlocking "index"
+        }
+        val missing = Wei.fromEth(maxETH!!).subtract(balance)
+        val nonce = jsonrpcClient!!.getTransactionCount_latest(wallet!!.address())
+        // Otherwise, send money with the faucet account.
+        logger.info("Sending $missing to $addr")
+        val tx =
+          wallet!!.sign(
+            nonce,
+            Wei.valueOf(30000),
+            Gas.valueOf(3000000),
+            addr,
+            missing,
+            Bytes.EMPTY,
+            chainId!!
+          )
+
+        logger.info("Transaction ready to send")
+        val txHash = jsonrpcClient!!.sendRawTransaction(tx)
+        logger.info("Transaction sent to client with hash $txHash")
+        request.message = "Transaction hash: $txHash"
+        return@runBlocking "index"
+      }
+    } catch (e: ClientRequestException) {
+      request.message = e.message
+      return "index"
+    }
+  }
+}
diff --git a/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetRequest.kt b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetRequest.kt
new file mode 100644
index 0000000..094d53c
--- /dev/null
+++ b/eth-faucet/src/main/kotlin/org/apache/tuweni/faucet/controller/FaucetRequest.kt
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.faucet.controller
+
+class FaucetRequest(var addr: String?, var message: String?)
diff --git a/eth-faucet/src/main/resources/application.yml b/eth-faucet/src/main/resources/application.yml
new file mode 100644
index 0000000..884d0f2
--- /dev/null
+++ b/eth-faucet/src/main/resources/application.yml
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+spring:
+  main:
+    banner-mode: "off"
+  security:
+    oauth2:
+      client:
+        registration:
+          github:
+            clientId: 140f55b6cc1b06164bec
+            clientSecret: f1d6b479b741021cbc6d518bcb10ecd34b6d4505
+html:
+  title: Faucet
+  request_message: Welcome to our faucet. You can ask for up to 100 ETH on this faucet.
+
+auth:
+  org: apache
+
+faucet:
+  maxETH: 100
+  chainId: 2018
+  rpcPort: 8545
+  rpcHost: localhost
+
+wallet:
+  path: wallet.key
+banner: >
+  Apache Tuweni Faucet example.
+
+           `:oyhdhhhhhhyo-`
+         :yds/.        ./sdy:
+       :mh:                :hm:
+     `ym:                    :my`
+     hm`                      `mh
+    +N.                        .N+
+    my :ydh/              /hdy- ym
+    Mo`MMMMM`            .MMMMN oM
+    my /hdh/              +hdh: ym
+    +N.                        .N+
+     hm`              `m:     `mh
+     `ym:    `sssssssssN:    :my`
+       :dh:   ``````````   :hd:
+         :yds/.        ./sdy:
+           `-oyhdhhhhdhyo-`
\ No newline at end of file
diff --git a/eth-faucet/src/main/resources/static/error/4xx.html b/eth-faucet/src/main/resources/static/error/4xx.html
new file mode 100644
index 0000000..308ba9c
--- /dev/null
+++ b/eth-faucet/src/main/resources/static/error/4xx.html
@@ -0,0 +1,25 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+<html>
+<head>
+    <title>Faucet</title>
+</head>
+<h1>Error</h1>
+<h3>There was an error.</h3>
+<p>Go back to the <a href="/">home page</a>.</p>
+</body>
+</html>
\ No newline at end of file
diff --git a/eth-faucet/src/main/resources/static/error/5xx.html b/eth-faucet/src/main/resources/static/error/5xx.html
new file mode 100644
index 0000000..308ba9c
--- /dev/null
+++ b/eth-faucet/src/main/resources/static/error/5xx.html
@@ -0,0 +1,25 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+<html>
+<head>
+    <title>Faucet</title>
+</head>
+<h1>Error</h1>
+<h3>There was an error.</h3>
+<p>Go back to the <a href="/">home page</a>.</p>
+</body>
+</html>
\ No newline at end of file
diff --git a/eth-faucet/src/main/resources/templates/index.html b/eth-faucet/src/main/resources/templates/index.html
new file mode 100644
index 0000000..53af428
--- /dev/null
+++ b/eth-faucet/src/main/resources/templates/index.html
@@ -0,0 +1,43 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+<!DOCTYPE HTML>
+<html xmlns:th="https://www.thymeleaf.org">
+<head>
+  <title th:text="${@htmlConfig.title}"></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+<body>
+<h1 th:text="${@htmlConfig.title}"></h1>
+<h3 th:text="${@htmlConfig.requestMessage}"></h3>
+
+<form action="#" th:action="@{/}" th:object="${faucetRequest}" method="post">
+  <h4 th:text="*{message}"></h4>
+  <input
+      type="hidden"
+      th:name="${_csrf.parameterName}"
+      th:value="${_csrf.token}" />
+  <p>Address: <input type="text" th:field="*{addr}"/></p>
+  <p><input type="submit" value="Submit"/> <input type="reset" value="Reset"/></p>
+</form>
+
+<p>Faucet information</p>
+<ul>
+  <li>Address: <span th:text="${@wallet.address}"></span></li>
+  <li>ETH delivery max: <span th:text="${@htmlConfig.maxETH}"></span></li>
+</ul>
+</body>
+</html>
\ No newline at end of file
diff --git a/jsonrpc/build.gradle b/jsonrpc/build.gradle
index ebe6d74..4f155dc 100644
--- a/jsonrpc/build.gradle
+++ b/jsonrpc/build.gradle
@@ -13,6 +13,7 @@
 description = 'Asynchronous Ethereum JSON-RPC client'
 
 dependencies {
+  implementation 'org.slf4j:slf4j-api'
   implementation 'com.fasterxml.jackson.core:jackson-databind'
   implementation "com.google.guava:guava"
   implementation "org.jetbrains.kotlin:kotlin-stdlib"
@@ -31,4 +32,6 @@ dependencies {
   testImplementation 'org.junit.jupiter:junit-jupiter-params'
 
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+
+  runtimeOnly 'ch.qos.logback:logback-classic'
 }
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/ClientRequestException.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/ClientRequestException.kt
new file mode 100644
index 0000000..cf6b496
--- /dev/null
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/ClientRequestException.kt
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.jsonrpc
+
+/**
+ * Exception thrown when a JSON-RPC request is denied.
+ */
+class ClientRequestException(message: String) : RuntimeException(message)
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
index e69f499..cf351bf 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
@@ -20,18 +20,22 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.vertx.core.Vertx
 import io.vertx.core.buffer.Buffer
 import io.vertx.core.http.HttpMethod
+import io.vertx.core.json.JsonObject
 import io.vertx.kotlin.core.http.endAwait
 import kotlinx.coroutines.CompletableDeferred
+import org.apache.tuweni.eth.Address
 import org.apache.tuweni.eth.Transaction
+import org.apache.tuweni.units.bigints.UInt256
+import org.slf4j.LoggerFactory
 import java.io.Closeable
-import java.nio.charset.StandardCharsets
 
+val logger = LoggerFactory.getLogger(JSONRPCClient::class.java)
+val mapper = ObjectMapper()
 /**
  * JSON-RPC client to send requests to an Ethereum client.
  */
-class JSONRPCClient(val vertx: Vertx, val serverPort: Int, val serverHost: String) : Closeable {
+class JSONRPCClient(vertx: Vertx, val serverPort: Int, val serverHost: String) : Closeable {
 
-  val mapper = ObjectMapper()
   val client = vertx.createHttpClient()
 
   /**
@@ -52,7 +56,74 @@ class JSONRPCClient(val vertx: Vertx, val serverPort: Int, val serverHost: Strin
     @Suppress("DEPRECATION")
     client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
       response.bodyHandler {
-        deferred.complete(it.toString(StandardCharsets.UTF_8))
+        val jsonResponse = it.toJson() as JsonObject
+        if (jsonResponse.containsKey("error")) {
+          val err = jsonResponse.getJsonObject("error")
+          val errorMessage = "Code ${err.getInteger("code")}: ${err.getString("message")}"
+          deferred.completeExceptionally(ClientRequestException(errorMessage))
+        } else {
+          deferred.complete(jsonResponse.getString("result"))
+        }
+      }.exceptionHandler {
+        deferred.completeExceptionally(it)
+      }
+    }.putHeader("Content-Type", "application/json")
+      .exceptionHandler { deferred.completeExceptionally(it) }
+      .endAwait(Buffer.buffer(mapper.writeValueAsBytes(body)))
+
+    return deferred.await()
+  }
+
+  /**
+   * Gets the account balance.
+   * @param tx the transaction object to send
+   * @return the hash of the transaction, or an empty string if the hash is not available yet.
+   * @throws ClientRequestException is the request is rejected
+   */
+  suspend fun getBalance_latest(address: Address): UInt256 {
+    val body = mapOf(
+      Pair("jsonrpc", "2.0"),
+      Pair("method", "eth_getBalance"),
+      Pair("id", 1),
+      Pair("params", listOf(address.toHexString(), "latest"))
+    )
+    val deferred = CompletableDeferred<UInt256>()
+
+    @Suppress("DEPRECATION")
+    client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
+      response.bodyHandler {
+        val jsonResponse = it.toJson() as JsonObject
+        deferred.complete(UInt256.fromHexString(jsonResponse.getString("result")))
+      }.exceptionHandler {
+        deferred.completeExceptionally(it)
+      }
+    }.putHeader("Content-Type", "application/json")
+      .exceptionHandler { deferred.completeExceptionally(it) }
+      .endAwait(Buffer.buffer(mapper.writeValueAsBytes(body)))
+
+    return deferred.await()
+  }
+
+  /**
+   * Gets the number of transactions sent from an address.
+   * @param tx the transaction object to send
+   * @return the hash of the transaction, or an empty string if the hash is not available yet.
+   * @throws ClientRequestException is the request is rejected
+   */
+  suspend fun getTransactionCount_latest(address: Address): UInt256 {
+    val body = mapOf(
+      Pair("jsonrpc", "2.0"),
+      Pair("method", "eth_getTransactionCount"),
+      Pair("id", 1),
+      Pair("params", listOf(address.toHexString(), "latest"))
+    )
+    val deferred = CompletableDeferred<UInt256>()
+
+    @Suppress("DEPRECATION")
+    client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
+      response.bodyHandler {
+        val jsonResponse = it.toJson() as JsonObject
+        deferred.complete(UInt256.fromHexString(jsonResponse.getString("result")))
       }.exceptionHandler {
         deferred.completeExceptionally(it)
       }
diff --git a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
index 5832944..30ac56d 100644
--- a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
+++ b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
@@ -83,7 +83,7 @@ class JSONRPCClientTest {
         request.bodyHandler {
           sent.complete(it.toString(StandardCharsets.UTF_8))
         }
-        request.response().end("")
+        request.response().end("{\"result\":\"\"}")
       }
 
       val hash = it.sendRawTransaction(tx)
diff --git a/scuttlebutt-client-lib/src/main/java/org/apache/tuweni/scuttlebutt/lib/KeyFileLoader.java b/scuttlebutt-client-lib/src/main/java/org/apache/tuweni/scuttlebutt/lib/KeyFileLoader.java
index 1efcde0..287fb90 100644
--- a/scuttlebutt-client-lib/src/main/java/org/apache/tuweni/scuttlebutt/lib/KeyFileLoader.java
+++ b/scuttlebutt-client-lib/src/main/java/org/apache/tuweni/scuttlebutt/lib/KeyFileLoader.java
@@ -23,7 +23,6 @@ import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Map;
 import java.util.Scanner;
 
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -83,7 +82,7 @@ public class KeyFileLoader {
       }
       String secretJSON = String.join("", list);
 
-      HashMap<String, String> values = objectMapper.readValue(secretJSON, new TypeReference<Map<String, String>>() {});
+      HashMap<String, String> values = objectMapper.readValue(secretJSON, new TypeReference<>() {});
       String pubKey = values.get("public").replace(".ed25519", "");
       String privateKey = values.get("private").replace(".ed25519", "");
 
diff --git a/scuttlebutt-rpc/src/integrationTest/java/org/apache/tuweni/scuttlebutt/rpc/Utils.java b/scuttlebutt-rpc/src/integrationTest/java/org/apache/tuweni/scuttlebutt/rpc/Utils.java
index 6c2eb3f..7460224 100644
--- a/scuttlebutt-rpc/src/integrationTest/java/org/apache/tuweni/scuttlebutt/rpc/Utils.java
+++ b/scuttlebutt-rpc/src/integrationTest/java/org/apache/tuweni/scuttlebutt/rpc/Utils.java
@@ -22,7 +22,6 @@ import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Scanner;
 
@@ -58,7 +57,7 @@ class Utils {
 
     ObjectMapper mapper = new ObjectMapper();
 
-    HashMap<String, String> values = mapper.readValue(secretJSON, new TypeReference<Map<String, String>>() {});
+    Map<String, String> values = mapper.readValue(secretJSON, new TypeReference<>() {});
     String pubKey = values.get("public").replace(".ed25519", "");
     String privateKey = values.get("private").replace(".ed25519", "");
 
diff --git a/settings.gradle b/settings.gradle
index 025f8b4..18afa97 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,6 +29,7 @@ include 'eth-reference-tests'
 include 'eth-repository'
 include 'ethstats'
 include 'evm'
+include 'eth-faucet'
 include 'gossip'
 include 'hobbits'
 include 'hobbits-relayer'
diff --git a/units/src/main/java/org/apache/tuweni/units/ethereum/Wei.java b/units/src/main/java/org/apache/tuweni/units/ethereum/Wei.java
index e430561..44aab89 100644
--- a/units/src/main/java/org/apache/tuweni/units/ethereum/Wei.java
+++ b/units/src/main/java/org/apache/tuweni/units/ethereum/Wei.java
@@ -40,6 +40,17 @@ public final class Wei extends BaseUInt256Value<Wei> {
   }
 
   /**
+   * Return a {@link Wei} containing the specified value from an ETH
+   * 
+   * @param ethValue the value in eth
+   * @return A {@link Wei} containing the specified value.
+   * @throws IllegalArgumentException If the value is negative.
+   */
+  public static Wei fromEth(long ethValue) {
+    return valueOf(ethValue * (long) Math.pow(10, 18));
+  }
+
+  /**
    * Return a {@link Wei} containing the specified value.
    *
    * @param value The value to create a {@link Wei} for.
diff --git a/units/src/test/java/org/apache/tuweni/units/ethereum/WeiTest.java b/units/src/test/java/org/apache/tuweni/units/ethereum/WeiTest.java
index bfdb1ed..e056867 100644
--- a/units/src/test/java/org/apache/tuweni/units/ethereum/WeiTest.java
+++ b/units/src/test/java/org/apache/tuweni/units/ethereum/WeiTest.java
@@ -60,4 +60,9 @@ class WeiTest {
       Wei.valueOf(BigInteger.valueOf(-123L));
     });
   }
+
+  @Test
+  void testFromEth() {
+    assertEquals(Wei.valueOf((long) Math.pow(10, 18)), Wei.fromEth(1));
+  }
 }
diff --git a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
index a29f866..0c858c7 100644
--- a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
+++ b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
@@ -111,4 +111,8 @@ class Wallet(file: Path, password: String) {
     val pubKey = tx.extractPublicKey()
     return keyPair.publicKey() == pubKey
   }
+
+  fun address(): Address {
+    return Address.fromPublicKey(keyPair.publicKey())
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org