2015-12-13 18:13:24 +09:00
#!/usr/bin/env python
#
# Electrum - Lightweight Bitcoin Client
# Copyright (C) 2015 Thomas Voegtlin
#
2016-02-23 11:36:42 +01:00
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
2015-12-13 18:13:24 +09:00
#
2016-02-23 11:36:42 +01:00
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
2015-12-13 18:13:24 +09:00
#
2016-02-23 11:36:42 +01:00
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2015-12-13 18:13:24 +09:00
2015-11-28 00:22:06 +01:00
from functools import partial
2018-05-08 20:04:36 +02:00
import threading
2019-02-11 20:21:24 +01:00
import sys
import os
2020-12-08 10:33:43 +01:00
from typing import TYPE_CHECKING
2015-11-28 00:22:06 +01:00
2019-02-11 20:21:24 +01:00
from PyQt5 . QtGui import QPixmap
from PyQt5 . QtCore import QObject , pyqtSignal
from PyQt5 . QtWidgets import ( QTextEdit , QVBoxLayout , QLabel , QGridLayout , QHBoxLayout ,
QRadioButton , QCheckBox , QLineEdit )
2015-11-26 14:15:54 +01:00
2019-02-11 20:21:24 +01:00
from electrum . gui . qt . util import ( read_QIcon , WindowModalDialog , WaitingDialog , OkButton ,
CancelButton , Buttons , icon_path , WWLabel , CloseButton )
2018-07-11 17:38:47 +02:00
from electrum . gui . qt . qrcodewidget import QRCodeWidget
from electrum . gui . qt . amountedit import AmountEdit
from electrum . gui . qt . main_window import StatusBarButton
2019-02-04 16:51:19 +01:00
from electrum . gui . qt . installwizard import InstallWizard
2015-11-26 14:15:54 +01:00
from electrum . i18n import _
2018-07-11 17:38:47 +02:00
from electrum . plugin import hook
2019-04-26 18:52:26 +02:00
from electrum . util import is_valid_email
from electrum . logging import Logger
2020-12-09 18:31:08 +01:00
from electrum . base_wizard import GoBack , UserCancelled
2019-04-26 18:52:26 +02:00
2017-02-16 10:54:24 +01:00
from . trustedcoin import TrustedCoinPlugin , server
2015-12-13 18:13:24 +09:00
2020-12-08 10:33:43 +01:00
if TYPE_CHECKING :
from electrum . gui . qt . main_window import ElectrumWindow
from electrum . wallet import Abstract_Wallet
2015-11-23 19:38:48 +01:00
2017-10-18 16:11:30 +02:00
class TOS ( QTextEdit ) :
tos_signal = pyqtSignal ( )
2017-12-04 12:02:06 +01:00
error_signal = pyqtSignal ( object )
2017-09-23 05:54:38 +02:00
2019-04-26 18:52:26 +02:00
class HandlerTwoFactor ( QObject , Logger ) :
2018-05-08 20:04:36 +02:00
def __init__ ( self , plugin , window ) :
2019-04-26 18:52:26 +02:00
QObject . __init__ ( self )
2018-05-08 20:04:36 +02:00
self . plugin = plugin
self . window = window
2019-04-26 18:52:26 +02:00
Logger . __init__ ( self )
2018-05-08 20:04:36 +02:00
2018-05-18 18:07:52 +02:00
def prompt_user_for_otp ( self , wallet , tx , on_success , on_failure ) :
if not isinstance ( wallet , self . plugin . wallet_class ) :
return
if wallet . can_sign_without_server ( ) :
return
2020-02-12 16:41:58 +01:00
if not wallet . keystores [ ' x3/ ' ] . can_sign ( tx , ignore_watching_only = True ) :
2019-04-26 18:52:26 +02:00
self . logger . info ( " twofactor: xpub3 not needed " )
2018-05-18 18:07:52 +02:00
return
window = self . window . top_level_window ( )
auth_code = self . plugin . auth_dialog ( window )
2019-03-25 23:36:52 +01:00
WaitingDialog ( parent = window ,
message = _ ( ' Waiting for TrustedCoin server to sign transaction... ' ) ,
task = lambda : wallet . on_otp ( tx , auth_code ) ,
on_success = lambda * args : on_success ( tx ) ,
on_error = on_failure )
2018-05-08 20:04:36 +02:00
2015-11-23 19:38:48 +01:00
class Plugin ( TrustedCoinPlugin ) :
2017-09-23 05:54:38 +02:00
def __init__ ( self , parent , config , name ) :
super ( ) . __init__ ( parent , config , name )
2015-11-26 14:15:54 +01:00
@hook
2020-12-08 10:33:43 +01:00
def load_wallet ( self , wallet : ' Abstract_Wallet ' , window : ' ElectrumWindow ' ) :
2016-01-03 19:16:37 +01:00
if not isinstance ( wallet , self . wallet_class ) :
return
2018-05-08 20:04:36 +02:00
wallet . handler_2fa = HandlerTwoFactor ( self , window )
2016-01-03 19:16:37 +01:00
if wallet . can_sign_without_server ( ) :
msg = ' ' . join ( [
2017-09-24 01:42:24 +02:00
_ ( ' This wallet was restored from seed, and it contains two master private keys. ' ) ,
2016-01-03 19:16:37 +01:00
_ ( ' Therefore, two-factor authentication is disabled. ' )
] )
action = lambda : window . show_message ( msg )
2022-10-16 15:10:46 +00:00
icon = read_QIcon ( " trustedcoin-status-disabled.png " )
2016-01-03 19:16:37 +01:00
else :
action = partial ( self . settings_dialog , window )
2022-10-16 15:10:46 +00:00
icon = read_QIcon ( " trustedcoin-status.png " )
2023-03-14 17:28:33 +01:00
sb = window . statusBar ( )
button = StatusBarButton ( icon , _ ( " TrustedCoin " ) , action , sb . height ( ) )
sb . addPermanentWidget ( button )
2017-07-06 16:03:21 +02:00
self . start_request_thread ( window . wallet )
2015-11-26 14:15:54 +01:00
2015-11-23 19:38:48 +01:00
def auth_dialog ( self , window ) :
2015-12-23 12:20:19 +09:00
d = WindowModalDialog ( window , _ ( " Authorization " ) )
2015-11-23 19:38:48 +01:00
vbox = QVBoxLayout ( d )
pw = AmountEdit ( None , is_int = True )
msg = _ ( ' Please enter your Google Authenticator code ' )
vbox . addWidget ( QLabel ( msg ) )
grid = QGridLayout ( )
grid . setSpacing ( 8 )
grid . addWidget ( QLabel ( _ ( ' Code ' ) ) , 1 , 0 )
grid . addWidget ( pw , 1 , 1 )
vbox . addLayout ( grid )
2017-11-13 11:47:25 +01:00
msg = _ ( ' If you have lost your second factor, you need to restore your wallet from seed in order to request a new code. ' )
label = QLabel ( msg )
label . setWordWrap ( 1 )
vbox . addWidget ( label )
2015-11-23 19:38:48 +01:00
vbox . addLayout ( Buttons ( CancelButton ( d ) , OkButton ( d ) ) )
if not d . exec_ ( ) :
return
return pw . get_amount ( )
2018-05-18 18:07:52 +02:00
def prompt_user_for_otp ( self , wallet , tx , on_success , on_failure ) :
wallet . handler_2fa . prompt_user_for_otp ( wallet , tx , on_success , on_failure )
2015-11-23 19:38:48 +01:00
2019-03-16 20:05:10 +01:00
def waiting_dialog_for_billing_info ( self , window , * , on_finished = None ) :
def task ( ) :
return self . request_billing_info ( window . wallet , suppress_connection_error = False )
def on_error ( exc_info ) :
e = exc_info [ 1 ]
window . show_error ( " {header} \n {exc} \n \n {tor} "
. format ( header = _ ( ' Error getting TrustedCoin account info. ' ) ,
2019-07-17 20:12:52 +02:00
exc = repr ( e ) ,
2019-03-16 20:05:10 +01:00
tor = _ ( ' If you keep experiencing network problems, try using a Tor proxy. ' ) ) )
return WaitingDialog ( parent = window ,
message = _ ( ' Requesting account info from TrustedCoin server... ' ) ,
task = task ,
on_success = on_finished ,
on_error = on_error )
2015-12-23 22:21:29 +09:00
2015-11-23 19:38:48 +01:00
@hook
def abort_send ( self , window ) :
wallet = window . wallet
2016-01-03 19:16:37 +01:00
if not isinstance ( wallet , self . wallet_class ) :
return
2017-07-06 16:03:21 +02:00
if wallet . can_sign_without_server ( ) :
return
if wallet . billing_info is None :
2019-03-16 20:05:10 +01:00
self . waiting_dialog_for_billing_info ( window )
2017-07-06 16:03:21 +02:00
return True
2015-11-23 19:38:48 +01:00
return False
def settings_dialog ( self , window ) :
2019-03-16 20:05:10 +01:00
self . waiting_dialog_for_billing_info ( window ,
on_finished = partial ( self . show_settings_dialog , window ) )
2015-11-23 19:38:48 +01:00
def show_settings_dialog ( self , window , success ) :
if not success :
window . show_message ( _ ( ' Server not reachable. ' ) )
return
wallet = window . wallet
2015-12-23 12:20:19 +09:00
d = WindowModalDialog ( window , _ ( " TrustedCoin Information " ) )
2015-11-23 19:38:48 +01:00
d . setMinimumSize ( 500 , 200 )
vbox = QVBoxLayout ( d )
hbox = QHBoxLayout ( )
logo = QLabel ( )
2019-02-01 19:01:21 +01:00
logo . setPixmap ( QPixmap ( icon_path ( " trustedcoin-status.png " ) ) )
2015-11-23 19:38:48 +01:00
msg = _ ( ' This wallet is protected by TrustedCoin \' s two-factor authentication. ' ) + ' <br/> ' \
+ _ ( " For more information, visit " ) + " <a href= \" https://api.trustedcoin.com/#/electrum-help \" >https://api.trustedcoin.com/#/electrum-help</a> "
label = QLabel ( msg )
label . setOpenExternalLinks ( 1 )
hbox . addStretch ( 10 )
hbox . addWidget ( logo )
hbox . addStretch ( 10 )
hbox . addWidget ( label )
hbox . addStretch ( 10 )
vbox . addLayout ( hbox )
vbox . addStretch ( 10 )
2018-04-15 20:45:30 +03:00
msg = _ ( ' TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions. ' ) + ' <br/> '
2015-11-23 19:38:48 +01:00
label = QLabel ( msg )
label . setWordWrap ( 1 )
vbox . addWidget ( label )
vbox . addStretch ( 10 )
grid = QGridLayout ( )
vbox . addLayout ( grid )
price_per_tx = wallet . price_per_tx
2019-09-10 16:24:21 +02:00
n_prepay = wallet . num_prepay ( )
2017-05-12 15:58:00 +02:00
i = 0
2015-11-23 19:38:48 +01:00
for k , v in sorted ( price_per_tx . items ( ) ) :
if k == 1 :
continue
2017-05-12 15:58:00 +02:00
grid . addWidget ( QLabel ( " Pay every %d transactions: " % k ) , i , 0 )
grid . addWidget ( QLabel ( window . format_amount ( v / k ) + ' ' + window . base_unit ( ) + " /tx " ) , i , 1 )
b = QRadioButton ( )
b . setChecked ( k == n_prepay )
2023-05-24 17:41:44 +00:00
def on_click ( b , k ) :
self . config . PLUGIN_TRUSTEDCOIN_NUM_PREPAY = k
b . clicked . connect ( partial ( on_click , k = k ) )
2015-11-23 19:38:48 +01:00
grid . addWidget ( b , i , 2 )
i + = 1
n = wallet . billing_info . get ( ' tx_remaining ' , 0 )
2018-02-04 07:26:55 +01:00
grid . addWidget ( QLabel ( _ ( " Your wallet has {} prepaid transactions. " ) . format ( n ) ) , i , 0 )
2015-11-23 19:38:48 +01:00
vbox . addLayout ( Buttons ( CloseButton ( d ) ) )
d . exec_ ( )
2019-02-04 16:51:19 +01:00
def go_online_dialog ( self , wizard : InstallWizard ) :
2018-05-18 18:07:52 +02:00
msg = [
2019-02-23 15:59:01 +01:00
_ ( " Your wallet file is: {} . " ) . format ( os . path . abspath ( wizard . path ) ) ,
2018-05-18 18:07:52 +02:00
_ ( " You need to be online in order to complete the creation of "
" your wallet. If you generated your seed on an offline "
' computer, click on " {} " to close this window, move your '
" wallet file to an online computer, and reopen it with "
" Electrum. " ) . format ( _ ( ' Cancel ' ) ) ,
_ ( ' If you are online, click on " {} " to continue. ' ) . format ( _ ( ' Next ' ) )
]
msg = ' \n \n ' . join ( msg )
2019-02-04 16:51:19 +01:00
wizard . reset_stack ( )
2019-05-14 15:32:57 +02:00
try :
wizard . confirm_dialog ( title = ' ' , message = msg , run_next = lambda x : wizard . run ( ' accept_terms_of_use ' ) )
2020-12-09 18:31:08 +01:00
except ( GoBack , UserCancelled ) :
2019-05-14 15:32:57 +02:00
# user clicked 'Cancel' and decided to move wallet file manually
2020-02-05 15:13:37 +01:00
storage , db = wizard . create_storage ( wizard . path )
2019-05-14 15:32:57 +02:00
raise
2018-05-18 18:07:52 +02:00
2015-11-23 19:38:48 +01:00
def accept_terms_of_use ( self , window ) :
vbox = QVBoxLayout ( )
vbox . addWidget ( QLabel ( _ ( " Terms of Service " ) ) )
2017-10-18 16:11:30 +02:00
tos_e = TOS ( )
2015-11-23 19:38:48 +01:00
tos_e . setReadOnly ( True )
vbox . addWidget ( tos_e )
2017-12-04 12:02:06 +01:00
tos_received = False
2015-11-23 19:38:48 +01:00
vbox . addWidget ( QLabel ( _ ( " Please enter your e-mail address " ) ) )
email_e = QLineEdit ( )
vbox . addWidget ( email_e )
2016-01-13 22:25:40 +09:00
next_button = window . next_button
prior_button_text = next_button . text ( )
next_button . setText ( _ ( ' Accept ' ) )
2015-11-23 19:38:48 +01:00
def request_TOS ( ) :
2017-12-04 12:02:06 +01:00
try :
tos = server . get_terms_of_service ( )
except Exception as e :
2019-04-26 18:52:26 +02:00
self . logger . exception ( ' Could not retrieve Terms of Service ' )
2017-12-04 12:02:06 +01:00
tos_e . error_signal . emit ( _ ( ' Could not retrieve Terms of Service: ' )
2019-07-17 20:12:52 +02:00
+ ' \n ' + repr ( e ) )
2017-12-04 12:02:06 +01:00
return
2015-11-23 19:38:48 +01:00
self . TOS = tos
2017-10-18 16:11:30 +02:00
tos_e . tos_signal . emit ( )
2015-11-23 19:38:48 +01:00
def on_result ( ) :
tos_e . setText ( self . TOS )
2017-12-04 12:02:06 +01:00
nonlocal tos_received
tos_received = True
set_enabled ( )
def on_error ( msg ) :
window . show_error ( str ( msg ) )
window . terminate ( )
2015-11-23 19:38:48 +01:00
2016-01-13 22:25:40 +09:00
def set_enabled ( ) :
2018-05-18 18:07:52 +02:00
next_button . setEnabled ( tos_received and is_valid_email ( email_e . text ( ) ) )
2016-01-13 22:25:40 +09:00
2017-10-18 16:11:30 +02:00
tos_e . tos_signal . connect ( on_result )
2017-12-04 12:02:06 +01:00
tos_e . error_signal . connect ( on_error )
2019-02-11 20:21:24 +01:00
t = threading . Thread ( target = request_TOS )
2022-05-23 17:52:39 +02:00
t . daemon = True
2015-11-23 19:38:48 +01:00
t . start ( )
2016-01-13 22:25:40 +09:00
email_e . textChanged . connect ( set_enabled )
2015-11-23 19:38:48 +01:00
email_e . setFocus ( True )
2017-03-10 14:16:46 +01:00
window . exec_layout ( vbox , next_enabled = False )
2016-01-13 22:25:40 +09:00
next_button . setText ( prior_button_text )
2018-05-18 18:07:52 +02:00
email = str ( email_e . text ( ) )
self . create_remote_key ( email , window )
2015-11-23 19:38:48 +01:00
2018-05-18 18:07:52 +02:00
def request_otp_dialog ( self , window , short_id , otp_secret , xpub3 ) :
2015-11-23 19:38:48 +01:00
vbox = QVBoxLayout ( )
if otp_secret is not None :
uri = " otpauth://totp/ %s ?secret= %s " % ( ' trustedcoin.com ' , otp_secret )
2016-01-15 11:24:19 +01:00
l = QLabel ( " Please scan the following QR code in Google Authenticator. You may as well use the following key: %s " % otp_secret )
l . setWordWrap ( True )
vbox . addWidget ( l )
2015-11-23 19:38:48 +01:00
qrw = QRCodeWidget ( uri )
vbox . addWidget ( qrw , 1 )
msg = _ ( ' Then, enter your Google Authenticator code: ' )
else :
2016-09-28 06:30:00 +02:00
label = QLabel (
2018-04-15 20:45:30 +03:00
" This wallet is already registered with TrustedCoin. "
2016-09-28 06:30:00 +02:00
" To finalize wallet creation, please enter your Google Authenticator Code. "
2016-10-01 11:45:43 +02:00
)
2015-11-23 19:38:48 +01:00
label . setWordWrap ( 1 )
vbox . addWidget ( label )
msg = _ ( ' Google Authenticator code: ' )
hbox = QHBoxLayout ( )
2016-01-13 22:25:40 +09:00
hbox . addWidget ( WWLabel ( msg ) )
2015-11-23 19:38:48 +01:00
pw = AmountEdit ( None , is_int = True )
pw . setFocus ( True )
2016-01-13 22:25:40 +09:00
pw . setMaximumWidth ( 50 )
2015-11-23 19:38:48 +01:00
hbox . addWidget ( pw )
vbox . addLayout ( hbox )
2016-10-01 11:45:43 +02:00
cb_lost = QCheckBox ( _ ( " I have lost my Google Authenticator account " ) )
cb_lost . setToolTip ( _ ( " Check this box to request a new secret. You will need to retype your seed. " ) )
vbox . addWidget ( cb_lost )
cb_lost . setVisible ( otp_secret is None )
2016-01-13 22:25:40 +09:00
def set_enabled ( ) :
2016-10-01 11:45:43 +02:00
b = True if cb_lost . isChecked ( ) else len ( pw . text ( ) ) == 6
window . next_button . setEnabled ( b )
2016-01-13 22:25:40 +09:00
pw . textChanged . connect ( set_enabled )
2016-10-01 11:45:43 +02:00
cb_lost . toggled . connect ( set_enabled )
2018-05-18 18:07:52 +02:00
window . exec_layout ( vbox , next_enabled = False , raise_on_cancel = False )
self . check_otp ( window , short_id , otp_secret , xpub3 , pw . get_amount ( ) , cb_lost . isChecked ( ) )