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 electrum import SimpleConfig, Interface, WalletSynchronizer, Wallet, format_satoshis, mnemonic_encode, mnemonic_decode
25 from decimal import Decimal
30 def modal_dialog(title, msg = None):
31 droid.dialogCreateAlert(title,msg)
32 droid.dialogSetPositiveButtonText('OK')
34 droid.dialogGetResponse()
37 def modal_input(title, msg, value = None, etype=None):
38 droid.dialogCreateInput(title, msg, value, etype)
39 droid.dialogSetPositiveButtonText('OK')
40 droid.dialogSetNegativeButtonText('Cancel')
42 response = droid.dialogGetResponse()
43 result = response.result
45 print "modal input: result is none"
48 if result.get('which') == 'positive':
49 return result.get('value')
51 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
52 droid.dialogCreateAlert(q, msg)
53 droid.dialogSetPositiveButtonText(pos_text)
54 droid.dialogSetNegativeButtonText(neg_text)
56 response = droid.dialogGetResponse()
57 result = response.result
59 print "modal question: result is none"
62 return result.get('which') == 'positive'
65 v = modal_input('Edit label',None,wallet.labels.get(addr))
68 wallet.labels[addr] = v
70 if addr in wallet.labels.keys():
71 wallet.labels.pop(addr)
72 wallet.update_tx_history()
74 droid.fullSetProperty("labelTextView", "text", v)
76 def select_from_contacts():
78 droid.dialogCreateAlert(title)
80 for i in range(len(wallet.addressbook)):
81 addr = wallet.addressbook[i]
82 label = wallet.labels.get(addr,addr)
84 droid.dialogSetItems(l)
85 droid.dialogSetPositiveButtonText('New contact')
87 response = droid.dialogGetResponse().result
90 if response.get('which') == 'positive':
93 result = response.get('item')
95 if result is not None:
96 addr = wallet.addressbook[result]
100 def select_from_addresses():
101 droid.dialogCreateAlert("Addresses:")
103 for i in range(len(wallet.addresses)):
104 addr = wallet.addresses[i]
105 label = wallet.labels.get(addr,addr)
107 droid.dialogSetItems(l)
109 response = droid.dialogGetResponse()
110 result = response.result.get('item')
111 droid.dialogDismiss()
112 if result is not None:
113 addr = wallet.addresses[result]
117 def protocol_name(p):
118 if p == 't': return 'TCP'
119 if p == 'h': return 'HTTP'
120 if p == 's': return 'TCP/SSL'
121 if p == 'g': return 'HTTPS'
124 def protocol_dialog(host, protocol, z):
125 droid.dialogCreateAlert('Protocol',host)
131 current = protocols.index(protocol)
133 l.append(protocol_name(p))
134 droid.dialogSetSingleChoiceItems(l, current)
135 droid.dialogSetPositiveButtonText('OK')
136 droid.dialogSetNegativeButtonText('Cancel')
138 response = droid.dialogGetResponse().result
139 selected_item = droid.dialogGetSelectedItems().result
140 droid.dialogDismiss()
142 if not response: return
143 if not selected_item: return
144 if response.get('which') == 'positive':
145 p = protocols[selected_item[0]]
147 return host + ':' + port + ':' + p
152 def make_layout(s, scrollable = False):
157 android:layout_width="match_parent"
158 android:layout_height="wrap_content"
159 android:background="#ff222222">
162 android:id="@+id/textElectrum"
163 android:text="Electrum"
164 android:textSize="7pt"
165 android:textColor="#ff4444ff"
166 android:gravity="left"
167 android:layout_height="wrap_content"
168 android:layout_width="match_parent"
177 android:id="@+id/scrollview"
178 android:layout_width="match_parent"
179 android:layout_height="match_parent" >
182 android:orientation="vertical"
183 android:layout_width="match_parent"
184 android:layout_height="wrap_content" >
193 return """<?xml version="1.0" encoding="utf-8"?>
194 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
195 android:id="@+id/background"
196 android:orientation="vertical"
197 android:layout_width="match_parent"
198 android:layout_height="match_parent"
199 android:background="#ff000022">
202 </LinearLayout>"""%content
208 return make_layout("""
209 <TextView android:id="@+id/balanceTextView"
210 android:layout_width="match_parent"
212 android:textColor="#ffffffff"
213 android:textAppearance="?android:attr/textAppearanceLarge"
214 android:padding="7dip"
215 android:textSize="8pt"
216 android:gravity="center_vertical|center_horizontal|left">
219 <TextView android:id="@+id/historyTextView"
220 android:layout_width="match_parent"
221 android:layout_height="wrap_content"
222 android:text="Recent transactions"
223 android:textAppearance="?android:attr/textAppearanceLarge"
224 android:gravity="center_vertical|center_horizontal|center">
227 %s """%get_history_layout(15),True)
232 return make_layout("""
234 <TextView android:id="@+id/addrTextView"
235 android:layout_width="match_parent"
236 android:layout_height="50"
238 android:textAppearance="?android:attr/textAppearanceLarge"
239 android:gravity="center_vertical|center_horizontal|center">
243 android:id="@+id/qrView"
244 android:gravity="center"
245 android:layout_width="match_parent"
246 android:layout_height="350"
247 android:antialias="false"
248 android:src="file:///sdcard/sl4a/qrcode.bmp" />
250 <TextView android:id="@+id/labelTextView"
251 android:layout_width="match_parent"
252 android:layout_height="50"
254 android:textAppearance="?android:attr/textAppearanceLarge"
255 android:gravity="center_vertical|center_horizontal|center">
258 """%(addr,wallet.labels.get(addr,'')), True)
260 payto_layout = make_layout("""
262 <TextView android:id="@+id/recipientTextView"
263 android:layout_width="match_parent"
264 android:layout_height="wrap_content"
265 android:text="Pay to:"
266 android:textAppearance="?android:attr/textAppearanceLarge"
267 android:gravity="left">
271 <EditText android:id="@+id/recipient"
272 android:layout_width="match_parent"
273 android:layout_height="wrap_content"
274 android:tag="Tag Me" android:inputType="text">
277 <LinearLayout android:id="@+id/linearLayout1"
278 android:layout_width="match_parent"
279 android:layout_height="wrap_content">
280 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
281 android:layout_height="wrap_content" android:text="From QR code"></Button>
282 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
283 android:layout_height="wrap_content" android:text="From Contacts"></Button>
287 <TextView android:id="@+id/labelTextView"
288 android:layout_width="match_parent"
289 android:layout_height="wrap_content"
290 android:text="Description:"
291 android:textAppearance="?android:attr/textAppearanceLarge"
292 android:gravity="left">
295 <EditText android:id="@+id/label"
296 android:layout_width="match_parent"
297 android:layout_height="wrap_content"
298 android:tag="Tag Me" android:inputType="text">
301 <TextView android:id="@+id/amountLabelTextView"
302 android:layout_width="match_parent"
303 android:layout_height="wrap_content"
304 android:text="Amount:"
305 android:textAppearance="?android:attr/textAppearanceLarge"
306 android:gravity="left">
309 <EditText android:id="@+id/amount"
310 android:layout_width="match_parent"
311 android:layout_height="wrap_content"
312 android:tag="Tag Me" android:inputType="numberDecimal">
315 <LinearLayout android:layout_width="match_parent"
316 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
317 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
318 android:layout_height="wrap_content" android:text="Send"></Button>
319 </LinearLayout>""",False)
323 settings_layout = make_layout(""" <ListView
324 android:id="@+id/myListView"
325 android:layout_width="match_parent"
326 android:layout_height="wrap_content" />""")
330 def get_history_values(n):
332 h = wallet.get_tx_history()
333 length = min(n, len(h))
334 for i in range(length):
335 tx_hash, conf, is_mine, value, fee, balance, timestamp = h[-i-1]
337 dt = datetime.datetime.fromtimestamp( timestamp )
338 if dt.date() == dt.today().date():
339 time_str = str( dt.time() )
341 time_str = str( dt.date() )
345 conf_str = 'v' if conf else 'o'
346 label, is_default_label = wallet.get_label(tx_hash)
347 values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value,True), ' ' + label ))
352 def get_history_layout(n):
355 values = get_history_values(n)
358 color = "#ff00ff00" if a == 'v' else "#ffff0000"
362 android:id="@+id/hl_%d_col1"
363 android:layout_column="0"
365 android:textColor="%s"
366 android:padding="3" />
368 android:id="@+id/hl_%d_col2"
369 android:layout_column="1"
371 android:padding="3" />
373 android:id="@+id/hl_%d_col3"
374 android:layout_column="2"
376 android:padding="3" />
378 android:id="@+id/hl_%d_col4"
379 android:layout_column="3"
381 android:padding="4" />
382 </TableRow>"""%(i,a,color,i,b,i,c,i,d)
386 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
387 android:layout_width="fill_parent"
388 android:layout_height="wrap_content"
389 android:stretchColumns="0,1,2,3">
391 </TableLayout>"""% rows
395 def set_history_layout(n):
396 values = get_history_values(n)
400 droid.fullSetProperty("hl_%d_col1"%i,"text", a)
403 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
405 droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
407 droid.fullSetProperty("hl_%d_col2"%i,"text", b)
408 droid.fullSetProperty("hl_%d_col3"%i,"text", c)
409 droid.fullSetProperty("hl_%d_col4"%i,"text", d)
418 if not wallet.interface.is_connected:
419 text = "Not connected..."
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 def make_new_contact():
476 code = droid.scanBarcode()
479 data = r['extras']['SCAN_RESULT']
481 if re.match('^bitcoin:', data):
482 address, _, _, _, _, _, _ = wallet.parse_url(data, None, lambda x: modal_question('Question',x))
483 elif wallet.is_valid(data):
488 if modal_question('Add to contacts?', address):
489 wallet.addressbook.append(address)
492 modal_dialog('Invalid address', data)
497 def update_callback():
499 print "gui callback", wallet.interface.is_connected, wallet.up_to_date
501 droid.eventPost("refresh",'z')
511 event = droid.eventWait(1000).result
518 print "got event in main loop", repr(event)
519 if event == 'OK': continue
520 if event is None: continue
521 if not event.get("name"): continue
523 # request 2 taps before we exit
524 if event["name"]=="key":
525 if event["data"]["key"] == '4':
530 else: quitting = False
532 if event["name"]=="click":
533 id=event["data"]["id"]
535 elif event["name"]=="settings":
538 elif event["name"] in menu_commands:
541 if out == 'contacts':
543 contact_addr = select_from_contacts()
544 if contact_addr == 'newcontact':
550 elif out == "receive":
552 receive_addr = select_from_addresses()
554 amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal")
556 receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount)
568 droid.fullSetProperty("recipient","text",recipient)
573 event = droid.eventWait().result
574 if not event: continue
575 print "got event in payto loop", event
576 if event == 'OK': continue
577 if not event.get("name"): continue
579 if event["name"] == "click":
580 id = event["data"]["id"]
585 recipient = droid.fullQueryDetail("recipient").result.get('text')
586 label = droid.fullQueryDetail("label").result.get('text')
587 amount = droid.fullQueryDetail('amount').result.get('text')
589 if not wallet.is_valid(recipient):
590 modal_dialog('Error','Invalid Bitcoin address')
594 amount = int( 100000000 * Decimal(amount) )
596 modal_dialog('Error','Invalid amount')
599 result = pay_to(recipient, amount, wallet.fee, label)
603 elif id=="buttonContacts":
604 addr = select_from_contacts()
605 droid.fullSetProperty("recipient","text",addr)
608 code = droid.scanBarcode()
611 data = r['extras']['SCAN_RESULT']
613 if re.match('^bitcoin:', data):
614 payto, amount, label, _, _, _, _ = wallet.parse_url(data, None, lambda x: modal_question('Question', x))
615 droid.fullSetProperty("recipient", "text",payto)
616 droid.fullSetProperty("amount", "text", amount)
617 droid.fullSetProperty("label", "text", label)
619 droid.fullSetProperty("recipient", "text", data)
622 elif event["name"] in menu_commands:
625 elif event["name"]=="key":
626 if event["data"]["key"] == '4':
629 #elif event["name"]=="screen":
630 # if event["data"]=="destroy":
643 event = droid.eventWait().result
644 print "got event", event
645 if event["name"]=="key":
646 if event["data"]["key"] == '4':
649 elif event["name"]=="clipboard":
650 droid.setClipboard(receive_addr)
651 modal_dialog('Address copied to clipboard',receive_addr)
653 elif event["name"]=="edit":
654 edit_label(receive_addr)
662 event = droid.eventWait().result
663 print "got event", event
664 if event["name"]=="key":
665 if event["data"]["key"] == '4':
668 elif event["name"]=="clipboard":
669 droid.setClipboard(contact_addr)
670 modal_dialog('Address copied to clipboard',contact_addr)
672 elif event["name"]=="edit":
673 edit_label(contact_addr)
675 elif event["name"]=="paytocontact":
676 recipient = contact_addr
679 elif event["name"]=="deletecontact":
680 if modal_question('delete contact', contact_addr):
686 def server_dialog(plist):
687 droid.dialogCreateAlert("Public servers")
688 droid.dialogSetItems( plist.keys() )
689 droid.dialogSetPositiveButtonText('Private server')
691 response = droid.dialogGetResponse().result
692 droid.dialogDismiss()
693 if not response: return
695 if response.get('which') == 'positive':
696 return modal_input('Private server', None)
698 i = response.get('item')
700 response = plist.keys()[i]
705 if wallet.use_encryption:
706 password = droid.dialogGetPassword('Seed').result
707 if not password: return
712 seed = wallet.pw_decode( wallet.seed, password)
714 modal_dialog('error','incorrect password')
717 modal_dialog('Your seed is',seed)
718 modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(seed)) )
720 def change_password_dialog():
721 if wallet.use_encryption:
722 password = droid.dialogGetPassword('Your wallet is encrypted').result
723 if password is None: return
728 seed = wallet.pw_decode( wallet.seed, password)
730 modal_dialog('error','incorrect password')
733 new_password = droid.dialogGetPassword('Choose a password').result
734 if new_password == None:
737 if new_password != '':
738 password2 = droid.dialogGetPassword('Confirm new password').result
739 if new_password != password2:
740 modal_dialog('error','passwords do not match')
743 wallet.update_password(seed, password, new_password)
745 modal_dialog('Password updated','your wallet is encrypted')
747 modal_dialog('No password','your wallet is not encrypted')
755 server, port, p = interface.server.split(':')
756 fee = str( Decimal( wallet.fee)/100000000 )
757 is_encrypted = 'yes' if wallet.use_encryption else 'no'
758 protocol = protocol_name(p)
759 droid.fullShow(settings_layout)
760 droid.fullSetList("myListView",['Server: ' + server, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
766 event = droid.eventWait()
768 print "got event", event
769 if event == 'OK': continue
770 if not event: continue
772 plist, servers_list = interface.get_servers_list()
773 name = event.get("name")
774 if not name: continue
776 if name == "itemclick":
777 pos = event["data"]["position"]
778 host, port, protocol = interface.server.split(':')
780 if pos == "0": #server
781 host = server_dialog(plist)
785 srv = host + ':' + port + ':t'
786 wallet.config.set_key("server", srv, True)
788 wallet.interface.set_server(srv)
790 modal_dialog('error','invalid server')
793 elif pos == "1": #protocol
795 srv = protocol_dialog(host, protocol, plist[host])
797 wallet.config.set_key("server", srv, True)
799 wallet.interface.set_server(srv)
801 modal_dialog('error','invalid server')
804 elif pos == "2": #port
805 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
808 srv = host + ':' + a_port + ':'+ protocol
809 wallet.config.set_key("server", srv, True)
811 wallet.interface.set_server(srv)
813 modal_dialog('error','invalid port number')
816 elif pos == "3": #fee
817 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")
820 fee = int( 100000000 * Decimal(fee) )
822 modal_dialog('error','invalid fee value')
823 if wallet.fee != fee:
829 if change_password_dialog():
836 elif name in menu_commands:
839 elif name == 'cancel':
843 if event["data"]["key"] == '4':
849 droid.clearOptionsMenu()
851 droid.addOptionsMenuItem("Send","send",None,"")
852 droid.addOptionsMenuItem("Receive","receive",None,"")
853 droid.addOptionsMenuItem("Contacts","contacts",None,"")
854 droid.addOptionsMenuItem("Settings","settings",None,"")
856 droid.addOptionsMenuItem("Copy","clipboard",None,"")
857 droid.addOptionsMenuItem("Label","edit",None,"")
858 elif s == 'contacts':
859 droid.addOptionsMenuItem("Copy","clipboard",None,"")
860 droid.addOptionsMenuItem("Label","edit",None,"")
861 droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
862 #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
865 def make_bitmap(addr):
866 # fixme: this is highly inefficient
867 droid.dialogCreateSpinnerProgress("please wait")
870 import pyqrnative, bmp
871 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
874 k = qr.getModuleCount()
876 bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
878 droid.dialogDismiss()
883 droid = android.Android()
884 menu_commands = ["send", "receive", "settings", "contacts", "main"]
890 def __init__(self, w, config, app=None):
891 global wallet, interface
893 interface = wallet.interface
894 interface.register_callback('updated',update_callback)
895 interface.register_callback('connected', update_callback)
896 interface.register_callback('disconnected', update_callback)
897 interface.register_callback('disconnecting', update_callback)
905 droid.fullShow(main_layout())
909 droid.fullShow(payto_layout)
913 make_bitmap(receive_addr)
914 droid.fullShow(qr_layout(receive_addr))
917 elif s == 'contacts':
918 make_bitmap(contact_addr)
919 droid.fullShow(qr_layout(contact_addr))
922 elif s == 'settings':
928 droid.makeToast("Bye!")
930 def restore_or_create(self):
931 droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
932 droid.dialogSetPositiveButtonText('Create')
933 droid.dialogSetNeutralButtonText('Restore')
934 droid.dialogSetNegativeButtonText('Cancel')
936 response = droid.dialogGetResponse().result
937 droid.dialogDismiss()
938 if not response: return
939 if response.get('which') == 'negative':
942 return 'restore' if response.get('which') == 'neutral' else 'create'
944 def seed_dialog(self):
945 if modal_question("Input method",None,'QR Code', 'mnemonic'):
946 code = droid.scanBarcode()
949 seed = r['extras']['SCAN_RESULT']
953 m = modal_input('Mnemonic','please enter your code')
955 seed = mnemonic_decode(m.split(' '))
957 modal_dialog('error: could not decode this seed')
959 wallet.seed = str(seed)
963 def network_dialog(self):
968 modal_dialog('Your seed is:', wallet.seed)
969 modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(wallet.seed)) )
972 def password_dialog(self):
973 change_password_dialog()
976 def restore_wallet(self):
978 msg = "recovering wallet..."
979 droid.dialogCreateSpinnerProgress("Electrum", msg)
984 droid.dialogDismiss()
987 if wallet.is_found():
988 wallet.fill_addressbook()
989 modal_dialog("recovery successful")
991 if not modal_question("no transactions found for this seed","do you want to keep this wallet?"):