add EthereumRPC

This commit is contained in:
Michele Balistreri 2019-11-08 18:17:53 +03:00
parent a8a6b6c054
commit 200ef38c18
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
4 changed files with 122 additions and 72 deletions

View File

@ -3,10 +3,14 @@ package im.status.keycard.connect
import android.annotation.SuppressLint
import android.app.Activity
import android.nfc.NfcAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import im.status.keycard.android.NFCCardManager
import im.status.keycard.connect.card.*
import im.status.keycard.connect.data.PINCache
import im.status.keycard.connect.data.PairingManager
import im.status.keycard.connect.data.RPC_ENDPOINT
import im.status.keycard.connect.net.EthereumRPC
import im.status.keycard.connect.net.WalletConnect
@SuppressLint("StaticFieldLeak")
@ -30,8 +34,24 @@ object Registry {
private set
lateinit var walletConnect: WalletConnect
private set
lateinit var ethereumRPC: EthereumRPC
private set
private fun moshiAddKotlin() {
val factories = Moshi::class.java.getDeclaredField("BUILT_IN_FACTORIES")
factories.isAccessible = true
@Suppress("UNCHECKED_CAST")
val value = factories.get(null) as java.util.ArrayList<Any>
value.add(0, KotlinJsonAdapterFactory())
}
fun init(activity: Activity, listener: ScriptListener) {
//TODO: remove this hack, it is needed now because KEthereum does not add the KotlinJsonAdapterFactory
moshiAddKotlin()
this.mainActivity = activity
pairingManager = PairingManager(activity)
@ -44,6 +64,8 @@ object Registry {
cardManager.setCardListener(scriptExecutor)
cardManager.start()
//TODO: endpoint should be configurable
ethereumRPC = EthereumRPC(RPC_ENDPOINT)
walletConnect = WalletConnect()
}
}

View File

@ -16,4 +16,6 @@ const val SIGN_TEXT_MESSAGE = "signMessage"
const val REQ_INTERACTIVE_SCRIPT = 0x01
const val REQ_WALLETCONNECT = 0x02
const val CACHE_VALIDITY = 15 * 60 * 1000
const val CACHE_VALIDITY = 15 * 60 * 1000
const val RPC_ENDPOINT = "http://mainnet.infura.io/v3/27efcb33f94e4bd0866d1aadf8e1a12d"

View File

@ -0,0 +1,35 @@
package im.status.keycard.connect.net
import org.kethereum.extensions.hexToBigInteger
import org.kethereum.rpc.HttpEthereumRPC
import org.kethereum.rpc.model.StringResultResponse
import java.io.IOException
import java.math.BigInteger
class EthereumRPC(endpointURL: String) {
private var endpoint = HttpEthereumRPC(endpointURL)
fun changeEndpoint(endpointURL: String) {
endpoint = HttpEthereumRPC(endpointURL)
}
private inline fun <T> valueOrThrow(res: StringResultResponse?, body: (String) -> T) : T {
if (res != null && res.error == null) {
return body(res.result)
} else {
throw IOException("communication error")
}
}
fun ethGetTransactionCount(address: String): BigInteger {
return valueOrThrow(endpoint.getTransactionCount(address)) { it.hexToBigInteger() }
}
fun ethGasPrice(): BigInteger {
return valueOrThrow(endpoint.gasPrice()) { it.hexToBigInteger() }
}
fun ethSendRawTransaction(rawTx: String): String {
return valueOrThrow(endpoint.sendRawTransaction(rawTx)) { it }
}
}

View File

@ -26,92 +26,83 @@ import org.walleth.khex.hexToByteArray
import org.walleth.khex.toHexString
import org.walleth.khex.toNoPrefixHexString
import java.io.File
import java.lang.Exception
class WalletConnect : ExportKeyCommand.Listener, SignCommand.Listener {
class WalletConnect : ExportKeyCommand.Listener, SignCommand.Listener, Session.Callback {
//TODO: Provide settings for these two
private val bip39Path = "m/44'/60'/0'/0"
private val chainID: Long = 1
private val scope = MainScope()
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val moshi = Moshi.Builder().build()
private val okHttpClient = OkHttpClient()
private val sessionStore = FileWCSessionStore(File(Registry.mainActivity.filesDir, "wcSessions.json").apply { createNewFile() }, moshi)
private var session: WCSession? = null
private var requestId: Long = 0
private var action: (data: Intent?) -> Unit = this::nop
private val sessionCB = object : Session.Callback {
override fun onStatus(status: Session.Status) {
when (status) {
is Session.Status.Error -> println("WalletConnect Error")
is Session.Status.Approved -> println("WalletConnect Approved")
is Session.Status.Connected -> println("WalletConnect Connected")
is Session.Status.Disconnected -> println("WalletConnect Disconnected")
is Session.Status.Closed -> session = null
}
}
override fun onMethodCall(call: Session.MethodCall) {
scope.launch {
when (call) {
is Session.MethodCall.SessionRequest -> Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(ExportKeyCommand(Registry.walletConnect, bip39Path)))
is Session.MethodCall.SignMessage -> signText(call.id, call.message)
is Session.MethodCall.SendTransaction -> signTransaction(call.id, toTransaction(call), false)
is Session.MethodCall.Custom -> onCustomCall(call)
}
}
}
// would be more elegant with a single inline generic function with reified type, but apparently in Kotlin 1.3.50 code generation fails if I make this function inline.
// TODO: check newer version of Kotlin
private fun runOnValidParam(call: Session.MethodCall.Custom, body: (String) -> Unit) {
val param = call.params?.firstOrNull()
if (param is String) {
body(param)
} else {
session?.rejectRequest(call.id, 1L, "Invalid params")
}
}
private fun runOnValidParam(call: Session.MethodCall.Custom, index: Int, body: (Map<*, *>) -> Unit) {
val param = call.params?.getOrNull(index)
if (param is Map<*, *>) {
body(param)
} else {
session?.rejectRequest(call.id, 1L, "Invalid params")
}
}
private fun onCustomCall(call: Session.MethodCall.Custom) {
when(call.method) {
"personal_sign" -> runOnValidParam(call) { signText(call.id, it) }
"eth_signTypedData" -> { runOnValidParam(call, 1) { @Suppress("UNCHECKED_CAST") signTypedData(call.id, it as Map<String, String>) } }
"eth_signTransaction" -> { runOnValidParam(call, 0) { signTransaction(call.id, toTransaction(toSendTransaction(call.id, it)), false)} }
"eth_sendRawTransaction" -> { runOnValidParam(call) { relayTX(call.id, it) } }
else -> session?.rejectRequest(call.id, 1L, "Not implemented")
}
}
private fun toSendTransaction(id: Long, data: Map<*, *>): Session.MethodCall.SendTransaction {
val from = data["from"] as? String ?: throw IllegalArgumentException("from key missing")
val to = data["to"] as? String ?: throw IllegalArgumentException("to key missing")
val nonce = data["nonce"] as? String ?: (data["nonce"] as? Double)?.toLong()?.toString()
val gasPrice = data["gasPrice"] as? String
val gasLimit = data["gasLimit"] as? String
val value = data["value"] as? String ?: throw IllegalArgumentException("value key missing")
val txData = data["data"] as? String ?: throw IllegalArgumentException("data key missing")
return Session.MethodCall.SendTransaction(id, from, to, nonce, gasPrice, gasLimit, value, txData)
}
private fun toTransaction(tx: Session.MethodCall.SendTransaction): Transaction {
return createEmptyTransaction()
override fun onStatus(status: Session.Status) {
when (status) {
is Session.Status.Error -> println("WalletConnect Error")
is Session.Status.Approved -> println("WalletConnect Approved")
is Session.Status.Connected -> println("WalletConnect Connected")
is Session.Status.Disconnected -> println("WalletConnect Disconnected")
is Session.Status.Closed -> session = null
}
}
override fun onMethodCall(call: Session.MethodCall) {
scope.launch {
when (call) {
is Session.MethodCall.SessionRequest -> Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(ExportKeyCommand(Registry.walletConnect, bip39Path)))
is Session.MethodCall.SignMessage -> signText(call.id, call.message)
is Session.MethodCall.SendTransaction -> signTransaction(call.id, toTransaction(call), false)
is Session.MethodCall.Custom -> onCustomCall(call)
}
}
}
private inline fun <reified T> runOnValidParam(call: Session.MethodCall.Custom, index: Int, body: (T) -> Unit) {
val param = call.params?.getOrNull(index)
if (param is T) {
try {
body(param)
} catch(e: Exception) {
session?.rejectRequest(call.id, 1L, "Internal error")
}
} else {
session?.rejectRequest(call.id, 1L, "Invalid params")
}
}
private fun onCustomCall(call: Session.MethodCall.Custom) {
when(call.method) {
"personal_sign" -> runOnValidParam<String>(call, 0) { signText(call.id, it) }
"eth_signTypedData" -> { runOnValidParam<Map<*, *>>(call, 1) { @Suppress("UNCHECKED_CAST") signTypedData(call.id, it as Map<String, String>) } }
"eth_signTransaction" -> { runOnValidParam<Map<*, *>>(call, 0) { signTransaction(call.id, toTransaction(toSendTransaction(call.id, it)), false)} }
"eth_sendRawTransaction" -> { runOnValidParam<String>(call, 0) { relayTX(call.id, it) } }
else -> session?.rejectRequest(call.id, 1L, "Not implemented")
}
}
private fun toSendTransaction(id: Long, data: Map<*, *>): Session.MethodCall.SendTransaction {
val from = data["from"] as? String ?: throw IllegalArgumentException("from key missing")
val to = data["to"] as? String ?: throw IllegalArgumentException("to key missing")
val nonce = data["nonce"] as? String ?: (data["nonce"] as? Double)?.toLong()?.toString()
val gasPrice = data["gasPrice"] as? String
val gasLimit = data["gasLimit"] as? String
val value = data["value"] as? String ?: throw IllegalArgumentException("value key missing")
val txData = data["data"] as? String ?: throw IllegalArgumentException("data key missing")
return Session.MethodCall.SendTransaction(id, from, to, nonce, gasPrice, gasLimit, value, txData)
}
private fun toTransaction(tx: Session.MethodCall.SendTransaction): Transaction {
return createEmptyTransaction()
}
private fun relayTX(id: Long, signedTx: String) {
requestId = id
session?.rejectRequest(id, 1L, "Not implemented yet")
session?.approveRequest(id, Registry.ethereumRPC.ethSendRawTransaction(signedTx))
}
private fun signText(id: Long, message: String) {
@ -166,7 +157,7 @@ class WalletConnect : ExportKeyCommand.Listener, SignCommand.Listener {
Session.PeerMeta(name = "Keycard Connect")
)
session?.addCallback(sessionCB)
session?.addCallback(Registry.walletConnect)
session?.init()
}
}