layouts
[electrum-nvc.git] / client / electrum4a.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
5 #
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.
10 #
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.
15 #
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/>.
18
19
20
21 import android
22 from interface import WalletSynchronizer
23 from wallet import Wallet
24 from wallet import format_satoshis
25 from decimal import Decimal
26
27
28 import datetime
29
30
31 droid = android.Android()
32 wallet = Wallet()
33 wallet.set_path("/sdcard/electrum.dat")
34 wallet.read()
35
36
37
38
39 def select_from_contacts():
40     title = 'Contacts:'
41     droid.dialogCreateAlert(title)
42     droid.dialogSetItems(wallet.addressbook)
43     droid.dialogShow()
44     response = droid.dialogGetResponse()
45     result = response.result.get('item')
46     droid.dialogDismiss()
47     if result is not None:
48         addr = wallet.addressbook[result]
49         return addr
50
51
52 def select_from_addresses():
53     droid.dialogCreateAlert("Addresses:")
54     l = []
55     for i in range(len(wallet.addresses)):
56         addr = wallet.addresses[i]
57         l.append( wallet.labels.get(addr,'') + ' ' + addr)
58
59     droid.dialogSetItems(l)
60     droid.dialogShow()
61     response = droid.dialogGetResponse()
62     result = response.result.get('item')
63     droid.dialogDismiss()
64     if result is not None:
65         addr = wallet.addresses[result]
66         return addr
67
68
69
70 title = """
71         <TextView android:id="@+id/titleTextView" 
72                 android:layout_width="match_parent"
73                 android:layout_height="100" 
74                 android:text="Electrum"
75                 android:textAppearance="?android:attr/textAppearanceLarge" 
76                 android:gravity="center"
77                 android:textColor="0xff0055ff"
78                 android:textSize="30" >
79         </TextView>
80 """
81
82 def main_layout():
83     return """<?xml version="1.0" encoding="utf-8"?>
84 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
85     android:layout_width="match_parent" 
86     android:layout_height="match_parent">
87
88 <LinearLayout 
89         android:id="@+id/background"
90         android:orientation="vertical" 
91         android:layout_width="match_parent"
92         android:layout_height="match_parent" 
93         android:background="#ff000022">
94
95         %s
96
97         <TextView android:id="@+id/balanceTextView" 
98                 android:layout_width="match_parent"
99                 android:layout_height="wrap_content" 
100                 android:text=""
101                 android:textAppearance="?android:attr/textAppearanceLarge" 
102                 android:gravity="left"
103                 android:textColor="0xffffffff"
104                 android:padding="10"
105                 android:textSize="18" >
106         </TextView>
107
108
109         <TextView android:id="@+id/historyTextView" 
110                 android:layout_width="match_parent"
111                 android:layout_height="70" 
112                 android:text="Recent transactions"
113                 android:textAppearance="?android:attr/textAppearanceLarge" 
114                 android:gravity="center_vertical|center_horizontal|center">
115         </TextView>
116
117         %s
118
119         <TableLayout 
120            android:layout_width="match_parent" 
121            android:layout_height="wrap_content" 
122            android:id="@+id/linearLayout1">
123             <TableRow>
124                 <Button android:id="@+id/buttonSend" 
125                         android:layout_width="wrap_content"
126                         android:layout_height="wrap_content" 
127                         android:text="  Send ">
128                 </Button>
129                 <Button android:id="@+id/buttonReceive" 
130                         android:layout_width="wrap_content"
131                         android:layout_height="wrap_content" 
132                         android:text="Receive">
133                 </Button>
134                 <Button android:id="@+id/buttonContacts" 
135                         android:layout_width="wrap_content"
136                         android:layout_height="wrap_content" 
137                         android:text="Contacts">
138                 </Button>
139            </TableRow>
140         </TableLayout>
141
142 </LinearLayout>
143 </ScrollView>
144 """%(title, get_history_layout(15))
145
146
147 payto_layout="""<?xml version="1.0" encoding="utf-8"?>
148 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
149  android:id="@+id/background"
150  android:orientation="vertical" 
151  android:layout_width="match_parent"
152  android:layout_height="match_parent" 
153  android:background="#ff000022">
154
155         %s
156
157         <TextView android:id="@+id/recipientTextView" 
158                 android:layout_width="match_parent"
159                 android:layout_height="wrap_content" 
160                 android:text="Pay to:"
161                 android:textAppearance="?android:attr/textAppearanceLarge" 
162                 android:gravity="left">
163         </TextView>
164
165
166         <EditText android:id="@+id/recipient"
167                 android:layout_width="match_parent"
168                 android:layout_height="wrap_content" 
169                 android:tag="Tag Me" android:inputType="textCapWords|textPhonetic|number">
170         </EditText>
171
172         <LinearLayout android:id="@+id/linearLayout1"
173                 android:layout_width="match_parent"
174                 android:layout_height="wrap_content">
175                 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
176                         android:layout_height="wrap_content" android:text="Scan QR"></Button>
177                 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
178                         android:layout_height="wrap_content" android:text="Contacts"></Button>
179         </LinearLayout>
180
181
182         <TextView android:id="@+id/labelTextView" 
183                 android:layout_width="match_parent"
184                 android:layout_height="wrap_content" 
185                 android:text="Description:"
186                 android:textAppearance="?android:attr/textAppearanceLarge" 
187                 android:gravity="left">
188         </TextView>
189
190         <EditText android:id="@+id/label"
191                 android:layout_width="match_parent"
192                 android:layout_height="wrap_content" 
193                 android:tag="Tag Me" android:inputType="textCapWords|textPhonetic|number">
194         </EditText>
195
196         <TextView android:id="@+id/amountLabelTextView" 
197                 android:layout_width="match_parent"
198                 android:layout_height="wrap_content" 
199                 android:text="Amount:"
200                 android:textAppearance="?android:attr/textAppearanceLarge" 
201                 android:gravity="left">
202         </TextView>
203
204         <EditText android:id="@+id/amount"
205                 android:layout_width="match_parent"
206                 android:layout_height="wrap_content" 
207                 android:tag="Tag Me" android:inputType="numberDecimal">
208         </EditText>
209
210         <LinearLayout android:layout_width="match_parent"
211                 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
212                 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
213                         android:layout_height="wrap_content" android:text="Send"></Button>
214                 <Button android:id="@+id/buttonCancelSend" android:layout_width="wrap_content"
215                         android:layout_height="wrap_content" android:text="Cancel"></Button>
216         </LinearLayout>
217 </LinearLayout>
218 """%title
219
220
221
222 def make_layout(s):
223     return """<?xml version="1.0" encoding="utf-8"?>
224       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
225         android:id="@+id/background"
226         android:orientation="vertical" 
227         android:layout_width="match_parent"
228         android:layout_height="match_parent" 
229         android:background="#ff000000">
230         %s
231         %s
232       </LinearLayout>"""%(title,s)
233
234 receive_layout = make_layout("""
235         <TextView android:id="@+id/receiveTextView" 
236                 android:layout_width="match_parent"
237                 android:layout_height="wrap_content" 
238                 android:text="Addr"
239                 android:textAppearance="?android:attr/textAppearanceLarge" 
240                 android:gravity="left">
241         </TextView>""")
242
243 contacts_layout = make_layout("""
244         <TextView android:id="@+id/contactTextView" 
245                 android:layout_width="match_parent"
246                 android:layout_height="wrap_content" 
247                 android:text="Contacts"
248                 android:textAppearance="?android:attr/textAppearanceLarge" 
249                 android:gravity="left">
250         </TextView>""")
251
252 settings_layout = make_layout("""
253         <TextView android:id="@+id/serverTextView" 
254                 android:layout_width="match_parent"
255                 android:layout_height="wrap_content" 
256                 android:text="Server:"
257                 android:textAppearance="?android:attr/textAppearanceLarge" 
258                 android:gravity="left">
259         </TextView>
260
261         <EditText android:id="@+id/server"
262                 android:layout_width="match_parent"
263                 android:layout_height="wrap_content" 
264                 android:tag="Tag Me" android:inputType="*">
265         </EditText>
266
267         <LinearLayout android:layout_width="match_parent"
268                 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
269                 <Button android:id="@+id/buttonServer" android:layout_width="wrap_content"
270                         android:layout_height="wrap_content" android:text="Server List"></Button>
271                 <Button android:id="@+id/buttonSave" android:layout_width="wrap_content"
272                         android:layout_height="wrap_content" android:text="Save"></Button>
273                 <Button android:id="@+id/buttonCancel" android:layout_width="wrap_content"
274                         android:layout_height="wrap_content" android:text="Cancel"></Button>
275         </LinearLayout>""")
276
277
278
279 def get_history_values(n):
280     values = []
281     h = wallet.get_tx_history()
282     for i in range(n):
283         line = h[-i-1]
284         v = line['value']
285         try:
286             dt = datetime.datetime.fromtimestamp( line['timestamp'] )
287             if dt.date() == dt.today().date():
288                 time_str = str( dt.time() )
289             else:
290                 time_str = str( dt.date() )
291             conf = 'v'
292
293         except:
294             print line['timestamp']
295             time_str = 'pending'
296             conf = 'o'
297
298         label = line.get('label')
299         #if not label: label = line['tx_hash']
300         is_default_label = (label == '') or (label is None)
301         if is_default_label: label = line['default_label']
302         values.append((conf, '  ' + time_str, '  ' + format_satoshis(v,True), '  ' + label ))
303
304     return values
305
306
307 def get_history_layout(n):
308     rows = ""
309     i = 0
310     values = get_history_values(n)
311     for v in values:
312         a,b,c,d = v
313         color = "0xff00ff00" if a == 'v' else "0xffff0000"
314         rows += """
315         <TableRow>
316           <TextView
317             android:id="@+id/hl_%d_col1" 
318             android:layout_column="0"
319             android:text="%s"
320             android:textColor="%s"
321             android:padding="3" />
322           <TextView
323             android:id="@+id/hl_%d_col2" 
324             android:layout_column="1"
325             android:text="%s"
326             android:padding="3" />
327           <TextView
328             android:id="@+id/hl_%d_col3" 
329             android:layout_column="2"
330             android:text="%s"
331             android:padding="3" />
332           <TextView
333             android:id="@+id/hl_%d_col4" 
334             android:layout_column="3"
335             android:text="%s"
336             android:padding="4" />
337         </TableRow>"""%(i,a,color,i,b,i,c,i,d)
338         i += 1
339
340     output = """
341 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
342     android:layout_width="fill_parent"
343     android:layout_height="wrap_content"
344     android:stretchColumns="0,1,2,3">
345     %s
346 </TableLayout>"""% rows
347     return output
348
349 def set_history_layout(n):
350     values = get_history_values(n)
351     i = 0
352     for v in values:
353         a,b,c,d = v
354         droid.fullSetProperty("hl_%d_col1"%i,"text", a)
355
356         if a == 'v':
357             droid.fullSetProperty("hl_%d_col1"%i, "textColor","0xff00ff00")
358         else:
359             droid.fullSetProperty("hl_%d_col1"%i, "textColor","0xffff0000")
360
361         droid.fullSetProperty("hl_%d_col2"%i,"text", b)
362         droid.fullSetProperty("hl_%d_col3"%i,"text", c)
363         droid.fullSetProperty("hl_%d_col4"%i,"text", d)
364
365         i += 1
366
367
368 def update_layout():
369
370     if not wallet.interface.is_connected:
371         text = "Not connected..."
372     elif wallet.blocks == 0:
373         text = "Server not ready"
374     elif not wallet.up_to_date:
375         text = "Synchronizing..."
376     else:
377         c, u = wallet.get_balance()
378         text = "Balance:"+format_satoshis(c) 
379         if u : text += '['+ format_satoshis(u,True)+']'
380
381     droid.fullSetProperty("balanceTextView", "text", text)
382
383     if wallet.was_updated and wallet.up_to_date:
384         wallet.was_updated = False
385         set_history_layout(15)
386         droid.vibrate()
387
388
389
390
391 def pay_to(recipient, amount, fee, label):
392
393     if wallet.use_encryption:
394         password  = droid.dialogGetPassword('Password').result
395         print "password", password
396     else:
397         password = None
398
399     droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
400     droid.dialogShow()
401     tx = wallet.mktx( recipient, amount, label, password, fee)
402     print tx
403     droid.dialogDismiss()
404
405     if tx:
406         r, h = wallet.sendtx( tx )
407         droid.dialogCreateAlert('tx sent', h)
408         droid.dialogSetPositiveButtonText('OK')
409         droid.dialogShow()
410         response = droid.dialogGetResponse().result
411         droid.dialogDismiss()
412         return h
413     else:
414         return 'error'
415
416
417
418
419
420
421
422 if not wallet.file_exists:
423     droid.dialogCreateAlert("wallet file not found")
424     droid.dialogSetPositiveButtonText('OK')
425     droid.dialogShow()
426     resp = droid.dialogGetResponse().result
427     print resp
428
429     code = droid.scanBarcode()
430     r = code.result
431     if r:
432         seed = r['extras']['SCAN_RESULT']
433     else:
434         exit(1)
435
436     droid.dialogCreateAlert('seed', seed)
437     droid.dialogSetPositiveButtonText('OK')
438     droid.dialogSetNegativeButtonText('Cancel')
439     droid.dialogShow()
440     response = droid.dialogGetResponse().result
441     droid.dialogDismiss()
442     print response
443
444     wallet.seed = str(seed)
445     wallet.init_mpk( wallet.seed )
446     droid.dialogCreateSpinnerProgress("Electrum", "recovering wallet...")
447     droid.dialogShow()
448     WalletSynchronizer(wallet,True).start()
449     wallet.update()
450     wallet.save()
451     droid.dialogDismiss()
452     droid.vibrate()
453
454     if wallet.is_found():
455         # history and addressbook
456         wallet.update_tx_history()
457         wallet.fill_addressbook()
458         droid.dialogCreateAlert("recovery successful")
459         droid.dialogShow()
460         wallet.save()
461     else:
462         droid.dialogCreateSpinnerProgress("wallet not found")
463         droid.dialogShow()
464         exit(1)
465
466 else:
467     WalletSynchronizer(wallet,True).start()
468
469
470 def add_menu():
471     droid.addOptionsMenuItem("Network","settings",None,"")
472     droid.addOptionsMenuItem("New contact","newcontact",None,"")
473     droid.addOptionsMenuItem("Quit","quit",None,"")
474
475 add_menu()
476
477
478 def main_loop():
479     droid.fullShow(main_layout())
480     update_layout()
481     out = None
482     while out is None:
483
484         event = droid.eventWait(1000).result  # wait for 1 second
485         if not event:
486             update_layout()
487             continue
488
489         print "got event in main loop", event
490
491         if event["name"]=="click":
492             id=event["data"]["id"]
493
494             if id=="buttonSend":
495                 out = 'payto'
496
497             if id=="buttonContacts":
498                 global contact_addr
499                 contact_addr = select_from_contacts()
500                 if contact_addr:
501                     out = 'contacts'
502                 
503             elif id=="buttonReceive":
504                 global receive_addr
505                 receive_addr = select_from_addresses()
506                 if receive_addr:
507                     out = 'receive'
508
509         elif event["name"]=="settings":
510             out = 'settings'
511
512         elif event["name"]=="newcontact":
513             code = droid.scanBarcode()
514             r = code.result
515             if r:
516                 address = r['extras']['SCAN_RESULT']
517                 if address:
518                     if wallet.is_valid(address):
519                         droid.dialogCreateAlert('Add to contacts?', address)
520                         droid.dialogSetPositiveButtonText('OK')
521                         droid.dialogSetNegativeButtonText('Cancel')
522                         droid.dialogShow()
523                         response = droid.dialogGetResponse().result
524                         droid.dialogDismiss()
525                         print response
526                         if response.get('which') == 'positive':
527                             wallet.addressbook.append(address)
528                             wallet.save()
529                     else:
530                         droid.dialogCreateAlert('Invalid address', address)
531                         droid.dialogSetPositiveButtonText('OK')
532                         droid.dialogShow()
533                         response = droid.dialogGetResponse().result
534                         droid.dialogDismiss()
535
536         elif event["name"]=="key":
537             if event["data"]["key"] == '4':
538                 out = 'quit'
539
540         elif event["name"]=="quit":
541             out = 'quit'
542
543     return out
544                     
545 def payto_loop():
546     droid.fullShow(payto_layout)
547     out = None
548     while out is None:
549         event = droid.eventWait().result
550         print "got event in payto loop", event
551
552         if event["name"] == "click":
553             id = event["data"]["id"]
554
555             if id=="buttonPay":
556
557                 droid.fullQuery()
558                 recipient = droid.fullQueryDetail("recipient").result.get('text')
559                 label  = droid.fullQueryDetail("label").result.get('text')
560                 amount = droid.fullQueryDetail('amount').result.get('text')
561                 fee    = '0.001'
562                 amount = int( 100000000 * Decimal(amount) )
563                 fee    = int( 100000000 * Decimal(fee) )
564                 result = pay_to(recipient, amount, fee, label)
565
566                 droid.dialogCreateAlert('result', result)
567                 droid.dialogSetPositiveButtonText('OK')
568                 droid.dialogShow()
569                 droid.dialogGetResponse()
570                 droid.dialogDismiss()
571                 out = 'main'
572
573             elif id=="buttonContacts":
574                 addr = select_from_contacts()
575                 droid.fullSetProperty("recipient","text",addr)
576
577             elif id=="buttonQR":
578                 code = droid.scanBarcode()
579                 r = code.result
580                 if r:
581                     addr = r['extras']['SCAN_RESULT']
582                     if addr:
583                         droid.fullSetProperty("recipient","text",addr)
584                     
585             elif id=="buttonCancelSend":
586                 out = 'main'
587
588         elif event["name"]=="settings":
589             out = 'settings'
590
591         elif event["name"]=="quit":
592             out = 'quit'
593
594         elif event["name"]=="key":
595             if event["data"]["key"] == '4':
596                 out = 'main'
597
598         #elif event["name"]=="screen":
599         #    if event["data"]=="destroy":
600         #        out = 'main'
601
602     return out
603
604
605 receive_addr = ''
606 contact_addr = ''
607
608
609 def receive_loop():
610     droid.fullShow(receive_layout)
611     droid.fullSetProperty("receiveTextView","text", receive_addr)
612     out = None
613     while out is None:
614         event = droid.eventWait().result
615         print "got event", event
616         if event["name"] == "click":
617
618             if event["data"]["text"] == "OK":
619                 out = 'main'
620
621         elif event["name"]=="key":
622             if event["data"]["key"] == '4':
623                 out = 'main'
624
625     return out
626
627 def contacts_loop():
628     droid.fullShow(contacts_layout)
629     droid.fullSetProperty("contactTextView","text", contact_addr)
630     out = None
631     while out is None:
632         event = droid.eventWait().result
633         print "got event", event
634         if event["name"] == "click":
635
636             if event["data"]["text"] == "OK":
637                 out = 'main'
638
639         elif event["name"]=="key":
640             if event["data"]["key"] == '4':
641                 out = 'main'
642
643     return out
644
645
646 def server_dialog(plist):
647     droid.dialogCreateAlert("servers")
648     droid.dialogSetItems( plist.keys() )
649     droid.dialogShow()
650     i = droid.dialogGetResponse().result.get('item')
651     droid.dialogDismiss()
652     if i is not None:
653         response = plist.keys()[i]
654         return response
655
656
657 def protocol_dialog(plist):
658     options=["TCP","HTTP","native"]
659     droid.dialogCreateAlert("Protocol")
660     droid.dialogSetSingleChoiceItems(options)
661
662
663
664 def settings_loop():
665     droid.fullShow(settings_layout)
666     droid.fullSetProperty("server","text",wallet.server)
667
668     out = None
669     while out is None:
670         event = droid.eventWait().result
671         if event["name"] == "click":
672
673             id = event["data"]["id"]
674
675             if id=="buttonServer":
676                 plist = {}
677                 for item in wallet.interface.servers:
678                     host, pp = item
679                     z = {}
680                     for item2 in pp:
681                         protocol, port = item2
682                         z[protocol] = port
683                     plist[host] = z
684
685                 host = server_dialog(plist)
686                 p = plist[host]
687                 port = p['t']
688                 srv = host + ':' + port + ':t'
689                 droid.fullSetProperty("server","text",srv)
690
691             elif id=="buttonSave":
692                 droid.fullQuery()
693                 srv = droid.fullQueryDetail("server").result.get('text')
694                 try:
695                     wallet.set_server(srv)
696                     out = 'main'
697                 except:
698                     droid.dialogCreateAlert('error')
699                     droid.dialogSetPositiveButtonText('OK')
700                     droid.dialogShow()
701                     droid.dialogGetResponse()
702                     droid.dialogDismiss()
703                     
704             elif id=="buttonCancel":
705                 out = 'main'
706
707         elif event["name"] == "key":
708             if event["data"]["key"] == '4':
709                 out = 'main'
710
711         elif event["name"]=="quit":
712             out = 'quit'
713
714     return out
715
716                 
717
718
719 s = 'main'
720 while True:
721     if s == 'main':
722         s = main_loop()
723     elif s == 'payto':
724         s = payto_loop()
725     elif s == 'receive':
726         s = receive_loop()
727     elif s == 'contacts':
728         s = contacts_loop()
729     elif s == 'settings':
730         s = settings_loop()
731     elif s == 'contacts':
732         s = contacts_loop()
733     else:
734         break
735
736 droid.fullDismiss()
737 droid.makeToast("Bye!")