3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 from __future__ import absolute_import
25 from electrum import SimpleConfig, Wallet, WalletStorage, format_satoshis, mnemonic_encode, mnemonic_decode
26 from electrum.bitcoin import is_valid
27 from electrum import util
28 from decimal import Decimal
33 def modal_dialog(title, msg = None):
34 droid.dialogCreateAlert(title,msg)
35 droid.dialogSetPositiveButtonText('OK')
37 droid.dialogGetResponse()
40 def modal_input(title, msg, value = None, etype=None):
41 droid.dialogCreateInput(title, msg, value, etype)
42 droid.dialogSetPositiveButtonText('OK')
43 droid.dialogSetNegativeButtonText('Cancel')
45 response = droid.dialogGetResponse()
46 result = response.result
50 print "modal input: result is none"
51 return modal_input(title, msg, value, etype)
53 if result.get('which') == 'positive':
54 return result.get('value')
56 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
57 droid.dialogCreateAlert(q, msg)
58 droid.dialogSetPositiveButtonText(pos_text)
59 droid.dialogSetNegativeButtonText(neg_text)
61 response = droid.dialogGetResponse()
62 result = response.result
66 print "modal question: result is none"
67 return modal_question(q,msg, pos_text, neg_text)
69 return result.get('which') == 'positive'
72 v = modal_input('Edit label',None,wallet.labels.get(addr))
75 wallet.labels[addr] = v
77 if addr in wallet.labels.keys():
78 wallet.labels.pop(addr)
79 wallet.update_tx_history()
81 droid.fullSetProperty("labelTextView", "text", v)
83 def select_from_contacts():
85 droid.dialogCreateAlert(title)
87 for i in range(len(wallet.addressbook)):
88 addr = wallet.addressbook[i]
89 label = wallet.labels.get(addr,addr)
91 droid.dialogSetItems(l)
92 droid.dialogSetPositiveButtonText('New contact')
94 response = droid.dialogGetResponse().result
97 if response.get('which') == 'positive':
100 result = response.get('item')
102 if result is not None:
103 addr = wallet.addressbook[result]
107 def select_from_addresses():
108 droid.dialogCreateAlert("Addresses:")
110 addresses = wallet.addresses()
111 for i in range(len(addresses)):
113 label = wallet.labels.get(addr,addr)
115 droid.dialogSetItems(l)
117 response = droid.dialogGetResponse()
118 result = response.result.get('item')
119 droid.dialogDismiss()
120 if result is not None:
121 addr = addresses[result]
125 def protocol_name(p):
126 if p == 't': return 'TCP'
127 if p == 'h': return 'HTTP'
128 if p == 's': return 'SSL'
129 if p == 'g': return 'HTTPS'
132 def protocol_dialog(host, protocol, z):
133 droid.dialogCreateAlert('Protocol',host)
139 current = protocols.index(protocol)
141 l.append(protocol_name(p))
142 droid.dialogSetSingleChoiceItems(l, current)
143 droid.dialogSetPositiveButtonText('OK')
144 droid.dialogSetNegativeButtonText('Cancel')
146 response = droid.dialogGetResponse().result
147 selected_item = droid.dialogGetSelectedItems().result
148 droid.dialogDismiss()
150 if not response: return
151 if not selected_item: return
152 if response.get('which') == 'positive':
153 return protocols[selected_item[0]]
158 def make_layout(s, scrollable = False):
163 android:layout_width="match_parent"
164 android:layout_height="wrap_content"
165 android:background="#ff222222">
168 android:id="@+id/textElectrum"
169 android:text="Electrum"
170 android:textSize="7pt"
171 android:textColor="#ff4444ff"
172 android:gravity="left"
173 android:layout_height="wrap_content"
174 android:layout_width="match_parent"
183 android:id="@+id/scrollview"
184 android:layout_width="match_parent"
185 android:layout_height="match_parent" >
188 android:orientation="vertical"
189 android:layout_width="match_parent"
190 android:layout_height="wrap_content" >
199 return """<?xml version="1.0" encoding="utf-8"?>
200 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
201 android:id="@+id/background"
202 android:orientation="vertical"
203 android:layout_width="match_parent"
204 android:layout_height="match_parent"
205 android:background="#ff000022">
208 </LinearLayout>"""%content
214 return make_layout("""
215 <TextView android:id="@+id/balanceTextView"
216 android:layout_width="match_parent"
218 android:textColor="#ffffffff"
219 android:textAppearance="?android:attr/textAppearanceLarge"
220 android:padding="7dip"
221 android:textSize="8pt"
222 android:gravity="center_vertical|center_horizontal|left">
225 <TextView android:id="@+id/historyTextView"
226 android:layout_width="match_parent"
227 android:layout_height="wrap_content"
228 android:text="Recent transactions"
229 android:textAppearance="?android:attr/textAppearanceLarge"
230 android:gravity="center_vertical|center_horizontal|center">
233 %s """%get_history_layout(15),True)
238 return make_layout("""
240 <TextView android:id="@+id/addrTextView"
241 android:layout_width="match_parent"
242 android:layout_height="50"
244 android:textAppearance="?android:attr/textAppearanceLarge"
245 android:gravity="center_vertical|center_horizontal|center">
249 android:id="@+id/qrView"
250 android:gravity="center"
251 android:layout_width="match_parent"
252 android:layout_height="350"
253 android:antialias="false"
254 android:src="file:///sdcard/sl4a/qrcode.bmp" />
256 <TextView android:id="@+id/labelTextView"
257 android:layout_width="match_parent"
258 android:layout_height="50"
260 android:textAppearance="?android:attr/textAppearanceLarge"
261 android:gravity="center_vertical|center_horizontal|center">
264 """%(addr,wallet.labels.get(addr,'')), True)
266 payto_layout = make_layout("""
268 <TextView android:id="@+id/recipientTextView"
269 android:layout_width="match_parent"
270 android:layout_height="wrap_content"
271 android:text="Pay to:"
272 android:textAppearance="?android:attr/textAppearanceLarge"
273 android:gravity="left">
277 <EditText android:id="@+id/recipient"
278 android:layout_width="match_parent"
279 android:layout_height="wrap_content"
280 android:tag="Tag Me" android:inputType="text">
283 <LinearLayout android:id="@+id/linearLayout1"
284 android:layout_width="match_parent"
285 android:layout_height="wrap_content">
286 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
287 android:layout_height="wrap_content" android:text="From QR code"></Button>
288 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
289 android:layout_height="wrap_content" android:text="From Contacts"></Button>
293 <TextView android:id="@+id/labelTextView"
294 android:layout_width="match_parent"
295 android:layout_height="wrap_content"
296 android:text="Description:"
297 android:textAppearance="?android:attr/textAppearanceLarge"
298 android:gravity="left">
301 <EditText android:id="@+id/label"
302 android:layout_width="match_parent"
303 android:layout_height="wrap_content"
304 android:tag="Tag Me" android:inputType="text">
307 <TextView android:id="@+id/amountLabelTextView"
308 android:layout_width="match_parent"
309 android:layout_height="wrap_content"
310 android:text="Amount:"
311 android:textAppearance="?android:attr/textAppearanceLarge"
312 android:gravity="left">
315 <EditText android:id="@+id/amount"
316 android:layout_width="match_parent"
317 android:layout_height="wrap_content"
318 android:tag="Tag Me" android:inputType="numberDecimal">
321 <LinearLayout android:layout_width="match_parent"
322 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
323 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
324 android:layout_height="wrap_content" android:text="Send"></Button>
325 </LinearLayout>""",False)
329 settings_layout = make_layout(""" <ListView
330 android:id="@+id/myListView"
331 android:layout_width="match_parent"
332 android:layout_height="wrap_content" />""")
336 def get_history_values(n):
338 h = wallet.get_tx_history()
339 length = min(n, len(h))
340 for i in range(length):
341 tx_hash, conf, is_mine, value, fee, balance, timestamp = h[-i-1]
343 dt = datetime.datetime.fromtimestamp( timestamp )
344 if dt.date() == dt.today().date():
345 time_str = str( dt.time() )
347 time_str = str( dt.date() )
351 conf_str = 'v' if conf else 'o'
352 label, is_default_label = wallet.get_label(tx_hash)
353 values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value,True), ' ' + label ))
358 def get_history_layout(n):
361 values = get_history_values(n)
364 color = "#ff00ff00" if a == 'v' else "#ffff0000"
368 android:id="@+id/hl_%d_col1"
369 android:layout_column="0"
371 android:textColor="%s"
372 android:padding="3" />
374 android:id="@+id/hl_%d_col2"
375 android:layout_column="1"
377 android:padding="3" />
379 android:id="@+id/hl_%d_col3"
380 android:layout_column="2"
382 android:padding="3" />
384 android:id="@+id/hl_%d_col4"
385 android:layout_column="3"
387 android:padding="4" />
388 </TableRow>"""%(i,a,color,i,b,i,c,i,d)
392 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
393 android:layout_width="fill_parent"
394 android:layout_height="wrap_content"
395 android:stretchColumns="0,1,2,3">
397 </TableLayout>"""% rows
401 def set_history_layout(n):
402 values = get_history_values(n)
406 droid.fullSetProperty("hl_%d_col1"%i,"text", a)
409 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
411 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
413 droid.fullSetProperty("hl_%d_col2"%i,"text", b)
414 droid.fullSetProperty("hl_%d_col3"%i,"text", c)
415 droid.fullSetProperty("hl_%d_col4"%i,"text", d)
424 if not network.is_connected():
425 text = "Not connected..."
426 elif not wallet.up_to_date:
427 text = "Synchronizing..."
429 c, u = wallet.get_balance()
430 text = "Balance:"+format_satoshis(c)
431 if u : text += ' [' + format_satoshis(u,True).strip() + ']'
434 # vibrate if status changed
435 if text != status_text:
436 if status_text and network.is_connected() and wallet.up_to_date:
440 droid.fullSetProperty("balanceTextView", "text", status_text)
442 if wallet.up_to_date:
443 set_history_layout(15)
448 def pay_to(recipient, amount, fee, label):
450 if wallet.use_encryption:
451 password = droid.dialogGetPassword('Password').result
452 if not password: return
456 droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
460 tx = wallet.mktx( [(recipient, amount)], password, fee)
461 except Exception as e:
462 modal_dialog('error', e.message)
463 droid.dialogDismiss()
467 wallet.labels[tx.hash()] = label
469 droid.dialogDismiss()
471 r, h = wallet.sendtx( tx )
473 modal_dialog('Payment sent', h)
476 modal_dialog('Error', h)
484 def make_new_contact():
485 code = droid.scanBarcode()
488 data = r['extras']['SCAN_RESULT']
490 if re.match('^bitcoin:', data):
491 address, _, _, _, _, _, _ = util.parse_url(data)
497 if modal_question('Add to contacts?', address):
498 wallet.add_contact(address)
500 modal_dialog('Invalid address', data)
505 def update_callback():
507 print "gui callback", network.is_connected()
509 droid.eventPost("refresh",'z')
519 event = droid.eventWait(1000).result
526 print "got event in main loop", repr(event)
527 if event == 'OK': continue
528 if event is None: continue
529 if not event.get("name"): continue
531 # request 2 taps before we exit
532 if event["name"]=="key":
533 if event["data"]["key"] == '4':
538 else: quitting = False
540 if event["name"]=="click":
541 id=event["data"]["id"]
543 elif event["name"]=="settings":
546 elif event["name"] in menu_commands:
549 if out == 'contacts':
551 contact_addr = select_from_contacts()
552 if contact_addr == 'newcontact':
558 elif out == "receive":
560 receive_addr = select_from_addresses()
562 amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal")
564 receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount)
576 droid.fullSetProperty("recipient","text",recipient)
581 event = droid.eventWait().result
582 if not event: continue
583 print "got event in payto loop", event
584 if event == 'OK': continue
585 if not event.get("name"): continue
587 if event["name"] == "click":
588 id = event["data"]["id"]
593 recipient = droid.fullQueryDetail("recipient").result.get('text')
594 label = droid.fullQueryDetail("label").result.get('text')
595 amount = droid.fullQueryDetail('amount').result.get('text')
597 if not is_valid(recipient):
598 modal_dialog('Error','Invalid Bitcoin address')
602 amount = int( 100000000 * Decimal(amount) )
604 modal_dialog('Error','Invalid amount')
607 result = pay_to(recipient, amount, wallet.fee, label)
611 elif id=="buttonContacts":
612 addr = select_from_contacts()
613 droid.fullSetProperty("recipient","text",addr)
616 code = droid.scanBarcode()
619 data = r['extras']['SCAN_RESULT']
621 if re.match('^bitcoin:', data):
622 payto, amount, label, _, _, _, _ = util.parse_url(data)
623 droid.fullSetProperty("recipient", "text",payto)
624 droid.fullSetProperty("amount", "text", amount)
625 droid.fullSetProperty("label", "text", label)
627 droid.fullSetProperty("recipient", "text", data)
630 elif event["name"] in menu_commands:
633 elif event["name"]=="key":
634 if event["data"]["key"] == '4':
637 #elif event["name"]=="screen":
638 # if event["data"]=="destroy":
651 event = droid.eventWait().result
652 print "got event", event
653 if event["name"]=="key":
654 if event["data"]["key"] == '4':
657 elif event["name"]=="clipboard":
658 droid.setClipboard(receive_addr)
659 modal_dialog('Address copied to clipboard',receive_addr)
661 elif event["name"]=="edit":
662 edit_label(receive_addr)
670 event = droid.eventWait().result
671 print "got event", event
672 if event["name"]=="key":
673 if event["data"]["key"] == '4':
676 elif event["name"]=="clipboard":
677 droid.setClipboard(contact_addr)
678 modal_dialog('Address copied to clipboard',contact_addr)
680 elif event["name"]=="edit":
681 edit_label(contact_addr)
683 elif event["name"]=="paytocontact":
684 recipient = contact_addr
687 elif event["name"]=="deletecontact":
688 if modal_question('delete contact', contact_addr):
694 def server_dialog(servers):
695 droid.dialogCreateAlert("Public servers")
696 droid.dialogSetItems( servers.keys() )
697 droid.dialogSetPositiveButtonText('Private server')
699 response = droid.dialogGetResponse().result
700 droid.dialogDismiss()
701 if not response: return
703 if response.get('which') == 'positive':
704 return modal_input('Private server', None)
706 i = response.get('item')
708 response = servers.keys()[i]
713 if wallet.use_encryption:
714 password = droid.dialogGetPassword('Seed').result
715 if not password: return
720 seed = wallet.get_seed(password)
722 modal_dialog('error','incorrect password')
725 modal_dialog('Your seed is',seed)
726 modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(seed)) )
728 def change_password_dialog():
729 if wallet.use_encryption:
730 password = droid.dialogGetPassword('Your wallet is encrypted').result
731 if password is None: return
736 wallet.get_seed(password)
738 modal_dialog('error','incorrect password')
741 new_password = droid.dialogGetPassword('Choose a password').result
742 if new_password == None:
745 if new_password != '':
746 password2 = droid.dialogGetPassword('Confirm new password').result
747 if new_password != password2:
748 modal_dialog('error','passwords do not match')
751 wallet.update_password(password, new_password)
753 modal_dialog('Password updated','your wallet is encrypted')
755 modal_dialog('No password','your wallet is not encrypted')
763 host, port, p = network.default_server.split(':')
764 fee = str( Decimal( wallet.fee)/100000000 )
765 is_encrypted = 'yes' if wallet.use_encryption else 'no'
766 protocol = protocol_name(p)
767 droid.fullShow(settings_layout)
768 droid.fullSetList("myListView",['Server: ' + host, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
774 event = droid.eventWait()
776 print "got event", event
777 if event == 'OK': continue
778 if not event: continue
780 servers = network.get_servers()
781 name = event.get("name")
782 if not name: continue
784 if name == "itemclick":
785 pos = event["data"]["position"]
786 host, port, protocol = network.default_server.split(':')
787 network_changed = False
789 if pos == "0": #server
790 host = server_dialog(servers)
794 network_changed = True
796 elif pos == "1": #protocol
798 protocol = protocol_dialog(host, protocol, servers[host])
801 network_changed = True
803 elif pos == "2": #port
804 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
807 network_changed = True
809 elif pos == "3": #fee
810 fee = modal_input('Transaction fee', 'The fee will be this amount multiplied by the number of inputs in your transaction. ', str( Decimal( wallet.fee)/100000000 ), "numberDecimal")
813 fee = int( 100000000 * Decimal(fee) )
815 modal_dialog('error','invalid fee value')
820 if change_password_dialog():
830 network.set_parameters(host, port, protocol, proxy, auto_connect)
832 modal_dialog('error','invalid server')
835 elif name in menu_commands:
838 elif name == 'cancel':
842 if event["data"]["key"] == '4':
848 droid.clearOptionsMenu()
850 droid.addOptionsMenuItem("Send","send",None,"")
851 droid.addOptionsMenuItem("Receive","receive",None,"")
852 droid.addOptionsMenuItem("Contacts","contacts",None,"")
853 droid.addOptionsMenuItem("Settings","settings",None,"")
855 droid.addOptionsMenuItem("Copy","clipboard",None,"")
856 droid.addOptionsMenuItem("Label","edit",None,"")
857 elif s == 'contacts':
858 droid.addOptionsMenuItem("Copy","clipboard",None,"")
859 droid.addOptionsMenuItem("Label","edit",None,"")
860 droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
861 #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
864 def make_bitmap(addr):
865 # fixme: this is highly inefficient
866 droid.dialogCreateSpinnerProgress("please wait")
869 import pyqrnative, bmp
870 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
873 k = qr.getModuleCount()
875 bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
877 droid.dialogDismiss()
882 droid = android.Android()
883 menu_commands = ["send", "receive", "settings", "contacts", "main"]
889 def __init__(self, config, _network):
890 global wallet, network
892 network.register_callback('updated', update_callback)
893 network.register_callback('connected', update_callback)
894 network.register_callback('disconnected', update_callback)
895 network.register_callback('disconnecting', update_callback)
897 storage = WalletStorage(config)
898 if not storage.file_exists:
899 action = self.restore_or_create()
900 if not action: exit()
902 wallet = Wallet(storage)
903 if action == 'create':
904 wallet.init_seed(None)
906 wallet.save_seed(None)
907 wallet.synchronize() # generate first addresses offline
909 elif action == 'restore':
910 seed = self.seed_dialog()
913 wallet.init_seed(str(seed))
914 wallet.save_seed(None)
918 wallet.start_threads(network)
920 if action == 'restore':
921 if not self.restore_wallet():
924 self.password_dialog()
927 wallet = Wallet(storage)
928 wallet.start_threads(network)
936 droid.fullShow(main_layout())
940 droid.fullShow(payto_layout)
944 make_bitmap(receive_addr)
945 droid.fullShow(qr_layout(receive_addr))
948 elif s == 'contacts':
949 make_bitmap(contact_addr)
950 droid.fullShow(qr_layout(contact_addr))
953 elif s == 'settings':
959 droid.makeToast("Bye!")
962 def restore_or_create(self):
963 droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
964 droid.dialogSetPositiveButtonText('Create')
965 droid.dialogSetNeutralButtonText('Restore')
966 droid.dialogSetNegativeButtonText('Cancel')
968 response = droid.dialogGetResponse().result
969 droid.dialogDismiss()
970 if not response: return
971 if response.get('which') == 'negative':
974 return 'restore' if response.get('which') == 'neutral' else 'create'
977 def seed_dialog(self):
978 if modal_question("Enter your seed","Input method",'QR Code', 'mnemonic'):
979 code = droid.scanBarcode()
982 seed = r['extras']['SCAN_RESULT']
986 m = modal_input('Mnemonic','please enter your code')
988 seed = mnemonic_decode(m.split(' '))
990 modal_dialog('error: could not decode this seed')
996 def network_dialog(self):
1000 def show_seed(self):
1001 modal_dialog('Your seed is:', wallet.seed)
1002 modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(wallet.seed)) )
1005 def password_dialog(self):
1006 change_password_dialog()
1009 def restore_wallet(self):
1011 msg = "recovering wallet..."
1012 droid.dialogCreateSpinnerProgress("Electrum", msg)
1015 wallet.restore(lambda x: None)
1017 droid.dialogDismiss()
1020 if wallet.is_found():
1021 wallet.fill_addressbook()
1022 modal_dialog("recovery successful")
1024 if not modal_question("no transactions found for this seed","do you want to keep this wallet?"):