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
48 print "modal input: result is none"
51 if result.get('which') == 'positive':
52 return result.get('value')
54 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
55 droid.dialogCreateAlert(q, msg)
56 droid.dialogSetPositiveButtonText(pos_text)
57 droid.dialogSetNegativeButtonText(neg_text)
59 response = droid.dialogGetResponse()
60 result = response.result
62 print "modal question: result is none"
65 return result.get('which') == 'positive'
68 v = modal_input('Edit label',None,wallet.labels.get(addr))
71 wallet.labels[addr] = v
73 if addr in wallet.labels.keys():
74 wallet.labels.pop(addr)
75 wallet.update_tx_history()
77 droid.fullSetProperty("labelTextView", "text", v)
79 def select_from_contacts():
81 droid.dialogCreateAlert(title)
83 for i in range(len(wallet.addressbook)):
84 addr = wallet.addressbook[i]
85 label = wallet.labels.get(addr,addr)
87 droid.dialogSetItems(l)
88 droid.dialogSetPositiveButtonText('New contact')
90 response = droid.dialogGetResponse().result
93 if response.get('which') == 'positive':
96 result = response.get('item')
98 if result is not None:
99 addr = wallet.addressbook[result]
103 def select_from_addresses():
104 droid.dialogCreateAlert("Addresses:")
106 addresses = wallet.addresses()
107 for i in range(len(addresses)):
109 label = wallet.labels.get(addr,addr)
111 droid.dialogSetItems(l)
113 response = droid.dialogGetResponse()
114 result = response.result.get('item')
115 droid.dialogDismiss()
116 if result is not None:
117 addr = addresses[result]
121 def protocol_name(p):
122 if p == 't': return 'TCP'
123 if p == 'h': return 'HTTP'
124 if p == 's': return 'SSL'
125 if p == 'g': return 'HTTPS'
128 def protocol_dialog(host, protocol, z):
129 droid.dialogCreateAlert('Protocol',host)
135 current = protocols.index(protocol)
137 l.append(protocol_name(p))
138 droid.dialogSetSingleChoiceItems(l, current)
139 droid.dialogSetPositiveButtonText('OK')
140 droid.dialogSetNegativeButtonText('Cancel')
142 response = droid.dialogGetResponse().result
143 selected_item = droid.dialogGetSelectedItems().result
144 droid.dialogDismiss()
146 if not response: return
147 if not selected_item: return
148 if response.get('which') == 'positive':
149 return protocols[selected_item[0]]
154 def make_layout(s, scrollable = False):
159 android:layout_width="match_parent"
160 android:layout_height="wrap_content"
161 android:background="#ff222222">
164 android:id="@+id/textElectrum"
165 android:text="Electrum"
166 android:textSize="7pt"
167 android:textColor="#ff4444ff"
168 android:gravity="left"
169 android:layout_height="wrap_content"
170 android:layout_width="match_parent"
179 android:id="@+id/scrollview"
180 android:layout_width="match_parent"
181 android:layout_height="match_parent" >
184 android:orientation="vertical"
185 android:layout_width="match_parent"
186 android:layout_height="wrap_content" >
195 return """<?xml version="1.0" encoding="utf-8"?>
196 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
197 android:id="@+id/background"
198 android:orientation="vertical"
199 android:layout_width="match_parent"
200 android:layout_height="match_parent"
201 android:background="#ff000022">
204 </LinearLayout>"""%content
210 return make_layout("""
211 <TextView android:id="@+id/balanceTextView"
212 android:layout_width="match_parent"
214 android:textColor="#ffffffff"
215 android:textAppearance="?android:attr/textAppearanceLarge"
216 android:padding="7dip"
217 android:textSize="8pt"
218 android:gravity="center_vertical|center_horizontal|left">
221 <TextView android:id="@+id/historyTextView"
222 android:layout_width="match_parent"
223 android:layout_height="wrap_content"
224 android:text="Recent transactions"
225 android:textAppearance="?android:attr/textAppearanceLarge"
226 android:gravity="center_vertical|center_horizontal|center">
229 %s """%get_history_layout(15),True)
234 return make_layout("""
236 <TextView android:id="@+id/addrTextView"
237 android:layout_width="match_parent"
238 android:layout_height="50"
240 android:textAppearance="?android:attr/textAppearanceLarge"
241 android:gravity="center_vertical|center_horizontal|center">
245 android:id="@+id/qrView"
246 android:gravity="center"
247 android:layout_width="match_parent"
248 android:layout_height="350"
249 android:antialias="false"
250 android:src="file:///sdcard/sl4a/qrcode.bmp" />
252 <TextView android:id="@+id/labelTextView"
253 android:layout_width="match_parent"
254 android:layout_height="50"
256 android:textAppearance="?android:attr/textAppearanceLarge"
257 android:gravity="center_vertical|center_horizontal|center">
260 """%(addr,wallet.labels.get(addr,'')), True)
262 payto_layout = make_layout("""
264 <TextView android:id="@+id/recipientTextView"
265 android:layout_width="match_parent"
266 android:layout_height="wrap_content"
267 android:text="Pay to:"
268 android:textAppearance="?android:attr/textAppearanceLarge"
269 android:gravity="left">
273 <EditText android:id="@+id/recipient"
274 android:layout_width="match_parent"
275 android:layout_height="wrap_content"
276 android:tag="Tag Me" android:inputType="text">
279 <LinearLayout android:id="@+id/linearLayout1"
280 android:layout_width="match_parent"
281 android:layout_height="wrap_content">
282 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
283 android:layout_height="wrap_content" android:text="From QR code"></Button>
284 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
285 android:layout_height="wrap_content" android:text="From Contacts"></Button>
289 <TextView android:id="@+id/labelTextView"
290 android:layout_width="match_parent"
291 android:layout_height="wrap_content"
292 android:text="Description:"
293 android:textAppearance="?android:attr/textAppearanceLarge"
294 android:gravity="left">
297 <EditText android:id="@+id/label"
298 android:layout_width="match_parent"
299 android:layout_height="wrap_content"
300 android:tag="Tag Me" android:inputType="text">
303 <TextView android:id="@+id/amountLabelTextView"
304 android:layout_width="match_parent"
305 android:layout_height="wrap_content"
306 android:text="Amount:"
307 android:textAppearance="?android:attr/textAppearanceLarge"
308 android:gravity="left">
311 <EditText android:id="@+id/amount"
312 android:layout_width="match_parent"
313 android:layout_height="wrap_content"
314 android:tag="Tag Me" android:inputType="numberDecimal">
317 <LinearLayout android:layout_width="match_parent"
318 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
319 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
320 android:layout_height="wrap_content" android:text="Send"></Button>
321 </LinearLayout>""",False)
325 settings_layout = make_layout(""" <ListView
326 android:id="@+id/myListView"
327 android:layout_width="match_parent"
328 android:layout_height="wrap_content" />""")
332 def get_history_values(n):
334 h = wallet.get_tx_history()
335 length = min(n, len(h))
336 for i in range(length):
337 tx_hash, conf, is_mine, value, fee, balance, timestamp = h[-i-1]
339 dt = datetime.datetime.fromtimestamp( timestamp )
340 if dt.date() == dt.today().date():
341 time_str = str( dt.time() )
343 time_str = str( dt.date() )
347 conf_str = 'v' if conf else 'o'
348 label, is_default_label = wallet.get_label(tx_hash)
349 values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value,True), ' ' + label ))
354 def get_history_layout(n):
357 values = get_history_values(n)
360 color = "#ff00ff00" if a == 'v' else "#ffff0000"
364 android:id="@+id/hl_%d_col1"
365 android:layout_column="0"
367 android:textColor="%s"
368 android:padding="3" />
370 android:id="@+id/hl_%d_col2"
371 android:layout_column="1"
373 android:padding="3" />
375 android:id="@+id/hl_%d_col3"
376 android:layout_column="2"
378 android:padding="3" />
380 android:id="@+id/hl_%d_col4"
381 android:layout_column="3"
383 android:padding="4" />
384 </TableRow>"""%(i,a,color,i,b,i,c,i,d)
388 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
389 android:layout_width="fill_parent"
390 android:layout_height="wrap_content"
391 android:stretchColumns="0,1,2,3">
393 </TableLayout>"""% rows
397 def set_history_layout(n):
398 values = get_history_values(n)
402 droid.fullSetProperty("hl_%d_col1"%i,"text", a)
405 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
407 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
409 droid.fullSetProperty("hl_%d_col2"%i,"text", b)
410 droid.fullSetProperty("hl_%d_col3"%i,"text", c)
411 droid.fullSetProperty("hl_%d_col4"%i,"text", d)
420 if not network.is_connected():
421 text = "Not connected..."
422 elif not wallet.up_to_date:
423 text = "Synchronizing..."
425 c, u = wallet.get_balance()
426 text = "Balance:"+format_satoshis(c)
427 if u : text += ' [' + format_satoshis(u,True).strip() + ']'
430 # vibrate if status changed
431 if text != status_text:
432 if status_text and network.is_connected() and wallet.up_to_date:
436 droid.fullSetProperty("balanceTextView", "text", status_text)
438 if wallet.up_to_date:
439 set_history_layout(15)
444 def pay_to(recipient, amount, fee, label):
446 if wallet.use_encryption:
447 password = droid.dialogGetPassword('Password').result
448 if not password: return
452 droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
456 tx = wallet.mktx( [(recipient, amount)], password, fee)
457 except BaseException, e:
458 modal_dialog('error', e.message)
459 droid.dialogDismiss()
463 wallet.labels[tx.hash()] = label
465 droid.dialogDismiss()
467 r, h = wallet.sendtx( tx )
469 modal_dialog('Payment sent', h)
472 modal_dialog('Error', h)
480 def make_new_contact():
481 code = droid.scanBarcode()
484 data = r['extras']['SCAN_RESULT']
486 if re.match('^bitcoin:', data):
487 address, _, _, _, _, _, _ = util.parse_url(data)
493 if modal_question('Add to contacts?', address):
494 wallet.add_contact(address)
496 modal_dialog('Invalid address', data)
501 def update_callback():
503 print "gui callback", network.is_connected()
505 droid.eventPost("refresh",'z')
515 event = droid.eventWait(1000).result
522 print "got event in main loop", repr(event)
523 if event == 'OK': continue
524 if event is None: continue
525 if not event.get("name"): continue
527 # request 2 taps before we exit
528 if event["name"]=="key":
529 if event["data"]["key"] == '4':
534 else: quitting = False
536 if event["name"]=="click":
537 id=event["data"]["id"]
539 elif event["name"]=="settings":
542 elif event["name"] in menu_commands:
545 if out == 'contacts':
547 contact_addr = select_from_contacts()
548 if contact_addr == 'newcontact':
554 elif out == "receive":
556 receive_addr = select_from_addresses()
558 amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal")
560 receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount)
572 droid.fullSetProperty("recipient","text",recipient)
577 event = droid.eventWait().result
578 if not event: continue
579 print "got event in payto loop", event
580 if event == 'OK': continue
581 if not event.get("name"): continue
583 if event["name"] == "click":
584 id = event["data"]["id"]
589 recipient = droid.fullQueryDetail("recipient").result.get('text')
590 label = droid.fullQueryDetail("label").result.get('text')
591 amount = droid.fullQueryDetail('amount').result.get('text')
593 if not is_valid(recipient):
594 modal_dialog('Error','Invalid Bitcoin address')
598 amount = int( 100000000 * Decimal(amount) )
600 modal_dialog('Error','Invalid amount')
603 result = pay_to(recipient, amount, wallet.fee, label)
607 elif id=="buttonContacts":
608 addr = select_from_contacts()
609 droid.fullSetProperty("recipient","text",addr)
612 code = droid.scanBarcode()
615 data = r['extras']['SCAN_RESULT']
617 if re.match('^bitcoin:', data):
618 payto, amount, label, _, _, _, _ = util.parse_url(data)
619 droid.fullSetProperty("recipient", "text",payto)
620 droid.fullSetProperty("amount", "text", amount)
621 droid.fullSetProperty("label", "text", label)
623 droid.fullSetProperty("recipient", "text", data)
626 elif event["name"] in menu_commands:
629 elif event["name"]=="key":
630 if event["data"]["key"] == '4':
633 #elif event["name"]=="screen":
634 # if event["data"]=="destroy":
647 event = droid.eventWait().result
648 print "got event", event
649 if event["name"]=="key":
650 if event["data"]["key"] == '4':
653 elif event["name"]=="clipboard":
654 droid.setClipboard(receive_addr)
655 modal_dialog('Address copied to clipboard',receive_addr)
657 elif event["name"]=="edit":
658 edit_label(receive_addr)
666 event = droid.eventWait().result
667 print "got event", event
668 if event["name"]=="key":
669 if event["data"]["key"] == '4':
672 elif event["name"]=="clipboard":
673 droid.setClipboard(contact_addr)
674 modal_dialog('Address copied to clipboard',contact_addr)
676 elif event["name"]=="edit":
677 edit_label(contact_addr)
679 elif event["name"]=="paytocontact":
680 recipient = contact_addr
683 elif event["name"]=="deletecontact":
684 if modal_question('delete contact', contact_addr):
690 def server_dialog(servers):
691 droid.dialogCreateAlert("Public servers")
692 droid.dialogSetItems( servers.keys() )
693 droid.dialogSetPositiveButtonText('Private server')
695 response = droid.dialogGetResponse().result
696 droid.dialogDismiss()
697 if not response: return
699 if response.get('which') == 'positive':
700 return modal_input('Private server', None)
702 i = response.get('item')
704 response = servers.keys()[i]
709 if wallet.use_encryption:
710 password = droid.dialogGetPassword('Seed').result
711 if not password: return
716 seed = wallet.decode_seed(password)
718 modal_dialog('error','incorrect password')
721 modal_dialog('Your seed is',seed)
722 modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(seed)) )
724 def change_password_dialog():
725 if wallet.use_encryption:
726 password = droid.dialogGetPassword('Your wallet is encrypted').result
727 if password is None: return
732 seed = wallet.decode_seed(password)
734 modal_dialog('error','incorrect password')
737 new_password = droid.dialogGetPassword('Choose a password').result
738 if new_password == None:
741 if new_password != '':
742 password2 = droid.dialogGetPassword('Confirm new password').result
743 if new_password != password2:
744 modal_dialog('error','passwords do not match')
747 wallet.update_password(seed, password, new_password)
749 modal_dialog('Password updated','your wallet is encrypted')
751 modal_dialog('No password','your wallet is not encrypted')
759 server, port, p = network.default_server.split(':')
760 fee = str( Decimal( wallet.fee)/100000000 )
761 is_encrypted = 'yes' if wallet.use_encryption else 'no'
762 protocol = protocol_name(p)
763 droid.fullShow(settings_layout)
764 droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
770 event = droid.eventWait()
772 print "got event", event
773 if event == 'OK': continue
774 if not event: continue
776 servers = network.get_servers()
777 name = event.get("name")
778 if not name: continue
780 if name == "itemclick":
781 pos = event["data"]["position"]
782 host, port, protocol = network.default_server.split(':')
783 network_changed = False
785 if pos == "0": #server
786 host = server_dialog(servers)
790 network_changed = True
792 elif pos == "1": #protocol
794 protocol = protocol_dialog(host, protocol, servers[host])
797 network_changed = True
799 elif pos == "2": #port
800 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
803 network_changed = True
805 elif pos == "3": #fee
806 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")
809 fee = int( 100000000 * Decimal(fee) )
811 modal_dialog('error','invalid fee value')
816 if change_password_dialog():
826 network.set_parameters(host, port, protocol, proxy, auto_connect)
828 modal_dialog('error','invalid server')
831 elif name in menu_commands:
834 elif name == 'cancel':
838 if event["data"]["key"] == '4':
844 droid.clearOptionsMenu()
846 droid.addOptionsMenuItem("Send","send",None,"")
847 droid.addOptionsMenuItem("Receive","receive",None,"")
848 droid.addOptionsMenuItem("Contacts","contacts",None,"")
849 droid.addOptionsMenuItem("Settings","settings",None,"")
851 droid.addOptionsMenuItem("Copy","clipboard",None,"")
852 droid.addOptionsMenuItem("Label","edit",None,"")
853 elif s == 'contacts':
854 droid.addOptionsMenuItem("Copy","clipboard",None,"")
855 droid.addOptionsMenuItem("Label","edit",None,"")
856 droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
857 #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
860 def make_bitmap(addr):
861 # fixme: this is highly inefficient
862 droid.dialogCreateSpinnerProgress("please wait")
865 import pyqrnative, bmp
866 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
869 k = qr.getModuleCount()
871 bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
873 droid.dialogDismiss()
878 droid = android.Android()
879 menu_commands = ["send", "receive", "settings", "contacts", "main"]
885 def __init__(self, config, _network):
886 global wallet, network
888 network.register_callback('updated', update_callback)
889 network.register_callback('connected', update_callback)
890 network.register_callback('disconnected', update_callback)
891 network.register_callback('disconnecting', update_callback)
893 storage = WalletStorage(config)
894 if not storage.file_exists:
895 action = self.restore_or_create()
896 if not action: exit()
898 wallet = Wallet(storage)
899 if action == 'create':
900 wallet.init_seed(None)
903 wallet.create_accounts()
904 wallet.synchronize() # generate first addresses offline
906 elif action == 'restore':
907 seed = self.seed_dialog()
910 wallet.init_seed(str(seed))
915 wallet.start_threads(network)
917 if action == 'restore':
918 if not self.restore_wallet():
921 self.password_dialog(wallet)
924 wallet = Wallet(storage)
925 wallet.start_threads(network)
933 droid.fullShow(main_layout())
937 droid.fullShow(payto_layout)
941 make_bitmap(receive_addr)
942 droid.fullShow(qr_layout(receive_addr))
945 elif s == 'contacts':
946 make_bitmap(contact_addr)
947 droid.fullShow(qr_layout(contact_addr))
950 elif s == 'settings':
956 droid.makeToast("Bye!")
959 def restore_or_create(self):
960 droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
961 droid.dialogSetPositiveButtonText('Create')
962 droid.dialogSetNeutralButtonText('Restore')
963 droid.dialogSetNegativeButtonText('Cancel')
965 response = droid.dialogGetResponse().result
966 droid.dialogDismiss()
967 if not response: return
968 if response.get('which') == 'negative':
971 return 'restore' if response.get('which') == 'neutral' else 'create'
974 def seed_dialog(self):
975 if modal_question("Enter your seed","Input method",'QR Code', 'mnemonic'):
976 code = droid.scanBarcode()
979 seed = r['extras']['SCAN_RESULT']
983 m = modal_input('Mnemonic','please enter your code')
985 seed = mnemonic_decode(m.split(' '))
987 modal_dialog('error: could not decode this seed')
993 def network_dialog(self):
996 def verify_seed(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?"):