diff --git a/app/src/main/java/im/status/keycard/connect/Registry.kt b/app/src/main/java/im/status/keycard/connect/Registry.kt index 2db11ad..c06137b 100644 --- a/app/src/main/java/im/status/keycard/connect/Registry.kt +++ b/app/src/main/java/im/status/keycard/connect/Registry.kt @@ -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 + 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() } } \ No newline at end of file diff --git a/app/src/main/java/im/status/keycard/connect/data/Constants.kt b/app/src/main/java/im/status/keycard/connect/data/Constants.kt index b056cae..e77aea8 100644 --- a/app/src/main/java/im/status/keycard/connect/data/Constants.kt +++ b/app/src/main/java/im/status/keycard/connect/data/Constants.kt @@ -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 \ No newline at end of file +const val CACHE_VALIDITY = 15 * 60 * 1000 + +const val RPC_ENDPOINT = "http://mainnet.infura.io/v3/27efcb33f94e4bd0866d1aadf8e1a12d" \ No newline at end of file diff --git a/app/src/main/java/im/status/keycard/connect/net/EthereumRPC.kt b/app/src/main/java/im/status/keycard/connect/net/EthereumRPC.kt new file mode 100644 index 0000000..ebafb74 --- /dev/null +++ b/app/src/main/java/im/status/keycard/connect/net/EthereumRPC.kt @@ -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 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 } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/status/keycard/connect/net/WalletConnect.kt b/app/src/main/java/im/status/keycard/connect/net/WalletConnect.kt index 839e2e9..b92c503 100644 --- a/app/src/main/java/im/status/keycard/connect/net/WalletConnect.kt +++ b/app/src/main/java/im/status/keycard/connect/net/WalletConnect.kt @@ -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) } } - "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 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(call, 0) { signText(call.id, it) } + "eth_signTypedData" -> { runOnValidParam>(call, 1) { @Suppress("UNCHECKED_CAST") signTypedData(call.id, it as Map) } } + "eth_signTransaction" -> { runOnValidParam>(call, 0) { signTransaction(call.id, toTransaction(toSendTransaction(call.id, it)), false)} } + "eth_sendRawTransaction" -> { runOnValidParam(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() } }