Skip to content

Commit 9611c57

Browse files
authored
Merge pull request #10 from komputing/multi_chain
Initial MultiChain version
2 parents 0e580f0 + 0afe226 commit 9611c57

File tree

6 files changed

+154
-40
lines changed

6 files changed

+154
-40
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ dependencies {
4141
implementation "com.github.komputing.kethereum:ens:${KETHEREUM_VERSION}"
4242
implementation "com.github.komputing.kethereum:keystore:${KETHEREUM_VERSION}"
4343

44-
implementation "com.github.komputing:kaptcha:1.0"
44+
implementation "com.github.komputing:kaptcha:1.0"
45+
implementation "com.github.ethereum-lists:chains:1.0"
4546

4647
implementation "com.natpryce:konfig:1.6.10.0"
4748
implementation "com.github.walleth:khex:0.6"

src/main/kotlin/org/komputing/fauceth/Application.kt

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.ktor.http.content.*
1010
import io.ktor.request.receiveParameters
1111
import io.ktor.response.*
1212
import io.ktor.routing.*
13+
import kotlinx.coroutines.flow.channelFlow
1314
import kotlinx.html.*
1415
import org.kethereum.ETH_IN_WEI
1516
import org.kethereum.crypto.toAddress
@@ -18,19 +19,15 @@ import org.kethereum.ens.isPotentialENSDomain
1819
import org.kethereum.erc55.isValid
1920
import org.kethereum.model.*
2021
import org.komputing.fauceth.FaucethLogLevel.*
21-
import org.komputing.fauceth.util.AtomicNonce
2222
import org.komputing.fauceth.util.log
2323
import java.math.BigDecimal
24+
import java.math.BigInteger
2425

2526
fun main(args: Array<String>) = io.ktor.server.netty.EngineMain.main(args)
2627

2728
fun Application.module() {
2829

29-
val initialNonce = rpc.getTransactionCount(config.keyPair.toAddress())
30-
31-
log(INFO, "Got initial nonce: $initialNonce for address ${config.keyPair.toAddress()}")
32-
33-
val atomicNonce = AtomicNonce(initialNonce!!)
30+
if (config.chains.size != chains.size) fail("Could not find definitions for all chains")
3431

3532
routing {
3633
static("/static") {
@@ -39,7 +36,9 @@ fun Application.module() {
3936
}
4037
get("/") {
4138
val address = call.request.queryParameters[ADDRESS_KEY]
42-
39+
val requestedChain = call.request.queryParameters[CHAIN_KEY]?.toLongOrNull().let { chainId ->
40+
chains.firstOrNull { it.staticChainInfo.chainId == chainId }
41+
}
4342
call.respondHtml {
4443
head {
4544
title { +config.appTitle }
@@ -82,6 +81,25 @@ fun Application.module() {
8281
img(src = url, classes = "image")
8382
}
8483
}
84+
85+
if (chains.size > 1 && requestedChain == null) {
86+
select {
87+
name = "chain"
88+
chains.forEach { chain ->
89+
option {
90+
value = chain.staticChainInfo.chainId.toString()
91+
+chain.staticChainInfo.name
92+
}
93+
}
94+
}
95+
} else {
96+
hiddenInput {
97+
name = "chain"
98+
value = (requestedChain ?: chains.first()).staticChainInfo.chainId.toString()
99+
}
100+
}
101+
102+
br
85103
input(classes = "input") {
86104
name = ADDRESS_KEY
87105
value = address ?: ""
@@ -90,11 +108,11 @@ fun Application.module() {
90108
div(classes = "h-captcha center") {
91109
attributes["data-sitekey"] = config.hcaptchaSiteKey
92110
}
93-
}
94-
div(classes = "center") {
95-
button(classes = "button") {
96-
onClick = "submitForm()"
97-
+"Request funds"
111+
div(classes = "center") {
112+
button(classes = "button") {
113+
onClick = "submitForm()"
114+
+"Request funds"
115+
}
98116
}
99117
}
100118
}
@@ -111,17 +129,23 @@ fun Application.module() {
111129
}
112130
+config.keyPair.toAddress().toString()
113131
br
114-
b {
115-
+"Nonce: "
132+
chains.forEach {
133+
134+
p {
135+
+"Chain: ${it.staticChainInfo.name}"
136+
}
137+
b {
138+
+"Nonce: "
139+
}
140+
+it.nonce.get().toString()
116141
}
117-
+atomicNonce.get().toString()
118142
}
119-
120143
}
121144
}
122145
post("/request") {
123146
val receiveParameters = call.receiveParameters()
124147
log(VERBOSE, "Serving /request with parameters $receiveParameters")
148+
125149
val captchaResult: Boolean = receiveParameters["h-captcha-response"]?.let { captchaVerifier.verifyCaptcha(it) } ?: false
126150
var address = Address(receiveParameters[ADDRESS_KEY] ?: "")
127151
val ensName = receiveParameters[ADDRESS_KEY]?.let { name -> ENSName(name) }
@@ -146,15 +170,18 @@ fun Application.module() {
146170
call.respondText("""Swal.fire("Error", "Could not verify your humanity", "error");""")
147171
} else {
148172

149-
val txHash: String = sendTransaction(address, atomicNonce)
173+
val chain = chains.findLast { it.staticChainInfo.chainId == receiveParameters["chain"]?.toLong() }!!
174+
val txHash: String = sendTransaction(address, chain)
150175

151176
val amountString = BigDecimal(config.amount).divide(BigDecimal(ETH_IN_WEI))
152-
val msg = if (config.chainExplorer != null) {
153-
"send $amountString ETH (<a href='${config.chainExplorer}/tx/$txHash'>view here</a>)"
177+
val explorer = chain.staticChainInfo.explorers?.firstOrNull()?.url
178+
val msg = if (explorer != null) {
179+
"send $amountString ETH (<a href='$explorer/tx/$txHash'>view here</a>)"
154180
} else {
155181
"send $amountString ETH (transaction: $txHash)"
156182
}
157183
call.respondText("""Swal.fire("Transaction send", "$msg", "success");""")
184+
158185
}
159186
}
160187
}
Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,94 @@
11
package org.komputing.fauceth
22

3+
import com.github.michaelbull.retry.retry
4+
import com.squareup.moshi.JsonAdapter
5+
import com.squareup.moshi.Moshi
6+
import com.squareup.moshi.Types
7+
import okhttp3.OkHttpClient
8+
import okhttp3.Request
9+
import okio.buffer
10+
import okio.source
11+
import org.ethereum.lists.chains.model.Chain
12+
import org.kethereum.crypto.toAddress
313
import org.kethereum.ens.ENS
4-
import org.kethereum.rpc.BaseEthereumRPC
5-
import org.kethereum.rpc.ConsoleLoggingTransportWrapper
6-
import org.kethereum.rpc.HttpEthereumRPC
7-
import org.kethereum.rpc.HttpTransport
14+
import org.kethereum.rpc.*
815
import org.kethereum.rpc.min3.getMin3RPC
916
import org.komputing.fauceth.FaucethLogLevel.*
17+
import org.komputing.fauceth.util.AtomicNonce
18+
import org.komputing.fauceth.util.log
1019
import org.komputing.kaptcha.HCaptcha
1120
import java.io.File
21+
import java.io.FileOutputStream
22+
import java.math.BigInteger
23+
import kotlin.system.exitProcess
24+
1225

1326
const val ADDRESS_KEY = "address"
27+
const val CHAIN_KEY = "chain"
28+
1429
val keystoreFile = File("fauceth_keystore.json")
1530
val ens = ENS(getMin3RPC())
1631
val config = FaucethConfig()
1732
val captchaVerifier = HCaptcha(config.hcaptchaSecret)
1833

19-
val rpc = if (config.logging == VERBOSE) {
20-
BaseEthereumRPC(ConsoleLoggingTransportWrapper(HttpTransport(config.chainRPCURL)))
21-
} else {
22-
HttpEthereumRPC(config.chainRPCURL)
23-
}
34+
val okHttpClient = OkHttpClient.Builder().build()
35+
36+
private val chainsDefinitionFile = File("chains.json").also {
37+
if (!it.exists()) {
38+
it.createNewFile()
39+
40+
val request = Request.Builder().url("https://chainid.network/chains_pretty.json").build();
41+
val response = okHttpClient.newCall(request).execute();
42+
if (!response.isSuccessful) {
43+
fail("could not download chains.json")
44+
}
45+
FileOutputStream(it).use { fos ->
46+
val body = response.body
47+
if (body == null) {
48+
fail("could not download chains.json")
49+
} else {
50+
fos.write(body.bytes())
51+
}
52+
}
53+
}
54+
}
55+
56+
private val moshi = Moshi.Builder().build()
57+
private var listMyData = Types.newParameterizedType(MutableList::class.java, Chain::class.java)
58+
var chainsAdapter: JsonAdapter<List<Chain>> = moshi.adapter(listMyData)
59+
60+
val unfilteredChains = chainsAdapter.fromJson(chainsDefinitionFile.source().buffer()) ?: fail("Could not read chains.json")
61+
62+
class ChainWithRPCAndNonce(
63+
val staticChainInfo: Chain,
64+
val nonce: AtomicNonce,
65+
val rpc: EthereumRPC
66+
)
67+
68+
val chains = unfilteredChains.filter { config.chains.contains(BigInteger.valueOf(it.chainId)) }.map {
69+
val rpc = if (config.logging == VERBOSE) {
70+
BaseEthereumRPC(ConsoleLoggingTransportWrapper(HttpTransport(it.rpc.first())))
71+
} else {
72+
HttpEthereumRPC(it.rpc.first())
73+
}
74+
75+
var initialNonce: BigInteger? = null
76+
77+
while(initialNonce == null) {
78+
log(INFO, "Fetching initial nonce for chain ${it.name}")
79+
initialNonce= rpc.getTransactionCount(config.keyPair.toAddress())
80+
}
81+
82+
log(INFO, "Got initial nonce for chain ${it.name}: $initialNonce for address ${config.keyPair.toAddress()}")
83+
84+
val atomicNonce = AtomicNonce(initialNonce!!)
85+
86+
ChainWithRPCAndNonce(it, atomicNonce, rpc)
87+
}
88+
89+
internal fun fail(msg: String): Nothing {
90+
println(msg)
91+
exitProcess(1)
92+
}
93+
94+

src/main/kotlin/org/komputing/fauceth/FaucethConfig.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package org.komputing.fauceth
22

33
import com.natpryce.konfig.*
4+
import org.ethereum.lists.chains.model.Chain
45
import org.kethereum.ETH_IN_WEI
56
import org.kethereum.crypto.createEthereumKeyPair
67
import org.kethereum.crypto.toECKeyPair
78
import org.kethereum.model.ECKeyPair
89
import org.kethereum.model.PrivateKey
910
import java.io.File
11+
import java.io.IOException
1012
import java.lang.IllegalArgumentException
1113
import java.math.BigInteger
1214
import kotlin.system.exitProcess
@@ -34,21 +36,21 @@ class FaucethConfig {
3436
PrivateKey(keystoreFile.readText().toBigInteger()).toECKeyPair()
3537
}
3638

39+
val chains: List<BigInteger> = config.getOrNull(Key("app.chains", stringType))?.let { chainIdString ->
40+
chainIdString.split(",").map { BigInteger(it) }
41+
} ?: emptyList()
42+
3743
val hcaptchaSecret = config[Key("hcaptcha.secret", stringType)]
3844
val hcaptchaSiteKey = config[Key("hcaptcha.sitekey", stringType)]
3945

4046
val appTitle = config.getOrElse(Key("app.title", stringType), "FaucETH")
4147
val appHeroImage = config.getOrNull(Key("app.imageURL", stringType))
4248
val amount = BigInteger(config.getOrNull(Key("app.amount", stringType)) ?: "$ETH_IN_WEI")
4349

44-
val chainRPCURL = config[Key("chain.rpc", stringType)]
45-
val chainExplorer = config.getOrNull(Key("chain.explorer", stringType))
46-
val chainId = BigInteger(config[Key("chain.id", stringType)])
47-
4850
val logging = try {
4951
config.getOrNull(Key("app.logging", stringType))?.let {
5052
FaucethLogLevel.valueOf(it.uppercase())
51-
}?:FaucethLogLevel.INFO
53+
} ?: FaucethLogLevel.INFO
5254
} catch (e: IllegalArgumentException) {
5355
println("value for app.logging invalid - possible values: " + FaucethLogLevel.values().joinToString(","))
5456
exitProcess(1)

src/main/kotlin/org/komputing/fauceth/TransactionSender.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ import java.lang.IllegalStateException
1919
import java.math.BigDecimal
2020
import java.math.BigInteger
2121

22-
suspend fun sendTransaction(address: Address, atomicNonce: AtomicNonce): String {
22+
suspend fun sendTransaction(address: Address, txChain: ChainWithRPCAndNonce): String {
2323

2424
val tx = createEmptyTransaction().apply {
2525
to = address
2626
value = config.amount
27-
nonce = atomicNonce.getAndIncrement()
27+
nonce = txChain.nonce.getAndIncrement()
2828
gasLimit = DEFAULT_GAS_LIMIT
29-
chain = config.chainId
29+
chain = txChain.staticChainInfo.chainId.toBigInteger()
3030
}
3131

3232
val txHashList = mutableListOf<String>()
3333

3434
while (true) {
3535
val feeSuggestionResult = retry(decorrelatedJitterBackoff(base = 10L, max = 5000L)) {
36-
val feeSuggestionResults = suggestEIP1559Fees(rpc)
36+
val feeSuggestionResults = suggestEIP1559Fees(txChain.rpc)
3737
log(FaucethLogLevel.VERBOSE, "Got FeeSuggestionResults $feeSuggestionResults")
3838
(feeSuggestionResults.keys.minOrNull() ?: throw IllegalArgumentException("Could not get 1559 fees")).let {
3939
feeSuggestionResults[it]
@@ -57,7 +57,7 @@ suspend fun sendTransaction(address: Address, atomicNonce: AtomicNonce): String
5757

5858
try {
5959
val txHash: String = retry(limitAttempts(5) + decorrelatedJitterBackoff(base = 10L, max = 5000L)) {
60-
val res = rpc.sendRawTransaction(tx.encode(signature).toHexString())
60+
val res = txChain.rpc.sendRawTransaction(tx.encode(signature).toHexString())
6161

6262
if (res?.startsWith("0x") != true) {
6363
log(FaucethLogLevel.ERROR, "sendRawTransaction got no hash $res")
@@ -76,7 +76,7 @@ suspend fun sendTransaction(address: Address, atomicNonce: AtomicNonce): String
7676
repeat(20) { // after 20 attempts we will try with a new fee calculation
7777
txHashList.forEach { hash ->
7878
// we wait for *any* tx we signed in this context to confirm - there could be (edge)cases where a old tx confirms and so a replacement tx will not
79-
txBlockNumber = rpc.getTransactionByHash(hash)?.transaction?.blockNumber
79+
txBlockNumber = txChain.rpc.getTransactionByHash(hash)?.transaction?.blockNumber
8080
if (txBlockNumber != null) {
8181
return hash
8282
}

src/main/resources/files/css/main.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,17 @@
4747
display: right;
4848
justify-content: right;
4949
align-items: center;
50+
}
51+
52+
.options {
53+
border: 1px solid #e5e5e5;
54+
padding: 10px;
55+
}
56+
57+
select {
58+
border: 1px solid #e5e5e5;
59+
font-size: 14px;
60+
width: 380px;
61+
width: %;
62+
background: white;
5063
}

0 commit comments

Comments
 (0)