Skip to content

Commit f3c7f08

Browse files
author
Łukasz Gąsior
authored
Merge pull request #439 from input-output-hk/feature/faucet
Add faucet
2 parents b765c62 + b4fdcaa commit f3c7f08

File tree

12 files changed

+356
-16
lines changed

12 files changed

+356
-16
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ val dep = {
4646
"commons-io" % "commons-io" % "2.5",
4747
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
4848
"org.scala-sbt.ipcsocket" % "ipcsocket" % "1.0.0",
49+
"com.twitter" %% "util-collection" % "18.4.0",
4950

5051
// Pluggable Consensus: AtomixRaft
5152
"io.atomix" % "atomix" % "2.1.0-beta1",

src/main/resources/application.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,6 @@ mantis {
547547
port = 8888
548548
}
549549
}
550-
551550
}
552551

553552
akka {

src/main/scala/io/iohk/ethereum/App.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.iohk.ethereum
22

33
import io.iohk.ethereum.extvm.VmServerApp
4+
import io.iohk.ethereum.faucet.Faucet
45
import io.iohk.ethereum.mallet.main.Mallet
56
import io.iohk.ethereum.utils.{Config, Logger}
67

@@ -14,6 +15,7 @@ object App extends Logger {
1415
val downloadBootstrap = "bootstrap"
1516
val vmServer = "vm-server"
1617
val mallet = "mallet"
18+
val faucet = "faucet"
1719

1820
args.headOption match {
1921
case None => Mantis.main(args)
@@ -22,9 +24,10 @@ object App extends Logger {
2224
case Some(`downloadBootstrap`) => BootstrapDownload.main(args.tail :+ Config.Db.LevelDb.path)
2325
case Some(`vmServer`) => VmServerApp.main(args.tail)
2426
case Some(`mallet`) => Mallet.main(args.tail)
27+
case Some(`faucet`) => Faucet.main(args.tail)
2528
case Some(unknown) =>
2629
log.error(s"Unrecognised launcher option, " +
27-
s"first parameter must be $launchKeytool, $downloadBootstrap, $launchMantis or $vmServer")
30+
s"first parameter must be $launchKeytool, $downloadBootstrap, $launchMantis, $mallet, $faucet or $vmServer")
2831
}
2932

3033

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.iohk.ethereum.faucet
2+
3+
import java.security.SecureRandom
4+
5+
import akka.actor.ActorSystem
6+
import akka.http.scaladsl.Http
7+
import akka.stream.ActorMaterializer
8+
import com.typesafe.config.ConfigFactory
9+
import io.iohk.ethereum.keystore.KeyStoreImpl
10+
import io.iohk.ethereum.mallet.service.RpcClient
11+
import io.iohk.ethereum.utils.Logger
12+
13+
import scala.concurrent.ExecutionContext.Implicits.global
14+
import scala.util.{Failure, Success}
15+
16+
object Faucet extends Logger {
17+
18+
def main(args: Array[String]): Unit = {
19+
val config = FaucetConfig(ConfigFactory.load())
20+
21+
implicit val system = ActorSystem("Faucet-system")
22+
implicit val materializer = ActorMaterializer()
23+
24+
val keyStore = new KeyStoreImpl(config.keyStoreDir, new SecureRandom())
25+
val rpcClient = new RpcClient(config.rpcAddress)
26+
val api = new FaucetApi(rpcClient, keyStore, config)
27+
28+
val bindingResultF = Http().bindAndHandle(api.route, config.listenInterface, config.listenPort)
29+
30+
bindingResultF onComplete {
31+
case Success(serverBinding) => log.info(s"Faucet HTTP server listening on ${serverBinding.localAddress}")
32+
case Failure(ex) => log.error("Cannot start faucet HTTP server", ex)
33+
}
34+
}
35+
36+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.iohk.ethereum.faucet
2+
3+
import java.time.Clock
4+
5+
import akka.http.scaladsl.model.{RemoteAddress, StatusCodes}
6+
import akka.http.scaladsl.server.Route
7+
import akka.http.scaladsl.server.Directives._
8+
import akka.util.ByteString
9+
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
10+
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
11+
import com.twitter.util.LruMap
12+
import io.iohk.ethereum.domain.{Address, Transaction}
13+
import io.iohk.ethereum.keystore.KeyStore
14+
import io.iohk.ethereum.mallet.service.RpcClient
15+
import io.iohk.ethereum.utils.Logger
16+
import io.iohk.ethereum.rlp
17+
import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions.SignedTransactionEnc
18+
import org.spongycastle.util.encoders.Hex
19+
20+
class FaucetApi(
21+
rpcClient: RpcClient,
22+
keyStore: KeyStore,
23+
config: FaucetConfig,
24+
clock: Clock = Clock.systemUTC())
25+
extends Logger {
26+
27+
private val latestRequestTimestamps = new LruMap[RemoteAddress, Long](config.latestTimestampCacheSize)
28+
29+
private val wallet = keyStore.unlockAccount(config.walletAddress, config.walletPassword) match {
30+
case Right(w) => w
31+
case Left(err) => throw new RuntimeException(s"Cannot unlock wallet for use in faucet (${config.walletAddress}), because of $err")
32+
}
33+
34+
private val corsSettings = CorsSettings.defaultSettings.copy(
35+
allowGenericHttpRequests = true,
36+
allowedOrigins = config.corsAllowedOrigins)
37+
38+
val route: Route = cors(corsSettings) {
39+
(path("faucet") & pathEndOrSingleSlash & post & parameter('address)) { targetAddress =>
40+
extractClientIP { clientAddr =>
41+
handleRequest(clientAddr, targetAddress)
42+
}
43+
}
44+
}
45+
46+
private def handleRequest(clientAddr: RemoteAddress, targetAddress: String) = {
47+
val timeMillis = clock.instant().toEpochMilli
48+
val latestRequestTimestamp = latestRequestTimestamps.getOrElse(clientAddr, 0L)
49+
if (latestRequestTimestamp + config.minRequestInterval.toMillis < timeMillis) {
50+
latestRequestTimestamps.put(clientAddr, timeMillis)
51+
52+
val res = for {
53+
nonce <- rpcClient.getNonce(wallet.address)
54+
txId <- rpcClient.sendTransaction(prepareTx(Address(targetAddress), nonce))
55+
} yield txId
56+
57+
res match {
58+
case Right(txId) =>
59+
log.info(s"Sending ${config.txValue} ETH to $targetAddress in tx: $txId. Requested by $clientAddr")
60+
complete(StatusCodes.OK, s"0x${Hex.toHexString(txId.toArray[Byte])}")
61+
62+
case Left(err) =>
63+
log.error(s"An error occurred while using faucet: $err")
64+
complete(StatusCodes.InternalServerError)
65+
}
66+
} else complete(StatusCodes.TooManyRequests)
67+
}
68+
69+
private def prepareTx(targetAddress: Address, nonce: BigInt): ByteString = {
70+
val transaction = Transaction(
71+
nonce,
72+
config.txGasPrice,
73+
config.txGasLimit,
74+
Some(targetAddress),
75+
config.txValue,
76+
ByteString())
77+
78+
val stx = wallet.signTx(transaction, None)
79+
ByteString(rlp.encode(stx.toRLPEncodable))
80+
}
81+
82+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.iohk.ethereum.faucet
2+
3+
import akka.http.scaladsl.model.headers.HttpOriginRange
4+
import com.typesafe.config.{Config => TypesafeConfig}
5+
import io.iohk.ethereum.domain.Address
6+
import io.iohk.ethereum.utils.ConfigUtils
7+
8+
import scala.concurrent.duration.{FiniteDuration, _}
9+
10+
case class FaucetConfig(
11+
walletAddress: Address,
12+
walletPassword: String,
13+
txGasPrice: BigInt,
14+
txGasLimit: BigInt,
15+
txValue: BigInt,
16+
corsAllowedOrigins: HttpOriginRange,
17+
rpcAddress: String,
18+
keyStoreDir: String,
19+
listenInterface: String,
20+
listenPort: Int,
21+
minRequestInterval: FiniteDuration,
22+
latestTimestampCacheSize: Int)
23+
24+
object FaucetConfig {
25+
def apply(typesafeConfig: TypesafeConfig): FaucetConfig = {
26+
val faucetConfig = typesafeConfig.getConfig("faucet")
27+
28+
val corsAllowedOrigins = ConfigUtils.parseCorsAllowedOrigins(faucetConfig, "cors-allowed-origins")
29+
30+
FaucetConfig(
31+
walletAddress = Address(faucetConfig.getString("wallet-address")),
32+
walletPassword = faucetConfig.getString("wallet-password"),
33+
txGasPrice = faucetConfig.getLong("tx-gas-price"),
34+
txGasLimit = faucetConfig.getLong("tx-gas-limit"),
35+
txValue = faucetConfig.getLong("tx-value"),
36+
corsAllowedOrigins = corsAllowedOrigins,
37+
rpcAddress = faucetConfig.getString("rpc-address"),
38+
keyStoreDir = faucetConfig.getString("keystore-dir"),
39+
listenInterface = faucetConfig.getString("listen-interface"),
40+
listenPort = faucetConfig.getInt("listen-port"),
41+
minRequestInterval = faucetConfig.getDuration("min-request-interval").toMillis.millis,
42+
latestTimestampCacheSize = faucetConfig.getInt("latest-timestamp-cache-size"))
43+
}
44+
}

src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import java.security.SecureRandom
44

55
import akka.actor.ActorSystem
66
import akka.http.scaladsl.model.StatusCodes
7-
import akka.http.scaladsl.model.headers.{HttpOrigin, HttpOriginRange}
7+
import akka.http.scaladsl.model.headers.HttpOriginRange
88
import akka.http.scaladsl.server.Directives._
99
import akka.http.scaladsl.server.{MalformedRequestContentRejection, RejectionHandler, Route}
1010
import ch.megard.akka.http.cors.javadsl.CorsRejection
1111
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
1212
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
1313
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
1414
import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcErrors, JsonRpcRequest, JsonRpcResponse}
15-
import io.iohk.ethereum.utils.Logger
15+
import io.iohk.ethereum.utils.{ConfigUtils, Logger}
1616
import org.json4s.JsonAST.JInt
1717
import org.json4s.{DefaultFormats, native}
1818

@@ -89,29 +89,18 @@ object JsonRpcHttpServer extends Logger {
8989
}
9090

9191
object JsonRpcHttpServerConfig {
92-
import scala.collection.JavaConverters._
9392
import com.typesafe.config.{Config => TypesafeConfig}
9493

9594
def apply(mantisConfig: TypesafeConfig): JsonRpcHttpServerConfig = {
9695
val rpcHttpConfig = mantisConfig.getConfig("network.rpc.http")
9796

98-
def parseMultipleOrigins(origins: Seq[String]): HttpOriginRange = HttpOriginRange(origins.map(HttpOrigin(_)): _*)
99-
100-
def parseSingleOrigin(origin: String): HttpOriginRange = origin match {
101-
case "*" => HttpOriginRange.*
102-
case s => HttpOriginRange.Default(HttpOrigin(s) :: Nil)
103-
}
104-
10597
new JsonRpcHttpServerConfig {
10698
override val mode: String = rpcHttpConfig.getString("mode")
10799
override val enabled: Boolean = rpcHttpConfig.getBoolean("enabled")
108100
override val interface: String = rpcHttpConfig.getString("interface")
109101
override val port: Int = rpcHttpConfig.getInt("port")
110102

111-
override val corsAllowedOrigins: HttpOriginRange =
112-
(Try(parseMultipleOrigins(rpcHttpConfig.getStringList("cors-allowed-origins").asScala)) recoverWith {
113-
case _ => Try(parseSingleOrigin(rpcHttpConfig.getString("cors-allowed-origins")))
114-
}).get
103+
override val corsAllowedOrigins = ConfigUtils.parseCorsAllowedOrigins(rpcHttpConfig, "cors-allowed-origins")
115104

116105
override val certificateKeyStorePath: Option[String] = Try(rpcHttpConfig.getString("certificate-keystore-path")).toOption
117106
override val certificateKeyStoreType: Option[String] = Try(rpcHttpConfig.getString("certificate-keystore-type")).toOption
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.iohk.ethereum.utils
2+
3+
import akka.http.scaladsl.model.headers.{HttpOrigin, HttpOriginRange}
4+
import com.typesafe.config.{Config => TypesafeConfig}
5+
6+
import scala.collection.JavaConverters._
7+
import scala.util.Try
8+
9+
object ConfigUtils {
10+
11+
def parseCorsAllowedOrigins(config: TypesafeConfig, key: String): HttpOriginRange = {
12+
(Try(parseMultipleOrigins(config.getStringList(key).asScala)) recoverWith {
13+
case _ => Try(parseSingleOrigin(config.getString(key)))
14+
}).get
15+
}
16+
17+
def parseMultipleOrigins(origins: Seq[String]): HttpOriginRange = HttpOriginRange(origins.map(HttpOrigin(_)): _*)
18+
19+
def parseSingleOrigin(origin: String): HttpOriginRange = origin match {
20+
case "*" => HttpOriginRange.*
21+
case s => HttpOriginRange.Default(HttpOrigin(s) :: Nil)
22+
}
23+
24+
}

0 commit comments

Comments
 (0)