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/>.
23 from interface import WalletSynchronizer
24 from wallet import Wallet
25 from wallet import format_satoshis
26 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().result
47 if response.get('which') == 'positive':
48 return response.get('value')
50 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
51 droid.dialogCreateAlert(q, msg)
52 droid.dialogSetPositiveButtonText(pos_text)
53 droid.dialogSetNegativeButtonText(neg_text)
55 response = droid.dialogGetResponse().result
57 return response.get('which') == 'positive'
60 v = modal_input('Edit label',None,wallet.labels.get(addr))
63 wallet.labels[addr] = v
65 if addr in wallet.labels.keys():
66 wallet.labels.pop(addr)
67 wallet.update_tx_history()
69 droid.fullSetProperty("labelTextView", "text", v)
71 def select_from_contacts():
73 droid.dialogCreateAlert(title)
75 for i in range(len(wallet.addressbook)):
76 addr = wallet.addressbook[i]
77 label = wallet.labels.get(addr,addr)
79 droid.dialogSetItems(l)
80 droid.dialogSetPositiveButtonText('New contact')
82 response = droid.dialogGetResponse().result
85 if response.get('which') == 'positive':
88 result = response.get('item')
90 if result is not None:
91 addr = wallet.addressbook[result]
95 def select_from_addresses():
96 droid.dialogCreateAlert("Addresses:")
98 for i in range(len(wallet.addresses)):
99 addr = wallet.addresses[i]
100 label = wallet.labels.get(addr,addr)
102 droid.dialogSetItems(l)
104 response = droid.dialogGetResponse()
105 result = response.result.get('item')
106 droid.dialogDismiss()
107 if result is not None:
108 addr = wallet.addresses[result]
112 def protocol_name(p):
113 if p == 't': return 'TCP/stratum'
114 if p == 'h': return 'HTTP/Stratum'
115 if p == 'n': return 'TCP/native'
117 def protocol_dialog(host, protocol, z):
118 droid.dialogCreateAlert('Protocol',host)
122 protocols = ['t','h','n']
124 current = protocols.index(protocol)
126 l.append(protocol_name(p))
127 droid.dialogSetSingleChoiceItems(l, current)
128 droid.dialogSetPositiveButtonText('OK')
129 droid.dialogSetNegativeButtonText('Cancel')
131 response = droid.dialogGetResponse().result
132 if not response: return
133 if response.get('which') == 'positive':
134 response = droid.dialogGetSelectedItems().result[0]
135 droid.dialogDismiss()
136 p = protocols[response]
138 return host + ':' + port + ':' + p
143 def make_layout(s, scrollable = False):
148 android:layout_width="match_parent"
149 android:layout_height="wrap_content"
150 android:background="#ff222222">
153 android:id="@+id/textElectrum"
154 android:text="Electrum"
155 android:textSize="7pt"
156 android:textColor="#ff4444ff"
157 android:gravity="left"
158 android:layout_height="wrap_content"
159 android:layout_width="match_parent"
168 android:id="@+id/scrollview"
169 android:layout_width="match_parent"
170 android:layout_height="match_parent" >
173 android:orientation="vertical"
174 android:layout_width="match_parent"
175 android:layout_height="wrap_content" >
184 return """<?xml version="1.0" encoding="utf-8"?>
185 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
186 android:id="@+id/background"
187 android:orientation="vertical"
188 android:layout_width="match_parent"
189 android:layout_height="match_parent"
190 android:background="#ff000022">
193 </LinearLayout>"""%content
199 return make_layout("""
200 <TextView android:id="@+id/balanceTextView"
201 android:layout_width="match_parent"
203 android:textColor="#ffffffff"
204 android:textAppearance="?android:attr/textAppearanceLarge"
205 android:padding="7dip"
206 android:textSize="8pt"
207 android:gravity="center_vertical|center_horizontal|left">
210 <TextView android:id="@+id/historyTextView"
211 android:layout_width="match_parent"
212 android:layout_height="wrap_content"
213 android:text="Recent transactions"
214 android:textAppearance="?android:attr/textAppearanceLarge"
215 android:gravity="center_vertical|center_horizontal|center">
218 %s """%get_history_layout(15),True)
223 return make_layout("""
225 <TextView android:id="@+id/addrTextView"
226 android:layout_width="match_parent"
227 android:layout_height="50"
229 android:textAppearance="?android:attr/textAppearanceLarge"
230 android:gravity="center_vertical|center_horizontal|center">
234 android:id="@+id/qrView"
235 android:gravity="center"
236 android:layout_width="match_parent"
237 android:layout_height="350"
238 android:antialias="false"
239 android:src="file:///sdcard/sl4a/qrcode.bmp" />
241 <TextView android:id="@+id/labelTextView"
242 android:layout_width="match_parent"
243 android:layout_height="50"
245 android:textAppearance="?android:attr/textAppearanceLarge"
246 android:gravity="center_vertical|center_horizontal|center">
249 """%(addr,wallet.labels.get(addr,'')), True)
251 payto_layout = make_layout("""
253 <TextView android:id="@+id/recipientTextView"
254 android:layout_width="match_parent"
255 android:layout_height="wrap_content"
256 android:text="Pay to:"
257 android:textAppearance="?android:attr/textAppearanceLarge"
258 android:gravity="left">
262 <EditText android:id="@+id/recipient"
263 android:layout_width="match_parent"
264 android:layout_height="wrap_content"
265 android:tag="Tag Me" android:inputType="text">
268 <LinearLayout android:id="@+id/linearLayout1"
269 android:layout_width="match_parent"
270 android:layout_height="wrap_content">
271 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
272 android:layout_height="wrap_content" android:text="From QR code"></Button>
273 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
274 android:layout_height="wrap_content" android:text="From Contacts"></Button>
278 <TextView android:id="@+id/labelTextView"
279 android:layout_width="match_parent"
280 android:layout_height="wrap_content"
281 android:text="Description:"
282 android:textAppearance="?android:attr/textAppearanceLarge"
283 android:gravity="left">
286 <EditText android:id="@+id/label"
287 android:layout_width="match_parent"
288 android:layout_height="wrap_content"
289 android:tag="Tag Me" android:inputType="text">
292 <TextView android:id="@+id/amountLabelTextView"
293 android:layout_width="match_parent"
294 android:layout_height="wrap_content"
295 android:text="Amount:"
296 android:textAppearance="?android:attr/textAppearanceLarge"
297 android:gravity="left">
300 <EditText android:id="@+id/amount"
301 android:layout_width="match_parent"
302 android:layout_height="wrap_content"
303 android:tag="Tag Me" android:inputType="numberDecimal">
306 <LinearLayout android:layout_width="match_parent"
307 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
308 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
309 android:layout_height="wrap_content" android:text="Send"></Button>
310 </LinearLayout>""",False)
314 settings_layout = make_layout(""" <ListView
315 android:id="@+id/myListView"
316 android:layout_width="match_parent"
317 android:layout_height="wrap_content" />""")
321 def get_history_values(n):
323 h = wallet.get_tx_history()
325 length = min(n, len(h))
326 for i in range(length):
330 dt = datetime.datetime.fromtimestamp( line['timestamp'] )
331 if dt.date() == dt.today().date():
332 time_str = str( dt.time() )
334 time_str = str( dt.date() )
338 print line['timestamp']
342 tx_hash = line['tx_hash']
343 label = wallet.labels.get(tx_hash)
344 is_default_label = (label == '') or (label is None)
345 if is_default_label: label = line['default_label']
346 values.append((conf, ' ' + time_str, ' ' + format_satoshis(v,True), ' ' + label ))
351 def get_history_layout(n):
354 values = get_history_values(n)
357 color = "#ff00ff00" if a == 'v' else "#ffff0000"
361 android:id="@+id/hl_%d_col1"
362 android:layout_column="0"
364 android:textColor="%s"
365 android:padding="3" />
367 android:id="@+id/hl_%d_col2"
368 android:layout_column="1"
370 android:padding="3" />
372 android:id="@+id/hl_%d_col3"
373 android:layout_column="2"
375 android:padding="3" />
377 android:id="@+id/hl_%d_col4"
378 android:layout_column="3"
380 android:padding="4" />
381 </TableRow>"""%(i,a,color,i,b,i,c,i,d)
385 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
386 android:layout_width="fill_parent"
387 android:layout_height="wrap_content"
388 android:stretchColumns="0,1,2,3">
390 </TableLayout>"""% rows
394 def set_history_layout(n):
395 values = get_history_values(n)
399 droid.fullSetProperty("hl_%d_col1"%i,"text", a)
402 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
404 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
406 droid.fullSetProperty("hl_%d_col2"%i,"text", b)
407 droid.fullSetProperty("hl_%d_col3"%i,"text", c)
408 droid.fullSetProperty("hl_%d_col4"%i,"text", d)
417 if not wallet.interface.is_connected:
418 text = "Not connected..."
419 elif wallet.blocks == 0:
420 text = "Server not ready"
421 elif not wallet.up_to_date:
422 text = "Synchronizing..."
424 c, u = wallet.get_balance()
425 text = "Balance:"+format_satoshis(c)
426 if u : text += ' [' + format_satoshis(u,True).strip() + ']'
429 # vibrate if status changed
430 if text != status_text:
431 if status_text and wallet.interface.is_connected and wallet.up_to_date:
435 droid.fullSetProperty("balanceTextView", "text", status_text)
437 if wallet.up_to_date:
438 set_history_layout(15)
443 def pay_to(recipient, amount, fee, label):
445 if wallet.use_encryption:
446 password = droid.dialogGetPassword('Password').result
447 if not password: return
451 droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
455 tx = wallet.mktx( recipient, amount, label, password, fee)
456 except BaseException, e:
457 modal_dialog('error', e.message)
458 droid.dialogDismiss()
461 droid.dialogDismiss()
463 r, h = wallet.sendtx( tx )
465 modal_dialog('Payment sent', h)
468 modal_dialog('Error', h)
476 droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
477 droid.dialogSetPositiveButtonText('Create')
478 droid.dialogSetNeutralButtonText('Restore')
479 droid.dialogSetNegativeButtonText('Cancel')
481 response = droid.dialogGetResponse().result
482 droid.dialogDismiss()
483 if response.get('which') == 'negative':
486 is_recovery = response.get('which') == 'neutral'
489 wallet.new_seed(None)
491 if modal_question("Input method",None,'QR Code', 'mnemonic'):
492 code = droid.scanBarcode()
495 seed = r['extras']['SCAN_RESULT']
499 m = modal_input('Mnemonic','please enter your code')
501 seed = mnemonic.mn_decode(m.split(' '))
503 modal_dialog('error: could not decode this seed')
506 wallet.seed = str(seed)
508 modal_dialog('Your seed is:', wallet.seed)
509 modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(wallet.seed)) )
511 msg = "recovering wallet..." if is_recovery else "creating wallet..."
512 droid.dialogCreateSpinnerProgress("Electrum", msg)
515 wallet.init_mpk( wallet.seed )
516 WalletSynchronizer(wallet,True).start()
519 droid.dialogDismiss()
523 if wallet.is_found():
524 wallet.update_tx_history()
525 wallet.fill_addressbook()
526 modal_dialog("recovery successful")
528 if not modal_question("no transactions found for this seed","do you want to keep this wallet?"):
531 change_password_dialog()
536 def make_new_contact():
537 code = droid.scanBarcode()
540 address = r['extras']['SCAN_RESULT']
542 if wallet.is_valid(address):
543 if modal_question('Add to contacts?', address):
544 wallet.addressbook.append(address)
547 modal_dialog('Invalid address', address)
552 def update_callback():
554 print "gui callback", wallet.interface.is_connected, wallet.up_to_date
556 droid.eventPost("refresh",'z')
566 event = droid.eventWait(1000).result
573 print "got event in main loop", repr(event)
574 if event == 'OK': continue
575 if event is None: continue
576 #if event["name"]=="refresh":
579 # request 2 taps before we exit
580 if event["name"]=="key":
581 if event["data"]["key"] == '4':
586 else: quitting = False
588 if event["name"]=="click":
589 id=event["data"]["id"]
591 elif event["name"]=="settings":
594 elif event["name"] in menu_commands:
597 if out == 'contacts':
599 contact_addr = select_from_contacts()
600 if contact_addr == 'newcontact':
606 elif out == "receive":
608 receive_addr = select_from_addresses()
619 droid.fullSetProperty("recipient","text",recipient)
624 event = droid.eventWait().result
625 print "got event in payto loop", event
627 if event["name"] == "click":
628 id = event["data"]["id"]
633 recipient = droid.fullQueryDetail("recipient").result.get('text')
634 label = droid.fullQueryDetail("label").result.get('text')
635 amount = droid.fullQueryDetail('amount').result.get('text')
637 if not wallet.is_valid(recipient):
638 modal_dialog('Error','Invalid Bitcoin address')
642 amount = int( 100000000 * Decimal(amount) )
644 modal_dialog('Error','Invalid amount')
647 result = pay_to(recipient, amount, wallet.fee, label)
651 elif id=="buttonContacts":
652 addr = select_from_contacts()
653 droid.fullSetProperty("recipient","text",addr)
656 code = droid.scanBarcode()
659 addr = r['extras']['SCAN_RESULT']
661 droid.fullSetProperty("recipient","text",addr)
663 elif event["name"] in menu_commands:
666 elif event["name"]=="key":
667 if event["data"]["key"] == '4':
670 #elif event["name"]=="screen":
671 # if event["data"]=="destroy":
684 event = droid.eventWait().result
685 print "got event", event
686 if event["name"]=="key":
687 if event["data"]["key"] == '4':
690 elif event["name"]=="clipboard":
691 droid.setClipboard(receive_addr)
692 modal_dialog('Address copied to clipboard',receive_addr)
694 elif event["name"]=="edit":
695 edit_label(receive_addr)
703 event = droid.eventWait().result
704 print "got event", event
705 if event["name"]=="key":
706 if event["data"]["key"] == '4':
709 elif event["name"]=="clipboard":
710 droid.setClipboard(contact_addr)
711 modal_dialog('Address copied to clipboard',contact_addr)
713 elif event["name"]=="edit":
714 edit_label(contact_addr)
716 elif event["name"]=="paytocontact":
717 recipient = contact_addr
720 elif event["name"]=="deletecontact":
721 if modal_question('delete contact', contact_addr):
727 def server_dialog(plist):
728 droid.dialogCreateAlert("Public servers")
729 droid.dialogSetItems( plist.keys() )
730 droid.dialogSetPositiveButtonText('Private server')
732 response = droid.dialogGetResponse().result
733 droid.dialogDismiss()
735 if response.get('which') == 'positive':
736 return modal_input('Private server', None)
738 i = response.get('item')
740 response = plist.keys()[i]
745 if wallet.use_encryption:
746 password = droid.dialogGetPassword('Seed').result
747 if not password: return
752 seed = wallet.pw_decode( wallet.seed, password)
754 modal_dialog('error','incorrect password')
757 modal_dialog('Your seed is',seed)
758 modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(seed)) )
760 def change_password_dialog():
761 if wallet.use_encryption:
762 password = droid.dialogGetPassword('Your wallet is encrypted').result
763 if password is None: return
768 seed = wallet.pw_decode( wallet.seed, password)
770 modal_dialog('error','incorrect password')
773 new_password = droid.dialogGetPassword('Choose a password').result
774 if new_password == None:
777 if new_password != '':
778 password2 = droid.dialogGetPassword('Confirm new password').result
779 if new_password != password2:
780 modal_dialog('error','passwords do not match')
783 wallet.update_password(seed, new_password)
785 modal_dialog('Password updated','your wallet is encrypted')
787 modal_dialog('No password','your wallet is not encrypted')
795 server, port, p = wallet.server.split(':')
796 fee = str( Decimal( wallet.fee)/100000000 )
797 is_encrypted = 'yes' if wallet.use_encryption else 'no'
798 protocol = protocol_name(p)
799 droid.fullShow(settings_layout)
800 droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
806 event = droid.eventWait().result
807 print "got event", event
808 if event == 'OK': continue
809 if not event: continue
812 for item in wallet.interface.servers:
816 protocol, port = item2
820 if event["name"] == "itemclick":
821 pos = event["data"]["position"]
822 host, port, protocol = wallet.server.split(':')
824 if pos == "0": #server
825 host = server_dialog(plist)
829 srv = host + ':' + port + ':t'
831 wallet.set_server(srv)
833 modal_dialog('error','invalid server')
836 elif pos == "1": #protocol
838 srv = protocol_dialog(host, protocol, plist[host])
841 wallet.set_server(srv)
843 modal_dialog('error','invalid server')
846 elif pos == "2": #port
847 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
850 srv = host + ':' + a_port + ':'+ protocol
852 wallet.set_server(srv)
854 modal_dialog('error','invalid port number')
857 elif pos == "3": #fee
858 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")
861 fee = int( 100000000 * Decimal(fee) )
863 modal_dialog('error','invalid fee value')
864 if wallet.fee != fee:
870 if change_password_dialog():
877 elif event["name"] in menu_commands:
880 elif event["name"] == 'cancel':
883 elif event["name"] == "key":
884 if event["data"]["key"] == '4':
892 menu_commands = ["send", "receive", "settings", "contacts", "main"]
893 droid = android.Android()
894 wallet = Wallet(update_callback)
896 wallet.set_path("/sdcard/electrum.dat")
898 if not wallet.file_exists:
901 WalletSynchronizer(wallet,True).start()
907 droid.clearOptionsMenu()
909 droid.addOptionsMenuItem("Send","send",None,"")
910 droid.addOptionsMenuItem("Receive","receive",None,"")
911 droid.addOptionsMenuItem("Contacts","contacts",None,"")
912 droid.addOptionsMenuItem("Settings","settings",None,"")
914 droid.addOptionsMenuItem("Copy","clipboard",None,"")
915 droid.addOptionsMenuItem("Label","edit",None,"")
916 elif s == 'contacts':
917 droid.addOptionsMenuItem("Copy","clipboard",None,"")
918 droid.addOptionsMenuItem("Label","edit",None,"")
919 droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
920 #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
922 def make_bitmap(addr):
923 # fixme: this is highly inefficient
924 droid.dialogCreateSpinnerProgress("please wait")
926 import pyqrnative, bmp
927 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
930 k = qr.getModuleCount()
931 bitmap = bmp.BitMap( 35*8, 35*8 )
932 print len(bitmap.bitarray)
937 tmparray = [ 0 ] * 35*8
941 if qr.isDark(r-1, c):
942 tmparray[ (1+c)*8:(2+c)*8] = [1]*8
945 bitmap.bitarray.append( tmparray[:] )
947 bitmap.saveFile( "/sdcard/sl4a/qrcode.bmp" )
948 droid.dialogDismiss()
955 droid.fullShow(main_layout())
960 droid.fullShow(payto_layout)
965 make_bitmap(receive_addr)
966 droid.fullShow(qr_layout(receive_addr))
969 elif s == 'contacts':
970 make_bitmap(contact_addr)
971 droid.fullShow(qr_layout(contact_addr))
974 elif s == 'settings':
975 #droid.fullShow(settings_layout)
981 droid.makeToast("Bye!")