add EthereumRPC
This commit is contained in:
parent
a8a6b6c054
commit
200ef38c18
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue