Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added multisig wallets to the Android application #2279

Merged
merged 64 commits into from Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
7e25aaa
Android: add new layout screens for the choice between standard and m…
Aldin-SXR Mar 22, 2021
67f9255
Android: reorganize code for creating a standard wallet.
Aldin-SXR Mar 22, 2021
5e1cd78
Android: Added initial UI for multisg wallet creation.
Aldin-SXR Apr 8, 2021
e286a8a
Android: Added the layout for displaying the master key.
Aldin-SXR Apr 9, 2021
73ec4a6
Android: Updated the keystore dialog text.
Aldin-SXR Apr 9, 2021
b784693
Android: Added the menu for cosigner keystores.
Aldin-SXR Apr 9, 2021
d68c1fd
Android: Added multisig wallet creation logic to console.py.
Aldin-SXR Apr 9, 2021
a626e64
Android: Implemented several new dialogs and reworked logic to suppor…
Aldin-SXR Apr 9, 2021
29da35d
Android: Added wallet type near the wallet name in the appbar.
Aldin-SXR Apr 9, 2021
1e73a50
Android: Added layouts and menu dropdown for wallet information (cont…
Aldin-SXR Apr 9, 2021
fd4d346
Android: Added logic to view wallet information.
Aldin-SXR Apr 9, 2021
9f82245
Android: improved the layout of the wallet information popup.
Aldin-SXR Apr 10, 2021
3c0da3e
Android: refactored to use the already existing copyToClipoard() meth…
Aldin-SXR Apr 10, 2021
c7186df
Android: Updated the cosigner and signature sliders in the cosigner d…
Aldin-SXR Apr 19, 2021
6b1875b
Android: Using Schnorr signatures for standard wallets; ECDSA for mul…
Aldin-SXR Apr 19, 2021
9ddb6ad
Android: on multisig wallets, the 'Send' dialog will correspond to th…
Aldin-SXR Apr 19, 2021
130877f
Android: Added Base43 encode/decode for working with Electron transac…
Aldin-SXR Apr 19, 2021
d0c4e26
Android: added the ability to load, sign and eventually broadcast hex…
Aldin-SXR Apr 19, 2021
308e1a0
Android: Update 'Load Transaction' to prevent signing the transaction…
Aldin-SXR Apr 20, 2021
3306947
Android: Using 'get_tx_info' to get the status of the transaction.
Aldin-SXR Apr 21, 2021
f4d5118
Android: Minor text updates.
Aldin-SXR Apr 21, 2021
ce56d60
Removed unused lines and layout elements.
Aldin-SXR May 24, 2021
fec98cd
Made 'sign_schnorr' into a Kwarg parameter
Aldin-SXR May 27, 2021
bf9bd17
Merge branch 'Electron-Cash:master' into master
Aldin-SXR May 27, 2021
947b9dc
Merge branch 'master' of github.com:Aldin-SXR/Electron-Cash
Aldin-SXR May 27, 2021
01ee099
Showing 'empty' and 'invalid' labels instead of having an invisible l…
Aldin-SXR May 27, 2021
ce5f256
Removed double calling of isNotBlank on hex transaction text field.
Aldin-SXR May 27, 2021
14d677c
Layout fixes in load.xml
Aldin-SXR May 27, 2021
3a61b24
Removed Base43 object and replaced it with a call to bitcoin.py's bas…
Aldin-SXR May 27, 2021
0083065
Layout and element ID updates.
Aldin-SXR May 27, 2021
77c2e27
Moved the copy button in wallet_information.xml, fixed layout order a…
Aldin-SXR May 27, 2021
b86c855
Changed 'Information' to 'Wallet information' and used proper capital…
Aldin-SXR May 28, 2021
998090d
Moved the copy button for 'Master public key' and fixed text case.
Aldin-SXR May 28, 2021
e7d15ac
Fixed vertical margins on newly added layouts; fixed the order of ele…
Aldin-SXR May 28, 2021
2394c66
Fixed capitalization on 'Add cosigner' dialog.
Aldin-SXR May 28, 2021
8d63735
Android: Removed extra (unnecessary) whitespaces and newlines.
Aldin-SXR May 29, 2021
f8aa097
Android: Reusing IDs in cosigner_type menu to save line space.
Aldin-SXR May 29, 2021
6b9af66
Android: Fixed unnecessary double call to showDialog() in NewWallet
Aldin-SXR May 29, 2021
952abff
Android: Fixed 'Add cosigner' dialog title to use a new '%d of %d' st…
Aldin-SXR May 29, 2021
8bca0fc
Android: Simplified closeDialogs() logic; using closeDialogs() for al…
Aldin-SXR May 29, 2021
8361a18
Android: Added additional clarification for create() with multisig pa…
Aldin-SXR May 29, 2021
88b2611
Android: Moved 'Import addresses or private keys' option to the 'wall…
Aldin-SXR May 29, 2021
42f2c86
Android: Fixed the 'no master public key' crash on imported wallets.
Aldin-SXR May 29, 2021
f2219fa
Android: Removed wallet type from app title.
Aldin-SXR May 29, 2021
0ecc3bf
Android: Moved closeDialog() calls to onPostExecute(); updated the wa…
Aldin-SXR Jun 2, 2021
653f9cf
Android: Removed the global keystores variable and replaced it with a…
Aldin-SXR Jun 5, 2021
57b7644
Android: Removed superfluous dialog arguments ('multisig' and 'i_sign…
Aldin-SXR Jun 5, 2021
e477103
Android: Moved common code up to NewWalletDialog2.
Aldin-SXR Jun 5, 2021
0c16494
Android: Moved up 'new seed/key' dialog title decision to NewWalletDi…
Aldin-SXR Jun 5, 2021
f08f34d
Android: Updated the cosigner/signature sliders to persist across scr…
Aldin-SXR Jun 5, 2021
e052880
Android: Moved QR decoding in ColdLoad inside the conditional block.
Aldin-SXR Jun 6, 2021
26aa8e0
Android: Checking if master public key is null in WalletInformation d…
Aldin-SXR Jun 6, 2021
5d2d6e3
Android: Updated the logic for choosing Schnorr signatures in Send an…
Aldin-SXR Jun 6, 2021
21fb70d
Android: Added a better check for deciding between 'Save' or 'Send' t…
Aldin-SXR Jun 6, 2021
d763755
Android: Updated status displaying for raw hex transactions.
Aldin-SXR Jun 6, 2021
bf71bbc
Android: Enabled broadcasting of fully signed hex transactions from a…
Aldin-SXR Jun 6, 2021
d93715a
Android: Moved UI updates when signing load hex transactions to onPos…
Aldin-SXR Jun 6, 2021
8a2c0d6
Merge branch 'Electron-Cash:master' into master
Aldin-SXR Jun 9, 2021
5610e65
Android: Moved signSchnorr() into a function; using canBroadcast() in…
Aldin-SXR Jun 9, 2021
0663281
Android: Showing transaction details in a dialog before signing a loa…
Aldin-SXR Jun 10, 2021
62feea1
Android: Disabled QR code scanner on loaded transactions; updated dia…
Aldin-SXR Jun 10, 2021
ef287d7
Android: Refactored the selection of a newly-created wallet into a me…
Aldin-SXR Jun 11, 2021
6a6c146
Android: Updated NewWallet.kt to not share arguments between dialogs;…
Aldin-SXR Jun 13, 2021
895c2d8
Android: Added the displaying of all master public keys in multisig w…
Aldin-SXR Jun 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
132 changes: 127 additions & 5 deletions android/app/src/main/java/org/electroncash/electroncash3/ColdLoad.kt
Expand Up @@ -2,8 +2,12 @@ package org.electroncash.electroncash3

import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.chaquo.python.Kwarg
import com.chaquo.python.PyException
import com.chaquo.python.PyObject
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.android.synthetic.main.load.*

Expand All @@ -17,12 +21,13 @@ val libTransaction by lazy { libMod("transaction") }
// Valid transaction quickly show up in transactions.

class ColdLoadDialog : AlertDialogFragment() {

override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.load_transaction)
.setView(R.layout.load)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.qr_code, null)
.setPositiveButton(R.string.send, null)
.setPositiveButton(R.string.OK, null)
}

override fun onShowDialog() {
Expand All @@ -42,23 +47,70 @@ class ColdLoadDialog : AlertDialogFragment() {
}

private fun updateUI() {
val currenttext = etTransaction.text
//checks if text is blank. further validations can be added here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = currenttext.isNotBlank()
val tx = libTransaction.callAttr("Transaction", etTransaction.text.toString())
updateStatusText(tx)

// Check hex transaction signing status
if (canSign(tx)) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.sign)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
} else if (canBroadcast(tx)) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.send)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
} else {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
}

// Receives the result of a QR scan.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)

if (result != null && result.contents != null) {
etTransaction.setText(result.contents)
// Try to decode the QR content as Base43; if that fails, treat it as is
val txHex: String = try {
baseDecode(result.contents, 43)
} catch (e: PyException) {
result.contents
}
etTransaction.setText(txHex)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

fun onOK() {
val tx = libTransaction.callAttr("Transaction", etTransaction.text.toString())

// If transaction can be broadcasted, broadcast it.
// Otherwise, prompt for signing. If the transaction hex is invalid,
// the OK button will be disabled, regardless.
try {
if (canBroadcast(tx)) {
broadcastSignedTransaction(tx)
} else {
signLoadedTransaction()
}
} catch (e: ToastException) {
e.show()
}
}

/**
* Sign a loaded transaction.
*/
private fun signLoadedTransaction() {
val dialog = SignPasswordDialog()
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
dialog.setArguments(Bundle().apply {
putString("tx", etTransaction.text.toString())
})
showDialog(this, dialog)
}

/**
* Broadcast a signed transaction.
*/
private fun broadcastSignedTransaction(tx: PyObject) {
try {
if (!daemonModel.isConnected()) {
throw ToastException(R.string.not_connected)
Expand All @@ -69,4 +121,74 @@ class ColdLoadDialog : AlertDialogFragment() {
dismiss()
} catch (e: ToastException) { e.show() }
}


/**
* Check if a loaded transaction is signed.
* Displays the signing status below the raw TX field.
* (signed, partially signed, empty or invalid)
*/
private fun updateStatusText(tx: PyObject) {
try {
if (etTransaction.text.isBlank()) {
idTxStatus.setText(R.string.empty)
} else {
// Check if the transaction can be processed by this wallet or not
val txInfo = daemonModel.wallet!!.callAttr("get_tx_info", tx)

if (txInfo["amount"] == null && !canBroadcast(tx)) {
idTxStatus.setText(R.string.transaction_unrelated)
} else {
idTxStatus.setText(txInfo["status"].toString())
}
}
} catch (e: PyException) {
idTxStatus.setText(R.string.invalid)
}
}
}

/* Check if the wallet can sign the transaction */
fun canSign(tx: PyObject): Boolean {
return try {
!tx.callAttr("is_complete").toBoolean() and
daemonModel.wallet!!.callAttr("can_sign", tx).toBoolean()
} catch (e: PyException) {
false
}
}

/* Check if the transaction is ready to be broadcasted */
fun canBroadcast(tx: PyObject): Boolean {
return try {
tx.callAttr("is_complete").toBoolean()
} catch (e: PyException) {
false
}
}

/**
* Sign a loaded transaction dialog.
*/
class SignPasswordDialog : PasswordDialog<PyObject>() {
val coldLoadDialog by lazy { targetFragment as ColdLoadDialog }

val tx by lazy { libTransaction.callAttr("Transaction", arguments!!.getString("tx"),
Kwarg("sign_schnorr", signSchnorr())) }
val wallet = daemonModel.wallet!!

override fun onPassword(password: String): PyObject {
wallet.callAttr("sign_transaction", tx, password)

return tx
}

override fun onPostExecute(result: PyObject) {
coldLoadDialog.etTransaction.setText(result.toString())

if (!canBroadcast(result)) {
coldLoadDialog.dismiss()
copyToClipboard(result.toString(), R.string.signed_transaction)
}
}
}
Expand Up @@ -32,8 +32,14 @@ class DaemonModel(val config: PyObject) {
val walletName: String?
get() {
val wallet = this.wallet
return if (wallet == null) null else wallet.callAttr("basename").toString()
return wallet?.callAttr("basename")?.toString()
}
val walletType: String?
get() {
return if (wallet == null) null else commands.callAttr("get", "wallet_type").toString()
}
val scriptType: String?
get() = wallet?.get("txin_type").toString()

lateinit var watchdog: Runnable

Expand Down
45 changes: 42 additions & 3 deletions android/app/src/main/java/org/electroncash/electroncash3/Main.kt
Expand Up @@ -27,10 +27,13 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.observe
import com.chaquo.python.Kwarg
import kotlinx.android.synthetic.main.main.*
import kotlinx.android.synthetic.main.show_master_key.walletMasterKey
import kotlinx.android.synthetic.main.wallet_export.*
import kotlinx.android.synthetic.main.wallet_information.*
import kotlinx.android.synthetic.main.wallet_open.*
import kotlinx.android.synthetic.main.wallet_rename.*
import java.io.File
import java.lang.NullPointerException
import kotlin.reflect.KClass


Expand Down Expand Up @@ -231,6 +234,7 @@ class MainActivity : AppCompatActivity(R.layout.main) {
}
R.id.menuChangePassword -> showDialog(this, PasswordChangeDialog())
R.id.menuShowSeed -> { showDialog(this, SeedPasswordDialog()) }
R.id.menuWalletInformation -> { showDialog(this, WalletInformationDialog()) }
R.id.menuExportSigned -> {
try {
showDialog(this, SendDialog().apply {
Expand Down Expand Up @@ -654,15 +658,50 @@ class SeedPasswordDialog : PasswordDialog<SeedResult>() {
}
}


class SeedDialog : AlertDialogFragment() {
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.Wallet_seed)
.setView(R.layout.wallet_new_2)
.setPositiveButton(android.R.string.ok, null)
.setView(R.layout.wallet_new_2)
.setPositiveButton(android.R.string.ok, null)
}

override fun onShowDialog() {
setupSeedDialog(this)
}
}

class WalletInformationDialog : AlertDialogFragment() {
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setView(R.layout.wallet_information)
.setPositiveButton(android.R.string.ok, null)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

fabCopyMasterKey2.setOnClickListener {
val textToCopy = walletMasterKey.text
copyToClipboard(textToCopy, R.string.Master_public)
}
}

override fun onShowDialog() {
super.onShowDialog()

// Imported wallets do not have a master public key.
val masterKey = daemonModel.commands.callAttr("getmpk")?.toString()
if (masterKey != null) {
walletMasterKey.setText(masterKey)
walletMasterKey.setFocusable(false)
} else {
tvMasterPublicKey.setVisibility(View.GONE)
walletMasterKey.setVisibility(View.GONE)
// Using View.INVISIBLE on the 'Copy' button to preserve layout.
(fabCopyMasterKey2 as View).setVisibility(View.INVISIBLE)
}

idWalletName.setText(daemonModel.walletName)
idWalletType.setText(daemonModel.walletType)
idScriptType.setText(daemonModel.scriptType)
}
}