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/>.
24 from interface import WalletSynchronizer
25 from wallet import Wallet, format_satoshis
27 from decimal import Decimal
32 def modal_dialog(title, msg = None):
33 droid.dialogCreateAlert(title,msg)
34 droid.dialogSetPositiveButtonText('OK')
36 droid.dialogGetResponse()
39 def modal_input(title, msg, value = None, etype=None):
40 droid.dialogCreateInput(title, msg, value, etype)
41 droid.dialogSetPositiveButtonText('OK')
42 droid.dialogSetNegativeButtonText('Cancel')
44 response = droid.dialogGetResponse().result
46 if response.get('which') == 'positive':
47 return response.get('value')
49 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
50 droid.dialogCreateAlert(q, msg)
51 droid.dialogSetPositiveButtonText(pos_text)
52 droid.dialogSetNegativeButtonText(neg_text)
54 response = droid.dialogGetResponse().result
56 return response.get('which') == 'positive'
59 v = modal_input('Edit label',None,wallet.labels.get(addr))
62 wallet.labels[addr] = v
64 if addr in wallet.labels.keys():
65 wallet.labels.pop(addr)
66 wallet.update_tx_history()
68 droid.fullSetProperty("labelTextView", "text", v)
70 def select_from_contacts():
72 droid.dialogCreateAlert(title)
74 for i in range(len(wallet.addressbook)):
75 addr = wallet.addressbook[i]
76 label = wallet.labels.get(addr,addr)
78 droid.dialogSetItems(l)
79 droid.dialogSetPositiveButtonText('New contact')
81 response = droid.dialogGetResponse().result
84 if response.get('which') == 'positive':
87 result = response.get('item')
89 if result is not None:
90 addr = wallet.addressbook[result]
94 def select_from_addresses():
95 droid.dialogCreateAlert("Addresses:")
97 for i in range(len(wallet.addresses)):
98 addr = wallet.addresses[i]
99 label = wallet.labels.get(addr,addr)
101 droid.dialogSetItems(l)
103 response = droid.dialogGetResponse()
104 result = response.result.get('item')
105 droid.dialogDismiss()
106 if result is not None:
107 addr = wallet.addresses[result]
111 def protocol_name(p):
112 if p == 't': return 'TCP/stratum'
113 if p == 'h': return 'HTTP/Stratum'
114 if p == 'n': return 'TCP/native'
116 def protocol_dialog(host, protocol, z):
117 droid.dialogCreateAlert('Protocol',host)
121 protocols = ['t','h','n']
123 current = protocols.index(protocol)
125 l.append(protocol_name(p))
126 droid.dialogSetSingleChoiceItems(l, current)
127 droid.dialogSetPositiveButtonText('OK')
128 droid.dialogSetNegativeButtonText('Cancel')
130 response = droid.dialogGetResponse().result
131 if not response: return
132 if response.get('which') == 'positive':
133 response = droid.dialogGetSelectedItems().result[0]
134 droid.dialogDismiss()
135 p = protocols[response]
137 return host + ':' + port + ':' + p
142 def make_layout(s, scrollable = False):
147 android:layout_width="match_parent"
148 android:layout_height="wrap_content"
149 android:background="#ff222222">
152 android:id="@+id/textElectrum"
153 android:text="Electrum"
154 android:textSize="7pt"
155 android:textColor="#ff4444ff"
156 android:gravity="left"
157 android:layout_height="wrap_content"
158 android:layout_width="match_parent"
167 android:id="@+id/scrollview"
168 android:layout_width="match_parent"
169 android:layout_height="match_parent" >
172 android:orientation="vertical"
173 android:layout_width="match_parent"
174 android:layout_height="wrap_content" >
183 return """<?xml version="1.0" encoding="utf-8"?>
184 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
185 android:id="@+id/background"
186 android:orientation="vertical"
187 android:layout_width="match_parent"
188 android:layout_height="match_parent"
189 android:background="#ff000022">
192 </LinearLayout>"""%content
198 return make_layout("""
199 <TextView android:id="@+id/balanceTextView"
200 android:layout_width="match_parent"
202 android:textColor="#ffffffff"
203 android:textAppearance="?android:attr/textAppearanceLarge"
204 android:padding="7dip"
205 android:textSize="8pt"
206 android:gravity="center_vertical|center_horizontal|left">
209 <TextView android:id="@+id/historyTextView"
210 android:layout_width="match_parent"
211 android:layout_height="wrap_content"
212 android:text="Recent transactions"
213 android:textAppearance="?android:attr/textAppearanceLarge"
214 android:gravity="center_vertical|center_horizontal|center">
217 %s """%get_history_layout(15),True)
222 return make_layout("""
224 <TextView android:id="@+id/addrTextView"
225 android:layout_width="match_parent"
226 android:layout_height="50"
228 android:textAppearance="?android:attr/textAppearanceLarge"
229 android:gravity="center_vertical|center_horizontal|center">
233 android:id="@+id/qrView"
234 android:gravity="center"
235 android:layout_width="match_parent"
236 android:layout_height="350"
237 android:antialias="false"
238 android:src="file:///sdcard/sl4a/qrcode.bmp" />
240 <TextView android:id="@+id/labelTextView"
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">
248 """%(addr,wallet.labels.get(addr,'')), True)
250 payto_layout = make_layout("""
252 <TextView android:id="@+id/recipientTextView"
253 android:layout_width="match_parent"
254 android:layout_height="wrap_content"
255 android:text="Pay to:"
256 android:textAppearance="?android:attr/textAppearanceLarge"
257 android:gravity="left">
261 <EditText android:id="@+id/recipient"
262 android:layout_width="match_parent"
263 android:layout_height="wrap_content"
264 android:tag="Tag Me" android:inputType="text">
267 <LinearLayout android:id="@+id/linearLayout1"
268 android:layout_width="match_parent"
269 android:layout_height="wrap_content">
270 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
271 android:layout_height="wrap_content" android:text="From QR code"></Button>
272 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
273 android:layout_height="wrap_content" android:text="From Contacts"></Button>
277 <TextView android:id="@+id/labelTextView"
278 android:layout_width="match_parent"
279 android:layout_height="wrap_content"
280 android:text="Description:"
281 android:textAppearance="?android:attr/textAppearanceLarge"
282 android:gravity="left">
285 <EditText android:id="@+id/label"
286 android:layout_width="match_parent"
287 android:layout_height="wrap_content"
288 android:tag="Tag Me" android:inputType="text">
291 <TextView android:id="@+id/amountLabelTextView"
292 android:layout_width="match_parent"
293 android:layout_height="wrap_content"
294 android:text="Amount:"
295 android:textAppearance="?android:attr/textAppearanceLarge"
296 android:gravity="left">
299 <EditText android:id="@+id/amount"
300 android:layout_width="match_parent"
301 android:layout_height="wrap_content"
302 android:tag="Tag Me" android:inputType="numberDecimal">
305 <LinearLayout android:layout_width="match_parent"
306 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
307 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
308 android:layout_height="wrap_content" android:text="Send"></Button>
309 </LinearLayout>""",False)
313 settings_layout = make_layout(""" <ListView
314 android:id="@+id/myListView"
315 android:layout_width="match_parent"
316 android:layout_height="wrap_content" />""")
320 def get_history_values(n):
322 h = wallet.get_tx_history()
324 length = min(n, len(h))
325 for i in range(length):
329 dt = datetime.datetime.fromtimestamp( line['timestamp'] )
330 if dt.date() == dt.today().date():
331 time_str = str( dt.time() )
333 time_str = str( dt.date() )
337 print line['timestamp']
341 tx_hash = line['tx_hash']
342 label = wallet.labels.get(tx_hash)
343 is_default_label = (label == '') or (label is None)
344 if is_default_label: label = line['default_label']
345 values.append((conf, ' ' + time_str, ' ' + format_satoshis(v,True), ' ' + label ))
350 def get_history_layout(n):
353 values = get_history_values(n)
356 color = "#ff00ff00" if a == 'v' else "#ffff0000"
360 android:id="@+id/hl_%d_col1"
361 android:layout_column="0"
363 android:textColor="%s"
364 android:padding="3" />
366 android:id="@+id/hl_%d_col2"
367 android:layout_column="1"
369 android:padding="3" />
371 android:id="@+id/hl_%d_col3"
372 android:layout_column="2"
374 android:padding="3" />
376 android:id="@+id/hl_%d_col4"
377 android:layout_column="3"
379 android:padding="4" />
380 </TableRow>"""%(i,a,color,i,b,i,c,i,d)
384 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
385 android:layout_width="fill_parent"
386 android:layout_height="wrap_content"
387 android:stretchColumns="0,1,2,3">
389 </TableLayout>"""% rows
393 def set_history_layout(n):
394 values = get_history_values(n)
398 droid.fullSetProperty("hl_%d_col1"%i,"text", a)
401 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
403 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
405 droid.fullSetProperty("hl_%d_col2"%i,"text", b)
406 droid.fullSetProperty("hl_%d_col3"%i,"text", c)
407 droid.fullSetProperty("hl_%d_col4"%i,"text", d)
416 if not wallet.interface.is_connected:
417 text = "Not connected..."
418 elif wallet.blocks == 0:
419 text = "Server not ready"
420 elif not wallet.up_to_date:
421 text = "Synchronizing..."
423 c, u = wallet.get_balance()
424 text = "Balance:"+format_satoshis(c)
425 if u : text += ' [' + format_satoshis(u,True).strip() + ']'
428 # vibrate if status changed
429 if text != status_text:
430 if status_text and wallet.interface.is_connected and wallet.up_to_date:
434 droid.fullSetProperty("balanceTextView", "text", status_text)
436 if wallet.up_to_date:
437 set_history_layout(15)
442 def pay_to(recipient, amount, fee, label):
444 if wallet.use_encryption:
445 password = droid.dialogGetPassword('Password').result
446 if not password: return
450 droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
454 tx = wallet.mktx( recipient, amount, label, password, fee)
455 except BaseException, e:
456 modal_dialog('error', e.message)
457 droid.dialogDismiss()
460 droid.dialogDismiss()
462 r, h = wallet.sendtx( tx )
464 modal_dialog('Payment sent', h)
467 modal_dialog('Error', h)
475 droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
476 droid.dialogSetPositiveButtonText('Create')
477 droid.dialogSetNeutralButtonText('Restore')
478 droid.dialogSetNegativeButtonText('Cancel')
480 response = droid.dialogGetResponse().result
481 droid.dialogDismiss()
482 if response.get('which') == 'negative':
485 is_recovery = response.get('which') == 'neutral'
488 wallet.new_seed(None)
490 if modal_question("Input method",None,'QR Code', 'mnemonic'):
491 code = droid.scanBarcode()
494 seed = r['extras']['SCAN_RESULT']
498 m = modal_input('Mnemonic','please enter your code')
500 seed = mnemonic.mn_decode(m.split(' '))
502 modal_dialog('error: could not decode this seed')
505 wallet.seed = str(seed)
507 modal_dialog('Your seed is:', wallet.seed)
508 modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(wallet.seed)) )
510 msg = "recovering wallet..." if is_recovery else "creating wallet..."
511 droid.dialogCreateSpinnerProgress("Electrum", msg)
514 wallet.init_mpk( wallet.seed )
515 WalletSynchronizer(wallet,True).start()
518 droid.dialogDismiss()
522 if wallet.is_found():
523 wallet.update_tx_history()
524 wallet.fill_addressbook()
525 modal_dialog("recovery successful")
527 if not modal_question("no transactions found for this seed","do you want to keep this wallet?"):
530 change_password_dialog()
535 def make_new_contact():
536 code = droid.scanBarcode()
539 data = r['extras']['SCAN_RESULT']
541 if re.match('^bitcoin:', data):
542 address, _, _, _, _, _, _ = wallet.parse_url(data, None, None)
543 elif wallet.is_valid(data):
548 if modal_question('Add to contacts?', address):
549 wallet.addressbook.append(address)
552 modal_dialog('Invalid address', data)
557 def update_callback():
559 print "gui callback", wallet.interface.is_connected, wallet.up_to_date
561 droid.eventPost("refresh",'z')
571 event = droid.eventWait(1000).result
578 print "got event in main loop", repr(event)
579 if event == 'OK': continue
580 if event is None: continue
581 #if event["name"]=="refresh":
584 # request 2 taps before we exit
585 if event["name"]=="key":
586 if event["data"]["key"] == '4':
591 else: quitting = False
593 if event["name"]=="click":
594 id=event["data"]["id"]
596 elif event["name"]=="settings":
599 elif event["name"] in menu_commands:
602 if out == 'contacts':
604 contact_addr = select_from_contacts()
605 if contact_addr == 'newcontact':
611 elif out == "receive":
613 receive_addr = select_from_addresses()
615 amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal")
617 receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount)
629 droid.fullSetProperty("recipient","text",recipient)
634 event = droid.eventWait().result
635 print "got event in payto loop", event
637 if event["name"] == "click":
638 id = event["data"]["id"]
643 recipient = droid.fullQueryDetail("recipient").result.get('text')
644 label = droid.fullQueryDetail("label").result.get('text')
645 amount = droid.fullQueryDetail('amount').result.get('text')
647 if not wallet.is_valid(recipient):
648 modal_dialog('Error','Invalid Bitcoin address')
652 amount = int( 100000000 * Decimal(amount) )
654 modal_dialog('Error','Invalid amount')
657 result = pay_to(recipient, amount, wallet.fee, label)
661 elif id=="buttonContacts":
662 addr = select_from_contacts()
663 droid.fullSetProperty("recipient","text",addr)
666 code = droid.scanBarcode()
669 data = r['extras']['SCAN_RESULT']
671 if re.match('^bitcoin:', data):
672 payto, amount, label, _, _, _, _ = wallet.parse_url(data, None, None)
673 droid.fullSetProperty("recipient", "text",payto)
674 droid.fullSetProperty("amount", "text", amount)
675 droid.fullSetProperty("label", "text", label)
677 droid.fullSetProperty("recipient", "text", data)
680 elif event["name"] in menu_commands:
683 elif event["name"]=="key":
684 if event["data"]["key"] == '4':
687 #elif event["name"]=="screen":
688 # if event["data"]=="destroy":
701 event = droid.eventWait().result
702 print "got event", event
703 if event["name"]=="key":
704 if event["data"]["key"] == '4':
707 elif event["name"]=="clipboard":
708 droid.setClipboard(receive_addr)
709 modal_dialog('Address copied to clipboard',receive_addr)
711 elif event["name"]=="edit":
712 edit_label(receive_addr)
720 event = droid.eventWait().result
721 print "got event", event
722 if event["name"]=="key":
723 if event["data"]["key"] == '4':
726 elif event["name"]=="clipboard":
727 droid.setClipboard(contact_addr)
728 modal_dialog('Address copied to clipboard',contact_addr)
730 elif event["name"]=="edit":
731 edit_label(contact_addr)
733 elif event["name"]=="paytocontact":
734 recipient = contact_addr
737 elif event["name"]=="deletecontact":
738 if modal_question('delete contact', contact_addr):
744 def server_dialog(plist):
745 droid.dialogCreateAlert("Public servers")
746 droid.dialogSetItems( plist.keys() )
747 droid.dialogSetPositiveButtonText('Private server')
749 response = droid.dialogGetResponse().result
750 droid.dialogDismiss()
752 if response.get('which') == 'positive':
753 return modal_input('Private server', None)
755 i = response.get('item')
757 response = plist.keys()[i]
762 if wallet.use_encryption:
763 password = droid.dialogGetPassword('Seed').result
764 if not password: return
769 seed = wallet.pw_decode( wallet.seed, password)
771 modal_dialog('error','incorrect password')
774 modal_dialog('Your seed is',seed)
775 modal_dialog('Mnemonic code:', ' '.join(mnemonic.mn_encode(seed)) )
777 def change_password_dialog():
778 if wallet.use_encryption:
779 password = droid.dialogGetPassword('Your wallet is encrypted').result
780 if password is None: return
785 seed = wallet.pw_decode( wallet.seed, password)
787 modal_dialog('error','incorrect password')
790 new_password = droid.dialogGetPassword('Choose a password').result
791 if new_password == None:
794 if new_password != '':
795 password2 = droid.dialogGetPassword('Confirm new password').result
796 if new_password != password2:
797 modal_dialog('error','passwords do not match')
800 wallet.update_password(seed, password, new_password)
802 modal_dialog('Password updated','your wallet is encrypted')
804 modal_dialog('No password','your wallet is not encrypted')
812 server, port, p = wallet.server.split(':')
813 fee = str( Decimal( wallet.fee)/100000000 )
814 is_encrypted = 'yes' if wallet.use_encryption else 'no'
815 protocol = protocol_name(p)
816 droid.fullShow(settings_layout)
817 droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
823 event = droid.eventWait().result
824 print "got event", event
825 if event == 'OK': continue
826 if not event: continue
829 for item in wallet.interface.servers:
833 protocol, port = item2
837 if event["name"] == "itemclick":
838 pos = event["data"]["position"]
839 host, port, protocol = wallet.server.split(':')
841 if pos == "0": #server
842 host = server_dialog(plist)
846 srv = host + ':' + port + ':t'
848 wallet.set_server(srv)
850 modal_dialog('error','invalid server')
853 elif pos == "1": #protocol
855 srv = protocol_dialog(host, protocol, plist[host])
858 wallet.set_server(srv)
860 modal_dialog('error','invalid server')
863 elif pos == "2": #port
864 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
867 srv = host + ':' + a_port + ':'+ protocol
869 wallet.set_server(srv)
871 modal_dialog('error','invalid port number')
874 elif pos == "3": #fee
875 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")
878 fee = int( 100000000 * Decimal(fee) )
880 modal_dialog('error','invalid fee value')
881 if wallet.fee != fee:
887 if change_password_dialog():
894 elif event["name"] in menu_commands:
897 elif event["name"] == 'cancel':
900 elif event["name"] == "key":
901 if event["data"]["key"] == '4':
909 menu_commands = ["send", "receive", "settings", "contacts", "main"]
910 droid = android.Android()
912 wallet.register_callback(update_callback)
914 wallet.set_path("/sdcard/electrum.dat")
916 if not wallet.file_exists:
919 WalletSynchronizer(wallet,True).start()
925 droid.clearOptionsMenu()
927 droid.addOptionsMenuItem("Send","send",None,"")
928 droid.addOptionsMenuItem("Receive","receive",None,"")
929 droid.addOptionsMenuItem("Contacts","contacts",None,"")
930 droid.addOptionsMenuItem("Settings","settings",None,"")
932 droid.addOptionsMenuItem("Copy","clipboard",None,"")
933 droid.addOptionsMenuItem("Label","edit",None,"")
934 elif s == 'contacts':
935 droid.addOptionsMenuItem("Copy","clipboard",None,"")
936 droid.addOptionsMenuItem("Label","edit",None,"")
937 droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
938 #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
940 def make_bitmap(addr):
941 # fixme: this is highly inefficient
942 droid.dialogCreateSpinnerProgress("please wait")
945 import pyqrnative, bmp
946 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
949 k = qr.getModuleCount()
951 bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
953 droid.dialogDismiss()
960 droid.fullShow(main_layout())
965 droid.fullShow(payto_layout)
970 make_bitmap(receive_addr)
971 droid.fullShow(qr_layout(receive_addr))
974 elif s == 'contacts':
975 make_bitmap(contact_addr)
976 droid.fullShow(qr_layout(contact_addr))
979 elif s == 'settings':
980 #droid.fullShow(settings_layout)
986 droid.makeToast("Bye!")