diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 73dc7f5..c01d068 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,8 +16,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> - + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> @@ -32,12 +31,19 @@ + android:label="@string/app_name" + android:launchMode="singleTask" + android:alwaysRetainTaskState="true"> - + + + + + + Unit = this::nop private var signAction: (RecoverableSignature) -> Unit = this::nop + var currentAccount: String? = null + private set(value) { + field = value + wcListener.onAccountChanged(value) + } 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 + when(status) { + Session.Status.Approved, Session.Status.Connected -> wcListener.onConnected() + is Session.Status.Error, Session.Status.Disconnected, Session.Status.Closed -> { wcListener.onDisconnected(); session = null; currentAccount = null } } } override fun onMethodCall(call: Session.MethodCall) { scope.launch(Dispatchers.IO) { when (call) { - is Session.MethodCall.SessionRequest -> Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(ExportKeyCommand(Registry.walletConnect, bip32Path))) + is Session.MethodCall.SessionRequest -> getAccountKeys() is Session.MethodCall.SignMessage -> signText(call.id, call.message) is Session.MethodCall.SendTransaction -> signTransaction(call.id, toTransaction(call), true) is Session.MethodCall.Custom -> onCustomCall(call) @@ -77,6 +77,10 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand } } + private fun getAccountKeys() { + Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(ExportKeyCommand(Registry.walletConnect, bip32Path))) + } + private inline fun runOnValidParam(call: Session.MethodCall.Custom, index: Int, body: (T) -> Unit) { val param = call.params?.getOrNull(index) @@ -94,7 +98,7 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand 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) } } + "eth_signTypedData" -> { runOnValidParam(call, 1) { signTypedData(call.id, it) } } "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") @@ -134,7 +138,7 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(SignCommand(Registry.walletConnect, hash))) } - signAction = { session?.approveRequest(requestId, "0x${it.r.toNoPrefixHexString()}${it.s.toNoPrefixHexString()}${encode((it.recId + 27).toByte())}") } + signAction = { session?.approveRequest(requestId, formatDataSignature(it)) } val intent = Intent(Registry.mainActivity, SignMessageActivity::class.java).apply { putExtra(SIGN_TEXT_MESSAGE, text) @@ -151,7 +155,7 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand Registry.scriptExecutor.runScript(scriptWithAuthentication().plus(SignCommand(Registry.walletConnect, hash))) } - signAction = { session?.approveRequest(requestId, "0x${it.r.toNoPrefixHexString()}${it.s.toNoPrefixHexString()}${encode((it.recId + 27).toByte())}") } + signAction = { session?.approveRequest(requestId, formatDataSignature(it)) } val intent = Intent(Registry.mainActivity, SignMessageActivity::class.java).apply { putExtra(SIGN_TEXT_MESSAGE, message) @@ -160,6 +164,8 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand Registry.mainActivity.startActivityForResult(intent, REQ_WALLETCONNECT) } + private fun formatDataSignature(sig: RecoverableSignature) : String = "0x${sig.r.toNoPrefixHexString()}${sig.s.toNoPrefixHexString()}${encode((sig.recId + 27).toByte())}" + private fun signTransaction(id: Long, tx: Transaction, send: Boolean) { requestId = id @@ -215,12 +221,12 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand uiAction = this::nop } - fun connect(uri: String) { + fun connect(uri: Session.FullyQualifiedConfig) { scope.launch(Dispatchers.IO) { session?.kill() session = WCSession( - fromWCUri(uri).toFullyQualifiedConfig(), + uri, MoshiPayloadAdapter(moshi), sessionStore, OkHttpTransport.Builder(okHttpClient, moshi), @@ -232,10 +238,35 @@ class WalletConnect(var bip32Path: String, var chainID: Long) : ExportKeyCommand } } + fun disconnect() { + session?.kill() + } + + fun updateChainAndDerivation(newBip32Path: String, newChainID: Long) { + if (newBip32Path == bip32Path) { + if (newChainID != chainID) { + this.chainID = newChainID + if (session != null && currentAccount != null) { + session?.update(listOf(currentAccount!!), this.chainID) + } + } + } else { + this.bip32Path = newBip32Path + this.chainID = newChainID + if (session != null && currentAccount != null) { + getAccountKeys() + } + } + } + override fun onResponse(keyPair: BIP32KeyPair) { scope.launch(Dispatchers.IO) { - val addr = keyPair.toEthereumAddress().toHexString() - session?.approve(listOf(addr), chainID) + currentAccount = keyPair.toEthereumAddress().toHexString() + if (session?.approvedAccounts() != null) { + session?.update(listOf(currentAccount!!), chainID) + } else { + session?.approve(listOf(currentAccount!!), chainID) + } } } diff --git a/app/src/main/java/im/status/keycard/connect/net/WalletConnectListener.kt b/app/src/main/java/im/status/keycard/connect/net/WalletConnectListener.kt new file mode 100644 index 0000000..e84d0bc --- /dev/null +++ b/app/src/main/java/im/status/keycard/connect/net/WalletConnectListener.kt @@ -0,0 +1,7 @@ +package im.status.keycard.connect.net + +interface WalletConnectListener { + fun onConnected() + fun onDisconnected() + fun onAccountChanged(account: String?) +} \ No newline at end of file diff --git a/app/src/main/java/im/status/keycard/connect/ui/InitActivity.kt b/app/src/main/java/im/status/keycard/connect/ui/InitActivity.kt index aec81d7..88f959f 100644 --- a/app/src/main/java/im/status/keycard/connect/ui/InitActivity.kt +++ b/app/src/main/java/im/status/keycard/connect/ui/InitActivity.kt @@ -48,6 +48,11 @@ class InitActivity : AppCompatActivity() { finish() } + override fun onBackPressed() { + setResult(Activity.RESULT_CANCELED) + finish() + } + private fun randomToken(length: Int): String { return Base64.encodeToString(Crypto.randomBytes(length), NO_PADDING or NO_WRAP) } diff --git a/app/src/main/java/im/status/keycard/connect/ui/LoadKeyActivity.kt b/app/src/main/java/im/status/keycard/connect/ui/LoadKeyActivity.kt index 0c0dd61..40c5c1a 100644 --- a/app/src/main/java/im/status/keycard/connect/ui/LoadKeyActivity.kt +++ b/app/src/main/java/im/status/keycard/connect/ui/LoadKeyActivity.kt @@ -43,4 +43,9 @@ class LoadKeyActivity : AppCompatActivity() { setResult(Activity.RESULT_CANCELED) finish() } + + override fun onBackPressed() { + setResult(Activity.RESULT_CANCELED) + finish() + } } diff --git a/app/src/main/java/im/status/keycard/connect/ui/MainActivity.kt b/app/src/main/java/im/status/keycard/connect/ui/MainActivity.kt index 91497f3..bb16d74 100644 --- a/app/src/main/java/im/status/keycard/connect/ui/MainActivity.kt +++ b/app/src/main/java/im/status/keycard/connect/ui/MainActivity.kt @@ -6,7 +6,7 @@ import android.nfc.NfcAdapter import android.os.Bundle import android.view.LayoutInflater import android.view.View -import android.widget.ViewSwitcher +import android.widget.* import androidx.appcompat.app.AppCompatActivity import com.google.zxing.client.android.Intents import com.google.zxing.integration.android.IntentIntegrator @@ -14,10 +14,14 @@ import im.status.keycard.connect.R import im.status.keycard.connect.Registry import im.status.keycard.connect.card.* import im.status.keycard.connect.data.* +import im.status.keycard.connect.net.WalletConnectListener +import org.walletconnect.Session.Config.Companion.fromWCUri import kotlin.reflect.KClass -class MainActivity : AppCompatActivity(), ScriptListener { +class MainActivity : AppCompatActivity(), ScriptListener, WalletConnectListener { private lateinit var viewSwitcher: ViewSwitcher + private lateinit var networkSpinner: Spinner + private lateinit var walletPath: EditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,8 +32,20 @@ class MainActivity : AppCompatActivity(), ScriptListener { inflater.inflate(R.layout.activity_nfc, viewSwitcher) setContentView(viewSwitcher) - Registry.init(this, this) + Registry.init(this, this, this) Registry.scriptExecutor.defaultScript = cardCheckupScript() + + networkSpinner = findViewById(R.id.networkSpinner) + walletPath = findViewById(R.id.walletPathText) + + ArrayAdapter.createFromResource(this, R.array.networks, android.R.layout.simple_spinner_item).also { + it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + networkSpinner.adapter = it + } + networkSpinner.setSelection(CHAIN_IDS.indexOf(Registry.settingsManager.chainID)) + walletPath.setText(Registry.settingsManager.bip32Path) + + handleIntent(intent) } override fun onResume() { @@ -42,6 +58,25 @@ class MainActivity : AppCompatActivity(), ScriptListener { Registry.nfcAdapter.disableReaderMode(this) } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + if (intent?.action == Intent.ACTION_VIEW) { + handleWCURI(intent.data?.toString()) + } + } + + override fun onBackPressed() { + if (viewSwitcher.displayedChild == 0) { + moveTaskToBack(false) + } else { + Registry.scriptExecutor.cancelScript() + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -66,6 +101,17 @@ class MainActivity : AppCompatActivity(), ScriptListener { } } + fun updateConnection(@Suppress("UNUSED_PARAMETER") view: View) { + val chainID = CHAIN_IDS[networkSpinner.selectedItemPosition] + Registry.settingsManager.chainID = chainID + Registry.ethereumRPC.changeEndpoint(Registry.settingsManager.rpcEndpoint) + + val bip32Path = walletPath.text.toString() + Registry.settingsManager.bip32Path = bip32Path + + Registry.walletConnect.updateChainAndDerivation(bip32Path, chainID) + } + fun cancelNFC(@Suppress("UNUSED_PARAMETER") view: View) { Registry.scriptExecutor.cancelScript() } @@ -77,6 +123,10 @@ class MainActivity : AppCompatActivity(), ScriptListener { integrator.initiateScan() } + fun disconnectWallet(@Suppress("UNUSED_PARAMETER") view: View) { + Registry.walletConnect.disconnect() + } + fun changePIN(@Suppress("UNUSED_PARAMETER") view: View) { startCommand(ChangePINActivity::class) } @@ -110,10 +160,6 @@ class MainActivity : AppCompatActivity(), ScriptListener { startCommand(ReinstallActivity::class) } - fun settings(@Suppress("UNUSED_PARAMETER") view: View) { - startCommand(SettingsActivity::class) - } - private fun loadKeyHandler(resultCode: Int, data: Intent?) { if (resultCode != Activity.RESULT_OK || data == null) return @@ -131,10 +177,31 @@ class MainActivity : AppCompatActivity(), ScriptListener { private fun qrCodeScanned(resultCode: Int, data: Intent?) { if (resultCode != Activity.RESULT_OK || data == null) return - val uri: String? = data.getStringExtra(Intents.Scan.RESULT) + handleWCURI(data.getStringExtra(Intents.Scan.RESULT)) - if (uri != null && uri.startsWith("wc:")) { - Registry.walletConnect.connect(uri) + } + + private fun handleWCURI(uri: String?) { + if (uri != null) { + try { + Registry.walletConnect.connect(fromWCUri(uri).toFullyQualifiedConfig()) + } catch (e: Exception) {} } } + + override fun onConnected() { + val button = findViewById