kivy: separate base wizard class from gui
This commit is contained in:
parent
362e4be6f0
commit
9182392b55
|
@ -67,7 +67,6 @@ class EventsDialog(Factory.Popup):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
super(EventsDialog, self).__init__(**kwargs)
|
||||
self._on_release = kwargs.get('on_release')
|
||||
|
||||
def on_release(self, instance):
|
||||
pass
|
||||
|
@ -76,7 +75,6 @@ class EventsDialog(Factory.Popup):
|
|||
pass
|
||||
|
||||
def close(self):
|
||||
self._on_release = None
|
||||
self.dismiss()
|
||||
|
||||
|
||||
|
|
|
@ -1,623 +0,0 @@
|
|||
|
||||
from functools import partial
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ObjectProperty, StringProperty, OptionProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.button import Button
|
||||
from kivy.utils import platform
|
||||
|
||||
from electrum_gui.kivy.uix.dialogs import EventsDialog
|
||||
from electrum_gui.kivy.i18n import _
|
||||
|
||||
test_xpub = "xpub661MyMwAqRbcFpV2JqonBDKdgJiExpxiSAtvphtpviunv42FNVJNNRA3Zdy5kQXoK7NpwUC2QQPXVMLKLoHxaekNfemFs5zkfrNnk91dobZ"
|
||||
test_seed = "powder idea leader task pretty harsh resemble alert quit athlete clerk almost able"
|
||||
is_test = platform != 'android'
|
||||
|
||||
Builder.load_string('''
|
||||
#:import Window kivy.core.window.Window
|
||||
#:import _ electrum_gui.kivy.i18n._
|
||||
|
||||
|
||||
<WizardTextInput@TextInput>
|
||||
border: 4, 4, 4, 4
|
||||
font_size: '15sp'
|
||||
padding: '15dp', '15dp'
|
||||
background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
|
||||
foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
|
||||
hint_text_color: self.foreground_color
|
||||
background_active: 'atlas://gui/kivy/theming/light/create_act_text_active'
|
||||
background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active'
|
||||
size_hint_y: None
|
||||
height: '48sp'
|
||||
|
||||
<WizardButton@Button>:
|
||||
root: None
|
||||
size_hint: 1, None
|
||||
height: '48sp'
|
||||
on_press: if self.root: self.root.dispatch('on_press', self)
|
||||
on_release: if self.root: self.root.dispatch('on_release', self)
|
||||
|
||||
<BigLabel@Label>
|
||||
color: .854, .925, .984, 1
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
bold: True
|
||||
|
||||
<-WizardDialog>
|
||||
text_color: .854, .925, .984, 1
|
||||
value: ''
|
||||
#auto_dismiss: False
|
||||
size_hint: None, None
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0, 0, 0, .9
|
||||
Rectangle:
|
||||
size: Window.size
|
||||
Color:
|
||||
rgba: .239, .588, .882, 1
|
||||
Rectangle:
|
||||
size: Window.size
|
||||
|
||||
crcontent: crcontent
|
||||
# add electrum icon
|
||||
BoxLayout:
|
||||
orientation: 'vertical' if self.width < self.height else 'horizontal'
|
||||
padding:
|
||||
min(dp(27), self.width/32), min(dp(27), self.height/32),\
|
||||
min(dp(27), self.width/32), min(dp(27), self.height/32)
|
||||
spacing: '10dp'
|
||||
GridLayout:
|
||||
id: grid_logo
|
||||
cols: 1
|
||||
pos_hint: {'center_y': .5}
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: 'ELECTRUM'
|
||||
size_hint: 1, None
|
||||
height: self.texture_size[1] if self.opacity else 0
|
||||
font_size: '33sp'
|
||||
font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf'
|
||||
GridLayout:
|
||||
cols: 1
|
||||
id: crcontent
|
||||
spacing: '1dp'
|
||||
Widget:
|
||||
size_hint: 1, 0.3
|
||||
GridLayout:
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
WizardButton:
|
||||
id: back
|
||||
text: _('Back')
|
||||
root: root
|
||||
WizardButton:
|
||||
id: next
|
||||
text: _('Next')
|
||||
root: root
|
||||
disabled: root.value == ''
|
||||
|
||||
|
||||
<WizardMultisigDialog>
|
||||
value: 'next'
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: _("Choose the number of signatures needed to unlock funds in your wallet")
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
GridLayout:
|
||||
orientation: 'vertical'
|
||||
cols: 2
|
||||
spacing: '14dp'
|
||||
size_hint: 1, 1
|
||||
height: self.minimum_height
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: _('From %d cosigners')%n.value
|
||||
Slider:
|
||||
id: n
|
||||
range: 2, 5
|
||||
step: 1
|
||||
value: 2
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: _('Require %d signatures')%m.value
|
||||
Slider:
|
||||
id: m
|
||||
range: 1, n.value
|
||||
step: 1
|
||||
value: 2
|
||||
|
||||
|
||||
<WizardChoiceDialog>
|
||||
msg : ''
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.msg
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
GridLayout:
|
||||
row_default_height: '48dp'
|
||||
orientation: 'vertical'
|
||||
id: choices
|
||||
cols: 1
|
||||
spacing: '14dp'
|
||||
size_hint: 1, None
|
||||
|
||||
<MButton@Button>:
|
||||
size_hint: 1, None
|
||||
height: '33dp'
|
||||
on_release:
|
||||
self.parent.update_amount(self.text)
|
||||
|
||||
<WordButton@Button>:
|
||||
size_hint: None, None
|
||||
padding: '5dp', '5dp'
|
||||
text_size: None, self.height
|
||||
width: self.texture_size[0]
|
||||
height: '30dp'
|
||||
on_release:
|
||||
self.parent.new_word(self.text)
|
||||
|
||||
|
||||
<SeedButton@Button>:
|
||||
height: dp(100)
|
||||
border: 4, 4, 4, 4
|
||||
halign: 'justify'
|
||||
valign: 'top'
|
||||
font_size: '18dp'
|
||||
text_size: self.width - dp(24), self.height - dp(12)
|
||||
color: .1, .1, .1, 1
|
||||
background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top'
|
||||
background_down: self.background_normal
|
||||
size_hint_y: None
|
||||
|
||||
|
||||
<SeedLabel@Label>:
|
||||
font_size: '12sp'
|
||||
text_size: self.width, None
|
||||
size_hint: 1, None
|
||||
height: self.texture_size[1]
|
||||
halign: 'justify'
|
||||
valign: 'middle'
|
||||
border: 4, 4, 4, 4
|
||||
|
||||
|
||||
<RestoreSeedDialog>
|
||||
word: ''
|
||||
BigLabel:
|
||||
text: "ENTER YOUR SEED PHRASE"
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input_seed
|
||||
text: ''
|
||||
on_text: Clock.schedule_once(root.on_text)
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
BoxLayout:
|
||||
id: suggestions
|
||||
height: '35dp'
|
||||
size_hint: 1, None
|
||||
new_word: root.on_word
|
||||
BoxLayout:
|
||||
id: line1
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
MButton:
|
||||
text: 'Q'
|
||||
MButton:
|
||||
text: 'W'
|
||||
MButton:
|
||||
text: 'E'
|
||||
MButton:
|
||||
text: 'R'
|
||||
MButton:
|
||||
text: 'T'
|
||||
MButton:
|
||||
text: 'Y'
|
||||
MButton:
|
||||
text: 'U'
|
||||
MButton:
|
||||
text: 'I'
|
||||
MButton:
|
||||
text: 'O'
|
||||
MButton:
|
||||
text: 'P'
|
||||
BoxLayout:
|
||||
id: line2
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
Widget:
|
||||
size_hint: 0.5, None
|
||||
height: '33dp'
|
||||
MButton:
|
||||
text: 'A'
|
||||
MButton:
|
||||
text: 'S'
|
||||
MButton:
|
||||
text: 'D'
|
||||
MButton:
|
||||
text: 'F'
|
||||
MButton:
|
||||
text: 'G'
|
||||
MButton:
|
||||
text: 'H'
|
||||
MButton:
|
||||
text: 'J'
|
||||
MButton:
|
||||
text: 'K'
|
||||
MButton:
|
||||
text: 'L'
|
||||
Widget:
|
||||
size_hint: 0.5, None
|
||||
height: '33dp'
|
||||
BoxLayout:
|
||||
id: line3
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
Widget:
|
||||
size_hint: 1, None
|
||||
MButton:
|
||||
text: 'Z'
|
||||
MButton:
|
||||
text: 'X'
|
||||
MButton:
|
||||
text: 'C'
|
||||
MButton:
|
||||
text: 'V'
|
||||
MButton:
|
||||
text: 'B'
|
||||
MButton:
|
||||
text: 'N'
|
||||
MButton:
|
||||
text: 'M'
|
||||
MButton:
|
||||
text: ' '
|
||||
MButton:
|
||||
text: '<'
|
||||
|
||||
<AddXpubDialog>
|
||||
title: ''
|
||||
message: ''
|
||||
BigLabel:
|
||||
text: root.title
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input
|
||||
text: ''
|
||||
on_text: Clock.schedule_once(root.check_text)
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
GridLayout
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
IconButton:
|
||||
id: scan
|
||||
height: '48sp'
|
||||
on_release: root.scan_xpub()
|
||||
icon: 'atlas://gui/kivy/theming/light/camera'
|
||||
size_hint: 1, None
|
||||
WizardButton:
|
||||
text: _('Paste')
|
||||
on_release: root.do_paste()
|
||||
WizardButton:
|
||||
text: _('Clear')
|
||||
on_release: root.do_clear()
|
||||
|
||||
|
||||
<ShowXpubDialog>
|
||||
xpub: ''
|
||||
message: _('Here is your master public key. Share it with your cosigners.')
|
||||
BigLabel:
|
||||
text: "MASTER PUBLIC KEY"
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input
|
||||
text: root.xpub
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
GridLayout
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
WizardButton:
|
||||
text: _('QR code')
|
||||
on_release: root.do_qr()
|
||||
WizardButton:
|
||||
text: _('Copy')
|
||||
on_release: root.do_copy()
|
||||
WizardButton:
|
||||
text: _('Share')
|
||||
on_release: root.do_share()
|
||||
|
||||
|
||||
<ShowSeedDialog>
|
||||
spacing: '12dp'
|
||||
value: 'next'
|
||||
BigLabel:
|
||||
text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
|
||||
GridLayout:
|
||||
id: grid
|
||||
cols: 1
|
||||
pos_hint: {'center_y': .5}
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
SeedButton:
|
||||
text: root.seed_text
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
''')
|
||||
|
||||
|
||||
|
||||
class WizardDialog(EventsDialog):
|
||||
''' Abstract dialog to be used as the base for all Create Account Dialogs
|
||||
'''
|
||||
crcontent = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(WizardDialog, self).__init__(**kwargs)
|
||||
self.action = kwargs.get('action')
|
||||
_trigger_size_dialog = Clock.create_trigger(self._size_dialog)
|
||||
Window.bind(size=_trigger_size_dialog,
|
||||
rotation=_trigger_size_dialog)
|
||||
_trigger_size_dialog()
|
||||
|
||||
def _size_dialog(self, dt):
|
||||
app = App.get_running_app()
|
||||
if app.ui_mode[0] == 'p':
|
||||
self.size = Window.size
|
||||
else:
|
||||
#tablet
|
||||
if app.orientation[0] == 'p':
|
||||
#portrait
|
||||
self.size = Window.size[0]/1.67, Window.size[1]/1.4
|
||||
else:
|
||||
self.size = Window.size[0]/2.5, Window.size[1]
|
||||
|
||||
def add_widget(self, widget, index=0):
|
||||
if not self.crcontent:
|
||||
super(WizardDialog, self).add_widget(widget)
|
||||
else:
|
||||
self.crcontent.add_widget(widget, index=index)
|
||||
|
||||
def on_dismiss(self):
|
||||
app = App.get_running_app()
|
||||
if app.wallet is None and self._on_release is not None:
|
||||
app.stop()
|
||||
|
||||
|
||||
class WizardMultisigDialog(WizardDialog):
|
||||
pass
|
||||
|
||||
class WizardChoiceDialog(WizardDialog):
|
||||
''' Multiple choices dialog '''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(WizardChoiceDialog, self).__init__(**kwargs)
|
||||
self.msg = kwargs.get('msg', '')
|
||||
choices = kwargs.get('choices', [])
|
||||
layout = self.ids.choices
|
||||
layout.bind(minimum_height=layout.setter('height'))
|
||||
for text, action in choices:
|
||||
l = WizardButton(text=text)
|
||||
l.action = action
|
||||
l.height = '48dp'
|
||||
l.root = self
|
||||
layout.add_widget(l)
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
app = App.get_running_app()
|
||||
self._back = _back = partial(app.dispatch, 'on_back')
|
||||
|
||||
|
||||
class ShowSeedDialog(WizardDialog):
|
||||
|
||||
seed_text = StringProperty('')
|
||||
message = StringProperty('')
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
app = App.get_running_app()
|
||||
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
|
||||
|
||||
|
||||
class WordButton(Button):
|
||||
pass
|
||||
|
||||
class WizardButton(Button):
|
||||
pass
|
||||
|
||||
class RestoreSeedDialog(WizardDialog):
|
||||
|
||||
message = StringProperty('')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RestoreSeedDialog, self).__init__(**kwargs)
|
||||
self._test = kwargs['test']
|
||||
from electrum.mnemonic import Mnemonic
|
||||
from electrum.old_mnemonic import words as old_wordlist
|
||||
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
|
||||
self.ids.text_input_seed.text = test_seed if is_test else ''
|
||||
|
||||
def get_suggestions(self, prefix):
|
||||
for w in self.words:
|
||||
if w.startswith(prefix):
|
||||
yield w
|
||||
|
||||
def on_text(self, dt):
|
||||
self.ids.next.disabled = not bool(self._test(self.get_text()))
|
||||
|
||||
text = self.ids.text_input_seed.text
|
||||
if not text:
|
||||
last_word = ''
|
||||
elif text[-1] == ' ':
|
||||
last_word = ''
|
||||
else:
|
||||
last_word = text.split(' ')[-1]
|
||||
|
||||
enable_space = False
|
||||
self.ids.suggestions.clear_widgets()
|
||||
suggestions = [x for x in self.get_suggestions(last_word)]
|
||||
if suggestions and len(suggestions) < 10:
|
||||
for w in suggestions:
|
||||
if w == last_word:
|
||||
enable_space = True
|
||||
else:
|
||||
b = WordButton(text=w)
|
||||
self.ids.suggestions.add_widget(b)
|
||||
|
||||
i = len(last_word)
|
||||
p = set()
|
||||
for x in suggestions:
|
||||
if len(x)>i: p.add(x[i])
|
||||
|
||||
for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
|
||||
for c in line.children:
|
||||
if isinstance(c, Button):
|
||||
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
|
||||
c.disabled = (c.text.lower() not in p) and last_word
|
||||
elif c.text == ' ':
|
||||
c.disabled = not enable_space
|
||||
|
||||
def on_word(self, w):
|
||||
text = self.get_text()
|
||||
words = text.split(' ')
|
||||
words[-1] = w
|
||||
text = ' '.join(words)
|
||||
self.ids.text_input_seed.text = text + ' '
|
||||
self.ids.suggestions.clear_widgets()
|
||||
|
||||
def get_text(self):
|
||||
ti = self.ids.text_input_seed
|
||||
text = unicode(ti.text).strip()
|
||||
text = ' '.join(text.split())
|
||||
return text
|
||||
|
||||
def update_text(self, c):
|
||||
c = c.lower()
|
||||
text = self.ids.text_input_seed.text
|
||||
if c == '<':
|
||||
text = text[:-1]
|
||||
else:
|
||||
text += c
|
||||
self.ids.text_input_seed.text = text
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
tis = self.ids.text_input_seed
|
||||
tis.focus = True
|
||||
#tis._keyboard.bind(on_key_down=self.on_key_down)
|
||||
self._back = _back = partial(self.ids.back.dispatch,
|
||||
'on_release')
|
||||
app = App.get_running_app()
|
||||
|
||||
def on_key_down(self, keyboard, keycode, key, modifiers):
|
||||
if keycode[0] in (13, 271):
|
||||
self.on_enter()
|
||||
return True
|
||||
|
||||
def on_enter(self):
|
||||
#self._remove_keyboard()
|
||||
# press next
|
||||
next = self.ids.next
|
||||
if not next.disabled:
|
||||
next.dispatch('on_release')
|
||||
|
||||
def _remove_keyboard(self):
|
||||
tis = self.ids.text_input_seed
|
||||
if tis._keyboard:
|
||||
tis._keyboard.unbind(on_key_down=self.on_key_down)
|
||||
tis.focus = False
|
||||
|
||||
|
||||
class ShowXpubDialog(WizardDialog):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
WizardDialog.__init__(self, **kwargs)
|
||||
self.app = App.get_running_app()
|
||||
self.xpub = kwargs['xpub']
|
||||
self.ids.next.disabled = False
|
||||
|
||||
def do_copy(self):
|
||||
self.app._clipboard.copy(self.xpub)
|
||||
|
||||
def do_share(self):
|
||||
self.app.do_share(self.xpub, _("Master Public Key"))
|
||||
|
||||
def do_qr(self):
|
||||
from qr_dialog import QRDialog
|
||||
popup = QRDialog(_("Master Public Key"), self.xpub, True)
|
||||
popup.open()
|
||||
|
||||
|
||||
class AddXpubDialog(WizardDialog):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
WizardDialog.__init__(self, **kwargs)
|
||||
self.app = App.get_running_app()
|
||||
self._test = kwargs['test']
|
||||
self.title = kwargs['title']
|
||||
self.message = kwargs['message']
|
||||
|
||||
def check_text(self, dt):
|
||||
self.ids.next.disabled = not bool(self._test(self.get_text()))
|
||||
|
||||
def get_text(self):
|
||||
ti = self.ids.text_input
|
||||
return unicode(ti.text).strip()
|
||||
|
||||
def scan_xpub(self):
|
||||
def on_complete(text):
|
||||
self.ids.text_input.text = text
|
||||
self.app.scan_qr(on_complete)
|
||||
|
||||
def do_paste(self):
|
||||
self.ids.text_input.text = test_xpub if is_test else unicode(self.app._clipboard.paste())
|
||||
|
||||
def do_clear(self):
|
||||
self.ids.text_input.text = ''
|
|
@ -1,30 +1,675 @@
|
|||
import os
|
||||
from electrum.wallet import Wallet, Multisig_Wallet
|
||||
from electrum_gui.kivy.i18n import _
|
||||
|
||||
from functools import partial
|
||||
import threading
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ObjectProperty, StringProperty, OptionProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.button import Button
|
||||
from kivy.utils import platform
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.core.window import Window
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from functools import partial
|
||||
import weakref
|
||||
|
||||
from create_restore import WizardChoiceDialog, ShowSeedDialog, RestoreSeedDialog, AddXpubDialog, ShowXpubDialog, WizardMultisigDialog
|
||||
from password_dialog import PasswordDialog
|
||||
from electrum_gui.kivy.uix.dialogs import EventsDialog
|
||||
from electrum_gui.kivy.i18n import _
|
||||
|
||||
|
||||
# global Variables
|
||||
app = App.get_running_app()
|
||||
|
||||
from password_dialog import PasswordDialog
|
||||
|
||||
class InstallWizard(Widget):
|
||||
'''Installation Wizard. Responsible for instantiating the
|
||||
creation/restoration of wallets.
|
||||
from electrum.base_wizard import BaseWizard
|
||||
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
#:import Window kivy.core.window.Window
|
||||
#:import _ electrum_gui.kivy.i18n._
|
||||
|
||||
|
||||
<WizardTextInput@TextInput>
|
||||
border: 4, 4, 4, 4
|
||||
font_size: '15sp'
|
||||
padding: '15dp', '15dp'
|
||||
background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
|
||||
foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
|
||||
hint_text_color: self.foreground_color
|
||||
background_active: 'atlas://gui/kivy/theming/light/create_act_text_active'
|
||||
background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active'
|
||||
size_hint_y: None
|
||||
height: '48sp'
|
||||
|
||||
<WizardButton@Button>:
|
||||
root: None
|
||||
size_hint: 1, None
|
||||
height: '48sp'
|
||||
on_press: if self.root: self.root.dispatch('on_press', self)
|
||||
on_release: if self.root: self.root.dispatch('on_release', self)
|
||||
|
||||
<BigLabel@Label>
|
||||
color: .854, .925, .984, 1
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
bold: True
|
||||
|
||||
<-WizardDialog>
|
||||
text_color: .854, .925, .984, 1
|
||||
value: ''
|
||||
#auto_dismiss: False
|
||||
size_hint: None, None
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0, 0, 0, .9
|
||||
Rectangle:
|
||||
size: Window.size
|
||||
Color:
|
||||
rgba: .239, .588, .882, 1
|
||||
Rectangle:
|
||||
size: Window.size
|
||||
|
||||
crcontent: crcontent
|
||||
# add electrum icon
|
||||
BoxLayout:
|
||||
orientation: 'vertical' if self.width < self.height else 'horizontal'
|
||||
padding:
|
||||
min(dp(27), self.width/32), min(dp(27), self.height/32),\
|
||||
min(dp(27), self.width/32), min(dp(27), self.height/32)
|
||||
spacing: '10dp'
|
||||
GridLayout:
|
||||
id: grid_logo
|
||||
cols: 1
|
||||
pos_hint: {'center_y': .5}
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: 'ELECTRUM'
|
||||
size_hint: 1, None
|
||||
height: self.texture_size[1] if self.opacity else 0
|
||||
font_size: '33sp'
|
||||
font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf'
|
||||
GridLayout:
|
||||
cols: 1
|
||||
id: crcontent
|
||||
spacing: '1dp'
|
||||
Widget:
|
||||
size_hint: 1, 0.3
|
||||
GridLayout:
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
WizardButton:
|
||||
id: back
|
||||
text: _('Back')
|
||||
root: root
|
||||
WizardButton:
|
||||
id: next
|
||||
text: _('Next')
|
||||
root: root
|
||||
disabled: root.value == ''
|
||||
|
||||
|
||||
<WizardMultisigDialog>
|
||||
value: 'next'
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: _("Choose the number of signatures needed to unlock funds in your wallet")
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
GridLayout:
|
||||
orientation: 'vertical'
|
||||
cols: 2
|
||||
spacing: '14dp'
|
||||
size_hint: 1, 1
|
||||
height: self.minimum_height
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: _('From %d cosigners')%n.value
|
||||
Slider:
|
||||
id: n
|
||||
range: 2, 5
|
||||
step: 1
|
||||
value: 2
|
||||
Label:
|
||||
color: root.text_color
|
||||
text: _('Require %d signatures')%m.value
|
||||
Slider:
|
||||
id: m
|
||||
range: 1, n.value
|
||||
step: 1
|
||||
value: 2
|
||||
|
||||
|
||||
<WizardChoiceDialog>
|
||||
msg : ''
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.msg
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
GridLayout:
|
||||
row_default_height: '48dp'
|
||||
orientation: 'vertical'
|
||||
id: choices
|
||||
cols: 1
|
||||
spacing: '14dp'
|
||||
size_hint: 1, None
|
||||
|
||||
<MButton@Button>:
|
||||
size_hint: 1, None
|
||||
height: '33dp'
|
||||
on_release:
|
||||
self.parent.update_amount(self.text)
|
||||
|
||||
<WordButton@Button>:
|
||||
size_hint: None, None
|
||||
padding: '5dp', '5dp'
|
||||
text_size: None, self.height
|
||||
width: self.texture_size[0]
|
||||
height: '30dp'
|
||||
on_release:
|
||||
self.parent.new_word(self.text)
|
||||
|
||||
|
||||
<SeedButton@Button>:
|
||||
height: dp(100)
|
||||
border: 4, 4, 4, 4
|
||||
halign: 'justify'
|
||||
valign: 'top'
|
||||
font_size: '18dp'
|
||||
text_size: self.width - dp(24), self.height - dp(12)
|
||||
color: .1, .1, .1, 1
|
||||
background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top'
|
||||
background_down: self.background_normal
|
||||
size_hint_y: None
|
||||
|
||||
|
||||
<SeedLabel@Label>:
|
||||
font_size: '12sp'
|
||||
text_size: self.width, None
|
||||
size_hint: 1, None
|
||||
height: self.texture_size[1]
|
||||
halign: 'justify'
|
||||
valign: 'middle'
|
||||
border: 4, 4, 4, 4
|
||||
|
||||
|
||||
<RestoreSeedDialog>
|
||||
word: ''
|
||||
BigLabel:
|
||||
text: "ENTER YOUR SEED PHRASE"
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input_seed
|
||||
text: ''
|
||||
on_text: Clock.schedule_once(root.on_text)
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
BoxLayout:
|
||||
id: suggestions
|
||||
height: '35dp'
|
||||
size_hint: 1, None
|
||||
new_word: root.on_word
|
||||
BoxLayout:
|
||||
id: line1
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
MButton:
|
||||
text: 'Q'
|
||||
MButton:
|
||||
text: 'W'
|
||||
MButton:
|
||||
text: 'E'
|
||||
MButton:
|
||||
text: 'R'
|
||||
MButton:
|
||||
text: 'T'
|
||||
MButton:
|
||||
text: 'Y'
|
||||
MButton:
|
||||
text: 'U'
|
||||
MButton:
|
||||
text: 'I'
|
||||
MButton:
|
||||
text: 'O'
|
||||
MButton:
|
||||
text: 'P'
|
||||
BoxLayout:
|
||||
id: line2
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
Widget:
|
||||
size_hint: 0.5, None
|
||||
height: '33dp'
|
||||
MButton:
|
||||
text: 'A'
|
||||
MButton:
|
||||
text: 'S'
|
||||
MButton:
|
||||
text: 'D'
|
||||
MButton:
|
||||
text: 'F'
|
||||
MButton:
|
||||
text: 'G'
|
||||
MButton:
|
||||
text: 'H'
|
||||
MButton:
|
||||
text: 'J'
|
||||
MButton:
|
||||
text: 'K'
|
||||
MButton:
|
||||
text: 'L'
|
||||
Widget:
|
||||
size_hint: 0.5, None
|
||||
height: '33dp'
|
||||
BoxLayout:
|
||||
id: line3
|
||||
update_amount: root.update_text
|
||||
size_hint: 1, None
|
||||
height: '30dp'
|
||||
Widget:
|
||||
size_hint: 1, None
|
||||
MButton:
|
||||
text: 'Z'
|
||||
MButton:
|
||||
text: 'X'
|
||||
MButton:
|
||||
text: 'C'
|
||||
MButton:
|
||||
text: 'V'
|
||||
MButton:
|
||||
text: 'B'
|
||||
MButton:
|
||||
text: 'N'
|
||||
MButton:
|
||||
text: 'M'
|
||||
MButton:
|
||||
text: ' '
|
||||
MButton:
|
||||
text: '<'
|
||||
|
||||
<AddXpubDialog>
|
||||
title: ''
|
||||
message: ''
|
||||
BigLabel:
|
||||
text: root.title
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input
|
||||
text: ''
|
||||
on_text: Clock.schedule_once(root.check_text)
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
GridLayout
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
IconButton:
|
||||
id: scan
|
||||
height: '48sp'
|
||||
on_release: root.scan_xpub()
|
||||
icon: 'atlas://gui/kivy/theming/light/camera'
|
||||
size_hint: 1, None
|
||||
WizardButton:
|
||||
text: _('Paste')
|
||||
on_release: root.do_paste()
|
||||
WizardButton:
|
||||
text: _('Clear')
|
||||
on_release: root.do_clear()
|
||||
|
||||
|
||||
<ShowXpubDialog>
|
||||
xpub: ''
|
||||
message: _('Here is your master public key. Share it with your cosigners.')
|
||||
BigLabel:
|
||||
text: "MASTER PUBLIC KEY"
|
||||
GridLayout
|
||||
cols: 1
|
||||
padding: 0, '12dp'
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
SeedButton:
|
||||
id: text_input
|
||||
text: root.xpub
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
GridLayout
|
||||
rows: 1
|
||||
spacing: '12dp'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
WizardButton:
|
||||
text: _('QR code')
|
||||
on_release: root.do_qr()
|
||||
WizardButton:
|
||||
text: _('Copy')
|
||||
on_release: root.do_copy()
|
||||
WizardButton:
|
||||
text: _('Share')
|
||||
on_release: root.do_share()
|
||||
|
||||
|
||||
<ShowSeedDialog>
|
||||
spacing: '12dp'
|
||||
value: 'next'
|
||||
BigLabel:
|
||||
text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
|
||||
GridLayout:
|
||||
id: grid
|
||||
cols: 1
|
||||
pos_hint: {'center_y': .5}
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: '12dp'
|
||||
SeedButton:
|
||||
text: root.seed_text
|
||||
SeedLabel:
|
||||
text: root.message
|
||||
''')
|
||||
|
||||
|
||||
|
||||
class WizardDialog(EventsDialog):
|
||||
''' Abstract dialog to be used as the base for all Create Account Dialogs
|
||||
'''
|
||||
crcontent = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(WizardDialog, self).__init__(**kwargs)
|
||||
#self.action = kwargs.get('action')
|
||||
self.run_next = kwargs['run_next']
|
||||
self.run_prev = kwargs['run_prev']
|
||||
_trigger_size_dialog = Clock.create_trigger(self._size_dialog)
|
||||
Window.bind(size=_trigger_size_dialog,
|
||||
rotation=_trigger_size_dialog)
|
||||
_trigger_size_dialog()
|
||||
self._on_release = False
|
||||
|
||||
def _size_dialog(self, dt):
|
||||
app = App.get_running_app()
|
||||
if app.ui_mode[0] == 'p':
|
||||
self.size = Window.size
|
||||
else:
|
||||
#tablet
|
||||
if app.orientation[0] == 'p':
|
||||
#portrait
|
||||
self.size = Window.size[0]/1.67, Window.size[1]/1.4
|
||||
else:
|
||||
self.size = Window.size[0]/2.5, Window.size[1]
|
||||
|
||||
def add_widget(self, widget, index=0):
|
||||
if not self.crcontent:
|
||||
super(WizardDialog, self).add_widget(widget)
|
||||
else:
|
||||
self.crcontent.add_widget(widget, index=index)
|
||||
|
||||
def on_dismiss(self):
|
||||
app = App.get_running_app()
|
||||
if app.wallet is None and not self._on_release:
|
||||
app.stop()
|
||||
|
||||
def get_params(self, button):
|
||||
return ()
|
||||
|
||||
def on_release(self, button):
|
||||
self._on_release = True
|
||||
self.close()
|
||||
if not button:
|
||||
self.parent.dispatch('on_wizard_complete', None)
|
||||
return
|
||||
if button is self.ids.back:
|
||||
self.run_prev()
|
||||
return
|
||||
params = self.get_params(button)
|
||||
self.run_next(*params)
|
||||
|
||||
|
||||
class WizardMultisigDialog(WizardDialog):
|
||||
|
||||
def get_params(self, button):
|
||||
m = self.ids.m.value
|
||||
n = self.ids.n.value
|
||||
return m, n
|
||||
|
||||
class WizardChoiceDialog(WizardDialog):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(WizardChoiceDialog, self).__init__(**kwargs)
|
||||
self.msg = kwargs.get('msg', '')
|
||||
choices = kwargs.get('choices', [])
|
||||
layout = self.ids.choices
|
||||
layout.bind(minimum_height=layout.setter('height'))
|
||||
for text, action in choices:
|
||||
l = WizardButton(text=text)
|
||||
l.action = action
|
||||
l.height = '48dp'
|
||||
l.root = self
|
||||
layout.add_widget(l)
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
app = App.get_running_app()
|
||||
self._back = _back = partial(app.dispatch, 'on_back')
|
||||
|
||||
def get_params(self, button):
|
||||
return (button.action,)
|
||||
|
||||
class ShowSeedDialog(WizardDialog):
|
||||
|
||||
seed_text = StringProperty('')
|
||||
message = StringProperty('')
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
app = App.get_running_app()
|
||||
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
|
||||
|
||||
def get_params(self, b):
|
||||
return(self.seed_text,)
|
||||
|
||||
|
||||
class WordButton(Button):
|
||||
pass
|
||||
|
||||
class WizardButton(Button):
|
||||
pass
|
||||
|
||||
class RestoreSeedDialog(WizardDialog):
|
||||
|
||||
message = StringProperty('')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RestoreSeedDialog, self).__init__(**kwargs)
|
||||
self._test = kwargs['test']
|
||||
from electrum.mnemonic import Mnemonic
|
||||
from electrum.old_mnemonic import words as old_wordlist
|
||||
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
|
||||
self.ids.text_input_seed.text = ''
|
||||
|
||||
def get_suggestions(self, prefix):
|
||||
for w in self.words:
|
||||
if w.startswith(prefix):
|
||||
yield w
|
||||
|
||||
def on_text(self, dt):
|
||||
self.ids.next.disabled = not bool(self._test(self.get_text()))
|
||||
|
||||
text = self.ids.text_input_seed.text
|
||||
if not text:
|
||||
last_word = ''
|
||||
elif text[-1] == ' ':
|
||||
last_word = ''
|
||||
else:
|
||||
last_word = text.split(' ')[-1]
|
||||
|
||||
enable_space = False
|
||||
self.ids.suggestions.clear_widgets()
|
||||
suggestions = [x for x in self.get_suggestions(last_word)]
|
||||
if suggestions and len(suggestions) < 10:
|
||||
for w in suggestions:
|
||||
if w == last_word:
|
||||
enable_space = True
|
||||
else:
|
||||
b = WordButton(text=w)
|
||||
self.ids.suggestions.add_widget(b)
|
||||
|
||||
i = len(last_word)
|
||||
p = set()
|
||||
for x in suggestions:
|
||||
if len(x)>i: p.add(x[i])
|
||||
|
||||
for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
|
||||
for c in line.children:
|
||||
if isinstance(c, Button):
|
||||
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
|
||||
c.disabled = (c.text.lower() not in p) and last_word
|
||||
elif c.text == ' ':
|
||||
c.disabled = not enable_space
|
||||
|
||||
def on_word(self, w):
|
||||
text = self.get_text()
|
||||
words = text.split(' ')
|
||||
words[-1] = w
|
||||
text = ' '.join(words)
|
||||
self.ids.text_input_seed.text = text + ' '
|
||||
self.ids.suggestions.clear_widgets()
|
||||
|
||||
def get_text(self):
|
||||
ti = self.ids.text_input_seed
|
||||
text = unicode(ti.text).strip()
|
||||
text = ' '.join(text.split())
|
||||
return text
|
||||
|
||||
def get_params(self, b):
|
||||
return (self.get_text(),)
|
||||
|
||||
def update_text(self, c):
|
||||
c = c.lower()
|
||||
text = self.ids.text_input_seed.text
|
||||
if c == '<':
|
||||
text = text[:-1]
|
||||
else:
|
||||
text += c
|
||||
self.ids.text_input_seed.text = text
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
tis = self.ids.text_input_seed
|
||||
tis.focus = True
|
||||
#tis._keyboard.bind(on_key_down=self.on_key_down)
|
||||
self._back = _back = partial(self.ids.back.dispatch,
|
||||
'on_release')
|
||||
app = App.get_running_app()
|
||||
|
||||
def on_key_down(self, keyboard, keycode, key, modifiers):
|
||||
if keycode[0] in (13, 271):
|
||||
self.on_enter()
|
||||
return True
|
||||
|
||||
def on_enter(self):
|
||||
#self._remove_keyboard()
|
||||
# press next
|
||||
next = self.ids.next
|
||||
if not next.disabled:
|
||||
next.dispatch('on_release')
|
||||
|
||||
def _remove_keyboard(self):
|
||||
tis = self.ids.text_input_seed
|
||||
if tis._keyboard:
|
||||
tis._keyboard.unbind(on_key_down=self.on_key_down)
|
||||
tis.focus = False
|
||||
|
||||
|
||||
class ShowXpubDialog(WizardDialog):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
WizardDialog.__init__(self, **kwargs)
|
||||
self.app = App.get_running_app()
|
||||
self.xpub = kwargs['xpub']
|
||||
self.ids.next.disabled = False
|
||||
|
||||
def do_copy(self):
|
||||
self.app._clipboard.copy(self.xpub)
|
||||
|
||||
def do_share(self):
|
||||
self.app.do_share(self.xpub, _("Master Public Key"))
|
||||
|
||||
def do_qr(self):
|
||||
from qr_dialog import QRDialog
|
||||
popup = QRDialog(_("Master Public Key"), self.xpub, True)
|
||||
popup.open()
|
||||
|
||||
|
||||
class AddXpubDialog(WizardDialog):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
WizardDialog.__init__(self, **kwargs)
|
||||
self.app = App.get_running_app()
|
||||
self._test = kwargs['test']
|
||||
self.title = kwargs['title']
|
||||
self.message = kwargs['message']
|
||||
|
||||
def check_text(self, dt):
|
||||
self.ids.next.disabled = not bool(self._test(self.get_text()))
|
||||
|
||||
def get_text(self):
|
||||
ti = self.ids.text_input
|
||||
return unicode(ti.text).strip()
|
||||
|
||||
def get_params(self, button):
|
||||
return (self.get_text(),)
|
||||
|
||||
def scan_xpub(self):
|
||||
def on_complete(text):
|
||||
self.ids.text_input.text = text
|
||||
self.app.scan_qr(on_complete)
|
||||
|
||||
def do_paste(self):
|
||||
self.ids.text_input.text = unicode(self.app._clipboard.paste())
|
||||
|
||||
def do_clear(self):
|
||||
self.ids.text_input.text = ''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InstallWizard(BaseWizard, Widget):
|
||||
'''
|
||||
events::
|
||||
`on_wizard_complete` Fired when the wizard is done creating/ restoring
|
||||
wallet/s.
|
||||
|
@ -32,12 +677,9 @@ class InstallWizard(Widget):
|
|||
|
||||
__events__ = ('on_wizard_complete', )
|
||||
|
||||
def __init__(self, config, network, storage):
|
||||
super(InstallWizard, self).__init__()
|
||||
self.config = config
|
||||
self.network = network
|
||||
self.storage = storage
|
||||
self.wallet = None
|
||||
def on_wizard_complete(self, wallet):
|
||||
"""overriden by main_window"""
|
||||
pass
|
||||
|
||||
def waiting_dialog(self, task, msg, on_complete=None):
|
||||
'''Perform a blocking task in the background by running the passed
|
||||
|
@ -60,189 +702,17 @@ class InstallWizard(Widget):
|
|||
t = threading.Thread(target = target)
|
||||
t.start()
|
||||
|
||||
def run(self, action, *args):
|
||||
'''Entry point of our Installation wizard'''
|
||||
if not action:
|
||||
return
|
||||
if hasattr(self, action):
|
||||
f = getattr(self, action)
|
||||
apply(f, *args)
|
||||
else:
|
||||
raise BaseException("unknown action", action)
|
||||
def choice_dialog(self, **kwargs): WizardChoiceDialog(**kwargs).open()
|
||||
def multisig_dialog(self, **kwargs): WizardMultisigDialog(**kwargs).open()
|
||||
def show_seed_dialog(self, **kwargs): ShowSeedDialog(**kwargs).open()
|
||||
def restore_seed_dialog(self, **kwargs): RestoreSeedDialog(**kwargs).open()
|
||||
def add_xpub_dialog(self, **kwargs): AddXpubDialog(**kwargs).open()
|
||||
def show_xpub_dialog(self, **kwargs): ShowXpubDialog(**kwargs).open()
|
||||
|
||||
def on_release(self, dialog, button):
|
||||
if not button or button.action == 'cancel':
|
||||
# soft back or escape button pressed
|
||||
return self.dispatch('on_wizard_complete', None)
|
||||
action = button.action if self.wallet is None else self.wallet.get_action()
|
||||
print "action", action
|
||||
dialog.close()
|
||||
self.run(action)
|
||||
|
||||
def add_seed_or_xpub(self):
|
||||
msg = ' '.join([
|
||||
_("Do you want to create a new seed, or to restore a wallet using an existing seed?")
|
||||
])
|
||||
choices = [
|
||||
(_('Create a new seed'), 'create_seed'),
|
||||
(_('I already have a seed'), 'restore_seed'),
|
||||
(_('Watching-only wallet'), 'restore_xpub')
|
||||
]
|
||||
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
|
||||
|
||||
def new(self):
|
||||
name = os.path.basename(self.storage.path)
|
||||
msg = "\n".join([
|
||||
_("Welcome to the Electrum installation wizard."),
|
||||
_("The wallet '%s' does not exist.") % name,
|
||||
_("What kind of wallet do you want to create?")
|
||||
])
|
||||
choices = [
|
||||
(_('Standard wallet'), 'create_standard'),
|
||||
(_('Multi-signature wallet'), 'create_multisig'),
|
||||
]
|
||||
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
|
||||
|
||||
def choose_cosigners(self):
|
||||
def on_release(dialog, button):
|
||||
if not button:
|
||||
# soft back or escape button pressed
|
||||
return self.dispatch('on_wizard_complete', None)
|
||||
m = dialog.ids.m.value
|
||||
n = dialog.ids.n.value
|
||||
dialog.close()
|
||||
self.wallet_type = "%dof%d"%(m, n)
|
||||
self.run('add_seed_or_xpub')
|
||||
name = os.path.basename(self.storage.path)
|
||||
WizardMultisigDialog(on_release=on_release).open()
|
||||
|
||||
def restore_seed(self):
|
||||
def on_seed(_dlg, btn):
|
||||
_dlg.close()
|
||||
if btn is _dlg.ids.back:
|
||||
self.run('new')
|
||||
return
|
||||
text = _dlg.get_text()
|
||||
self.run('enter_pin', (text,))
|
||||
msg = _('Please type your seed phrase using the virtual keyboard.')
|
||||
RestoreSeedDialog(test=Wallet.is_seed, message=msg, on_release=on_seed).open()
|
||||
|
||||
def restore_xpub(self):
|
||||
def on_xpub(_dlg, btn):
|
||||
_dlg.close()
|
||||
if btn is _dlg.ids.back:
|
||||
self.run('new')
|
||||
return
|
||||
text = _dlg.get_text()
|
||||
self.run('create_wallet', (text, None))
|
||||
title = "MASTER PUBLIC KEY"
|
||||
message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.')
|
||||
AddXpubDialog(title=title, message=message, test=Wallet.is_mpk, on_release=on_xpub).open()
|
||||
|
||||
def create_standard(self):
|
||||
self.wallet_type = 'standard'
|
||||
self.run('add_seed_or_xpub')
|
||||
|
||||
def create_multisig(self):
|
||||
self.wallet_type = 'multisig'
|
||||
self.run('choose_cosigners')
|
||||
|
||||
def create_wallet(self, text, password):
|
||||
if self.wallet_type == 'standard':
|
||||
self.wallet = Wallet.from_text(text, password, self.storage)
|
||||
self.run('create_addresses')
|
||||
else:
|
||||
self.storage.put('wallet_type', self.wallet_type)
|
||||
self.wallet = Multisig_Wallet(self.storage)
|
||||
self.wallet.add_seed(text, password)
|
||||
self.wallet.create_master_keys(password)
|
||||
action = self.wallet.get_action()
|
||||
self.run(action)
|
||||
|
||||
|
||||
def add_cosigners(self):
|
||||
def on_release(_dlg, btn):
|
||||
_dlg.close()
|
||||
self.run('add_cosigner')
|
||||
xpub = self.wallet.master_public_keys.get('x1/')
|
||||
ShowXpubDialog(xpub=xpub, test=Wallet.is_xpub, on_release=on_release).open()
|
||||
|
||||
def add_cosigner(self):
|
||||
def on_xpub(_dlg, btn):
|
||||
xpub = _dlg.get_text()
|
||||
_dlg.close()
|
||||
if btn is _dlg.ids.back:
|
||||
self.run('add_cosigners')
|
||||
return
|
||||
if xpub:
|
||||
self.wallet.add_master_public_key("x%d/" % 2, xpub)
|
||||
action = self.wallet.get_action()
|
||||
if action == 'add_cosigners': action = 'add_cosigner'
|
||||
self.run(action)
|
||||
|
||||
title = "ADD COSIGNERS"
|
||||
message = _('Please paste your cosigners master public key, or scan it using the camera button.')
|
||||
AddXpubDialog(title=title, message=message, test=Wallet.is_xpub, on_release=on_xpub).open()
|
||||
|
||||
def create_main_account(self):
|
||||
self.wallet.create_main_account()
|
||||
self.run('create_addresses')
|
||||
|
||||
def create_addresses(self):
|
||||
def task():
|
||||
self.wallet.create_main_account()
|
||||
self.wallet.synchronize()
|
||||
msg= _("Electrum is generating your addresses, please wait.")
|
||||
self.waiting_dialog(task, msg, on_complete=self.terminate)
|
||||
|
||||
def create_seed(self):
|
||||
from electrum.wallet import BIP32_Wallet
|
||||
seed = BIP32_Wallet.make_seed()
|
||||
msg = _("If you forget your PIN or lose your device, your seed phrase will be the "
|
||||
"only way to recover your funds.")
|
||||
def on_ok(_dlg, _btn):
|
||||
_dlg.close()
|
||||
if _btn == _dlg.ids.next:
|
||||
self.run('confirm_seed', (seed,))
|
||||
else:
|
||||
self.run('new')
|
||||
ShowSeedDialog(message=msg, seed_text=seed, on_release=on_ok).open()
|
||||
|
||||
def confirm_seed(self, seed):
|
||||
assert Wallet.is_seed(seed)
|
||||
def on_seed(_dlg, btn):
|
||||
if btn is _dlg.ids.back:
|
||||
_dlg.close()
|
||||
self.run('create_seed')
|
||||
return
|
||||
_dlg.close()
|
||||
self.run('enter_pin', (seed,))
|
||||
msg = _('Please retype your seed phrase, to confirm that you properly saved it')
|
||||
RestoreSeedDialog(test=lambda x: x==seed, message=msg, on_release=on_seed).open()
|
||||
|
||||
def enter_pin(self, seed):
|
||||
def callback(pin):
|
||||
action = 'confirm_pin' if pin else 'create_wallet'
|
||||
self.run(action, (seed, pin))
|
||||
def password_dialog(self, message, callback):
|
||||
popup = PasswordDialog()
|
||||
popup.init('Choose a PIN code', callback)
|
||||
popup.init(message, callback)
|
||||
popup.open()
|
||||
|
||||
def confirm_pin(self, seed, pin):
|
||||
def callback(conf):
|
||||
if conf == pin:
|
||||
self.run('create_wallet', (seed, pin))
|
||||
else:
|
||||
app.show_error(_('PIN mismatch'), duration=.5)
|
||||
self.run('enter_pin', (seed,))
|
||||
popup = PasswordDialog()
|
||||
popup.init('Confirm your PIN code', callback)
|
||||
popup.open()
|
||||
|
||||
def terminate(self):
|
||||
self.wallet.start_threads(self.network)
|
||||
self.dispatch('on_wizard_complete', self.wallet)
|
||||
|
||||
def on_wizard_complete(self, wallet):
|
||||
"""overriden by main_window"""
|
||||
pass
|
||||
def show_error(self, msg):
|
||||
app.show_error(msg, duration=0.5)
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2016 Thomas Voegtlin
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
from electrum.wallet import Wallet, Multisig_Wallet
|
||||
from electrum_gui.kivy.i18n import _
|
||||
|
||||
|
||||
class BaseWizard(object):
|
||||
|
||||
def __init__(self, config, network, storage):
|
||||
super(BaseWizard, self).__init__()
|
||||
self.config = config
|
||||
self.network = network
|
||||
self.storage = storage
|
||||
self.wallet = None
|
||||
|
||||
def run(self, action, *args):
|
||||
'''Entry point of our Installation wizard'''
|
||||
if not action:
|
||||
return
|
||||
if hasattr(self, action):
|
||||
f = getattr(self, action)
|
||||
apply(f, *args)
|
||||
else:
|
||||
raise BaseException("unknown action", action)
|
||||
|
||||
def new(self):
|
||||
name = os.path.basename(self.storage.path)
|
||||
msg = "\n".join([
|
||||
_("Welcome to the Electrum installation wizard."),
|
||||
_("The wallet '%s' does not exist.") % name,
|
||||
_("What kind of wallet do you want to create?")
|
||||
])
|
||||
choices = [
|
||||
(_('Standard wallet'), 'create_standard'),
|
||||
(_('Multi-signature wallet'), 'create_multisig'),
|
||||
]
|
||||
self.choice_dialog(msg=msg, choices=choices, run_prev=self.cancel, run_next=self.run)
|
||||
|
||||
def choose_seed(self):
|
||||
msg = ' '.join([
|
||||
_("Do you want to create a new seed, or to restore a wallet using an existing seed?")
|
||||
])
|
||||
choices = [
|
||||
(_('Create a new seed'), 'create_seed'),
|
||||
(_('I already have a seed'), 'restore_seed'),
|
||||
(_('Watching-only wallet'), 'restore_xpub')
|
||||
]
|
||||
self.choice_dialog(msg=msg, choices=choices, run_prev=self.new, run_next=self.run)
|
||||
|
||||
def create_multisig(self):
|
||||
def f(m, n):
|
||||
self.wallet_type = "%dof%d"%(m, n)
|
||||
self.run('choose_seed')
|
||||
name = os.path.basename(self.storage.path)
|
||||
self.multisig_dialog(run_prev=self.new, run_next=f)
|
||||
|
||||
def restore_seed(self):
|
||||
msg = _('Please type your seed phrase using the virtual keyboard.')
|
||||
self.restore_seed_dialog(run_prev=self.new, run_next=self.enter_pin, test=Wallet.is_seed, message=msg)
|
||||
|
||||
def restore_xpub(self):
|
||||
title = "MASTER PUBLIC KEY"
|
||||
message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.')
|
||||
self.add_xpub_dialog(run_prev=self.new, run_next=lambda xpub: self.create_wallet(xpub, None), title=title, message=message, test=Wallet.is_mpk)
|
||||
|
||||
def create_standard(self):
|
||||
self.wallet_type = 'standard'
|
||||
self.run('choose_seed')
|
||||
|
||||
def create_wallet(self, text, password):
|
||||
if self.wallet_type == 'standard':
|
||||
self.wallet = Wallet.from_text(text, password, self.storage)
|
||||
self.run('create_addresses')
|
||||
else:
|
||||
self.storage.put('wallet_type', self.wallet_type)
|
||||
self.wallet = Multisig_Wallet(self.storage)
|
||||
self.wallet.add_seed(text, password)
|
||||
self.wallet.create_master_keys(password)
|
||||
action = self.wallet.get_action()
|
||||
self.run(action)
|
||||
|
||||
def add_cosigners(self):
|
||||
xpub = self.wallet.master_public_keys.get('x1/')
|
||||
self.show_xpub_dialog(run_prev=self.create_multisig, run_next=self.add_cosigner, xpub=xpub, test=Wallet.is_xpub)
|
||||
|
||||
def add_cosigner(self):
|
||||
def on_xpub(xpub):
|
||||
self.wallet.add_cosigner(xpub)
|
||||
i = self.wallet.get_missing_cosigner()
|
||||
action = 'add_cosigner' if i else 'create_main_account'
|
||||
self.run(action)
|
||||
title = "ADD COSIGNER"
|
||||
message = _('Please paste your cosigners master public key, or scan it using the camera button.')
|
||||
self.add_xpub_dialog(run_prev=self.add_cosigners, run_next=on_xpub, title=title, message=message, test=Wallet.is_xpub)
|
||||
|
||||
def create_main_account(self):
|
||||
self.wallet.create_main_account()
|
||||
self.run('create_addresses')
|
||||
|
||||
def create_addresses(self):
|
||||
def task():
|
||||
self.wallet.create_main_account()
|
||||
self.wallet.synchronize()
|
||||
msg= _("Electrum is generating your addresses, please wait.")
|
||||
self.waiting_dialog(task, msg, on_complete=self.terminate)
|
||||
|
||||
def create_seed(self):
|
||||
from electrum.wallet import BIP32_Wallet
|
||||
seed = BIP32_Wallet.make_seed()
|
||||
msg = _("If you forget your PIN or lose your device, your seed phrase will be the "
|
||||
"only way to recover your funds.")
|
||||
self.show_seed_dialog(run_prev=self.new, run_next=self.confirm_seed, message=msg, seed_text=seed)
|
||||
|
||||
def confirm_seed(self, seed):
|
||||
assert Wallet.is_seed(seed)
|
||||
msg = _('Please retype your seed phrase, to confirm that you properly saved it')
|
||||
self.restore_seed_dialog(run_prev=self.create_seed, run_next=self.enter_pin, test=lambda x: x==seed, message=msg)
|
||||
|
||||
def enter_pin(self, seed):
|
||||
def callback(pin):
|
||||
action = 'confirm_pin' if pin else 'create_wallet'
|
||||
self.run(action, (seed, pin))
|
||||
self.password_dialog('Choose a PIN code', callback)
|
||||
|
||||
def confirm_pin(self, seed, pin):
|
||||
def callback(conf):
|
||||
if conf == pin:
|
||||
self.run('create_wallet', (seed, pin))
|
||||
else:
|
||||
self.show_error(_('PIN mismatch'))
|
||||
self.run('enter_pin', (seed,))
|
||||
self.password_dialog('Confirm your PIN code', callback)
|
||||
|
||||
def terminate(self):
|
||||
self.wallet.start_threads(self.network)
|
||||
self.dispatch('on_wizard_complete', self.wallet)
|
||||
|
||||
def cancel(self):
|
||||
self.dispatch('on_wizard_complete', None)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -1915,10 +1915,19 @@ class Multisig_Wallet(BIP32_RD_Wallet, Mnemonic):
|
|||
def get_master_public_keys(self):
|
||||
return self.master_public_keys
|
||||
|
||||
def get_action(self):
|
||||
def get_missing_cosigner(self):
|
||||
for i in range(self.n):
|
||||
if self.master_public_keys.get("x%d/"%(i+1)) is None:
|
||||
return 'create_seed' if i == 0 else 'add_cosigners'
|
||||
return i+1
|
||||
|
||||
def add_cosigner(self, xpub):
|
||||
i = self.get_missing_cosigner()
|
||||
self.add_master_public_key("x%d/" % i, xpub)
|
||||
|
||||
def get_action(self):
|
||||
i = self.get_missing_cosigner()
|
||||
if i is not None:
|
||||
return 'create_seed' if i == 1 else 'add_cosigners'
|
||||
if not self.accounts:
|
||||
return 'create_main_account'
|
||||
|
||||
|
|
Loading…
Reference in New Issue