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/>.
34 from util
import print_msg
, print_error
, format_satoshis
35 #from bitcoin import *
37 from transaction
import Transaction
38 #from plugins import run_hook
40 #from synchronizer import WalletSynchronizer
42 COINBASE_MATURITY
= 100
45 # internal ID for imported account
46 IMPORTED_ACCOUNT
= '/x'
55 def __init__(self
, config
):
56 self
.lock
= threading
.Lock()
59 self
.file_exists
= False
60 self
.path
= self
.init_path(config
)
61 print_error( "wallet path", self
.path
)
66 def init_path(self
, config
):
67 """Set the path of the wallet."""
69 # command line -w option
70 path
= config
.get('wallet_path')
75 path
= config
.get('default_wallet_path')
80 dirpath
= os
.path
.join(config
.path
, "wallets")
81 if not os
.path
.exists(dirpath
):
84 new_path
= os
.path
.join(config
.path
, "wallets", "default_wallet")
86 # default path in pre 1.9 versions
87 old_path
= os
.path
.join(config
.path
, "electrum.dat")
88 if os
.path
.exists(old_path
) and not os
.path
.exists(new_path
):
89 os
.rename(old_path
, new_path
)
95 """Read the contents of the wallet file."""
97 with
open(self
.path
, "r") as f
:
102 d
= ast
.literal_eval( data
) #parse raw data from reading wallet file
104 raise IOError("Cannot read wallet file.")
107 self
.file_exists
= True
110 def get(self
, key
, default
=None):
111 v
= self
.data
.get(key
)
116 def put(self
, key
, value
, save
= True):
119 if value
is not None:
120 self
.data
[key
] = value
121 elif key
in self
.data
:
128 class Abstract_Wallet
:
130 def __init__(self
, storage
):
132 self
.storage
= storage
133 self
.electrum_version
= ELECTRUM_VERSION
134 self
.gap_limit_for_change
= 3 # constant
136 self
.seed_version
= storage
.get('seed_version', NEW_SEED_VERSION
)
137 self
.gap_limit
= storage
.get('gap_limit', 5)
138 self
.use_change
= storage
.get('use_change',True)
139 self
.use_encryption
= storage
.get('use_encryption', False)
140 self
.seed
= storage
.get('seed', '') # encrypted
141 self
.labels
= storage
.get('labels', {})
142 self
.frozen_addresses
= storage
.get('frozen_addresses',[])
143 self
.addressbook
= storage
.get('contacts', [])
145 self
.history
= storage
.get('addr_history',{}) # address -> list(txid, height)
147 self
.fee
= int(storage
.get('fee_per_kb', 10000))
149 self
.master_public_keys
= storage
.get('master_public_keys',{})
150 self
.master_private_keys
= storage
.get('master_private_keys', {})
152 self
.next_addresses
= storage
.get('next_addresses',{})
157 self
.transactions
= {}
158 tx_list
= self
.storage
.get('transactions',{})
159 for k
,v
in tx_list
.items():
163 print_msg("Warning: Cannot deserialize transactions. skipping")
166 self
.add_extra_addresses(tx
)
167 self
.transactions
[k
] = tx
169 for h
,tx
in self
.transactions
.items():
170 if not self
.check_new_tx(h
, tx
):
171 print_error("removing unreferenced tx", h
)
172 self
.transactions
.pop(h
)
176 self
.prevout_values
= {} # my own transaction outputs
177 self
.spent_outputs
= []
182 # there is a difference between wallet.up_to_date and interface.is_up_to_date()
183 # interface.is_up_to_date() returns true when all requests have been answered and processed
184 # wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
186 self
.up_to_date
= False
187 self
.lock
= threading
.Lock()
188 self
.transaction_lock
= threading
.Lock()
189 self
.tx_event
= threading
.Event()
191 for tx_hash
, tx
in self
.transactions
.items():
192 self
.update_tx_outputs(tx_hash
)
195 def add_extra_addresses(self
, tx
):
197 # find the address corresponding to pay-to-pubkey inputs
198 tx
.add_extra_addresses(self
.transactions
)
199 for o
in tx
.d
.get('outputs'):
200 if o
.get('is_pubkey'):
201 for tx2
in self
.transactions
.values():
202 tx2
.add_extra_addresses({h
:tx
})
205 def load_accounts(self
):
207 self
.imported_keys
= self
.storage
.get('imported_keys',{})
209 d
= self
.storage
.get('accounts', {})
210 for k
, v
in d
.items():
212 v
['mpk'] = self
.storage
.get('master_public_key')
213 self
.accounts
[k
] = OldAccount(v
)
214 elif v
.get('imported'):
215 self
.accounts
[k
] = ImportedAccount(v
)
217 self
.accounts
[k
] = BIP32_Account_2of3(v
)
219 self
.accounts
[k
] = BIP32_Account_2of2(v
)
221 self
.accounts
[k
] = BIP32_Account(v
)
222 elif v
.get('pending'):
223 self
.accounts
[k
] = PendingAccount(v
)
225 print_error("cannot load account", v
)
228 def can_create_accounts(self
):
231 def set_up_to_date(self
,b
):
232 with self
.lock
: self
.up_to_date
= b
234 def is_up_to_date(self
):
235 with self
.lock
: return self
.up_to_date
239 self
.up_to_date
= False
240 while not self
.is_up_to_date():
243 def is_imported(self
, addr
):
244 account
= self
.accounts
.get(IMPORTED_ACCOUNT
)
246 return addr
in account
.get_addresses(0)
250 def has_imported_keys(self
):
251 account
= self
.accounts
.get(IMPORTED_ACCOUNT
)
252 return account
is not None
254 def set_label(self
, name
, text
= None):
256 old_text
= self
.labels
.get(name
)
259 self
.labels
[name
] = text
263 self
.labels
.pop(name
)
267 self
.storage
.put('labels', self
.labels
, True)
269 run_hook('set_label', name
, text
, changed
)
272 def addresses(self
, include_change
= True, _next
=True):
274 for a
in self
.accounts
.keys():
275 o
+= self
.get_account_addresses(a
, include_change
)
278 for addr
in self
.next_addresses
.values():
284 def is_mine(self
, address
):
285 return address
in self
.addresses(True)
288 def is_change(self
, address
):
289 if not self
.is_mine(address
): return False
290 acct
, s
= self
.get_address_index(address
)
291 if s
is None: return False
295 def get_address_index(self
, address
):
297 for account
in self
.accounts
.keys():
298 for for_change
in [0,1]:
299 addresses
= self
.accounts
[account
].get_addresses(for_change
)
300 for addr
in addresses
:
302 return account
, (for_change
, addresses
.index(addr
))
304 for k
,v
in self
.next_addresses
.items():
308 raise Exception("Address not found", address
)
311 def getpubkeys(self
, addr
):
312 assert is_valid(addr
) and self
.is_mine(addr
)
313 account
, sequence
= self
.get_address_index(addr
)
314 a
= self
.accounts
[account
]
315 return a
.get_pubkeys( sequence
)
318 def get_private_key(self
, address
, password
):
319 if self
.is_watching_only():
321 account_id
, sequence
= self
.get_address_index(address
)
322 return self
.accounts
[account_id
].get_private_key(sequence
, self
, password
)
325 def get_public_keys(self
, address
):
326 account_id
, sequence
= self
.get_address_index(address
)
327 return self
.accounts
[account_id
].get_pubkeys(sequence
)
330 def sign_message(self
, address
, message
, password
):
331 keys
= self
.get_private_key(address
, password
)
332 assert len(keys
) == 1
334 key
= regenerate_key(sec
)
335 compressed
= is_compressed(sec
)
336 return key
.sign_message(message
, compressed
, address
)
340 def decrypt_message(self
, pubkey
, message
, password
):
341 address
= public_key_to_bc_address(pubkey
.decode('hex'))
342 keys
= self
.get_private_key(address
, password
)
344 ec
= regenerate_key(secret
)
345 decrypted
= ec
.decrypt_message(message
)
351 return self
.history
.values() != [[]] * len(self
.history
)
354 def get_tx_value(self
, tx
, account
=None):
355 domain
= self
.get_account_addresses(account
)
356 return tx
.get_value(domain
, self
.prevout_values
)
359 def update_tx_outputs(self
, tx_hash
):
360 tx
= self
.transactions
.get(tx_hash
)
362 for i
, (addr
, value
) in enumerate(tx
.outputs
):
363 key
= tx_hash
+ ':%d'%i
364 self
.prevout_values
[key
] = value
366 for item
in tx
.inputs
:
367 if self
.is_mine(item
.get('address')):
368 key
= item
['prevout_hash'] + ':%d'%item
['prevout_n']
369 self
.spent_outputs
.append(key
)
372 def get_addr_balance(self
, address
):
373 #assert self.is_mine(address)
374 h
= self
.history
.get(address
,[])
375 if h
== ['*']: return 0,0
377 received_coins
= [] # list of coins received at address
379 for tx_hash
, tx_height
in h
:
380 tx
= self
.transactions
.get(tx_hash
)
383 for i
, (addr
, value
) in enumerate(tx
.outputs
):
385 key
= tx_hash
+ ':%d'%i
386 received_coins
.append(key
)
388 for tx_hash
, tx_height
in h
:
389 tx
= self
.transactions
.get(tx_hash
)
393 for item
in tx
.inputs
:
394 addr
= item
.get('address')
396 key
= item
['prevout_hash'] + ':%d'%item
['prevout_n']
397 value
= self
.prevout_values
.get( key
)
398 if key
in received_coins
:
401 for i
, (addr
, value
) in enumerate(tx
.outputs
):
402 key
= tx_hash
+ ':%d'%i
413 def get_account_name(self
, k
):
414 return self
.labels
.get(k
, self
.accounts
[k
].get_name(k
))
417 def get_account_names(self
):
419 for k
in self
.accounts
.keys():
420 account_names
[k
] = self
.get_account_name(k
)
424 def get_account_addresses(self
, a
, include_change
=True):
426 o
= self
.addresses(True)
427 elif a
in self
.accounts
:
428 ac
= self
.accounts
[a
]
429 o
= ac
.get_addresses(0)
430 if include_change
: o
+= ac
.get_addresses(1)
434 def get_account_balance(self
, account
):
435 return self
.get_balance(self
.get_account_addresses(account
))
437 def get_frozen_balance(self
):
438 return self
.get_balance(self
.frozen_addresses
)
440 def get_balance(self
, domain
=None):
441 if domain
is None: domain
= self
.addresses(True)
444 c
, u
= self
.get_addr_balance(addr
)
450 def get_unspent_coins(self
, domain
=None):
452 if domain
is None: domain
= self
.addresses(True)
454 h
= self
.history
.get(addr
, [])
455 if h
== ['*']: continue
456 for tx_hash
, tx_height
in h
:
457 tx
= self
.transactions
.get(tx_hash
)
458 if tx
is None: raise Exception("Wallet not synchronized")
459 is_coinbase
= tx
.inputs
[0].get('prevout_hash') == '0'*64
460 for o
in tx
.d
.get('outputs'):
462 if output
.get('address') != addr
: continue
463 key
= tx_hash
+ ":%d" % output
.get('prevout_n')
464 if key
in self
.spent_outputs
: continue
465 output
['prevout_hash'] = tx_hash
466 output
['height'] = tx_height
467 output
['coinbase'] = is_coinbase
468 coins
.append((tx_height
, output
))
472 coins
= sorted(coins
)
473 if coins
[-1][0] != 0:
474 while coins
[0][0] == 0:
475 coins
= coins
[1:] + [ coins
[0] ]
476 return [x
[1] for x
in coins
]
479 def get_history(self
, address
):
481 return self
.history
.get(address
)
484 def get_tx_history(self
, account
=None):
485 # if not self.verifier:
488 with self
.transaction_lock
:
489 history
= self
.transactions
.items()
490 #history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
494 for tx_hash
, tx
in history
:
495 is_relevant
, is_mine
, v
, fee
= self
.get_tx_value(tx
, account
)
496 if v
is not None: balance
+= v
498 c
, u
= self
.get_account_balance(account
)
501 result
.append( ('', 1000, 0, c
+u
-balance
, None, c
+u
-balance
, None ) )
503 balance
= c
+ u
- balance
504 for tx_hash
, tx
in history
:
505 is_relevant
, is_mine
, value
, fee
= self
.get_tx_value(tx
, account
)
508 if value
is not None:
511 conf
, timestamp
= self
.verifier
.get_confirmations(tx_hash
) if self
.verifier
else (None, None)
512 result
.append( (tx_hash
, conf
, is_mine
, value
, fee
, balance
, timestamp
) )
517 def get_label(self
, tx_hash
):
518 label
= self
.labels
.get(tx_hash
)
519 is_default
= (label
== '') or (label
is None)
520 if is_default
: label
= self
.get_default_label(tx_hash
)
521 return label
, is_default
524 def get_default_label(self
, tx_hash
):
525 tx
= self
.transactions
.get(tx_hash
)
528 is_relevant
, is_mine
, _
, _
= self
.get_tx_value(tx
)
532 if not self
.is_mine(o_addr
):
534 default_label
= self
.labels
[o_addr
]
536 default_label
= '>' + o_addr
539 default_label
= '(internal)'
543 if self
.is_mine(o_addr
) and not self
.is_change(o_addr
):
548 if self
.is_mine(o_addr
):
554 dest_label
= self
.labels
.get(o_addr
)
556 default_label
= self
.labels
[o_addr
]
558 default_label
= '<' + o_addr
563 def sign_transaction(self
, tx
, keypairs
, password
):
565 run_hook('sign_transaction', tx
, password
)
568 def check_new_tx(self
, tx_hash
, tx
):
569 # 1 check that tx is referenced in addr_history.
571 for addr
, hist
in self
.history
.items():
572 if hist
== ['*']:continue
573 for txh
, height
in hist
:
575 addresses
.append(addr
)
580 # 2 check that referencing addresses are in the tx
581 for addr
in addresses
:
582 if not tx
.has_address(addr
):
588 class Deterministic_Wallet(Abstract_Wallet
):
590 def __init__(self
, storage
):
591 Abstract_Wallet
.__init
__(self
, storage
)
594 return self
.seed
!= ''
596 def is_deterministic(self
):
599 def is_watching_only(self
):
600 return not self
.has_seed()
602 def get_seed(self
, password
):
603 return pw_decode(self
.seed
, password
)
605 def get_mnemonic(self
, password
):
606 return self
.get_seed(password
)
608 def num_unused_trailing_addresses(self
, addresses
):
610 for a
in addresses
[::-1]:
611 if self
.history
.get(a
):break
615 def min_acceptable_gap(self
):
616 # fixme: this assumes wallet is synchronized
620 for account
in self
.accounts
.values():
621 addresses
= account
.get_addresses(0)
622 k
= self
.num_unused_trailing_addresses(addresses
)
623 for a
in addresses
[0:-k
]:
624 if self
.history
.get(a
):
628 if n
> nmax
: nmax
= n
632 def address_is_old(self
, address
):
634 h
= self
.history
.get(address
, [])
638 # for tx_hash, tx_height in h:
642 # tx_age = self.network.get_local_height() - tx_height + 1
648 class OldWallet(Deterministic_Wallet
):
650 def get_seed(self
, password
):
651 seed
= pw_decode(self
.seed
, password
).encode('utf8')
654 def check_password(self
, password
):
655 seed
= self
.get_seed(password
)
656 self
.accounts
[0].check_seed(seed
)
658 def get_mnemonic(self
, password
):
660 s
= self
.get_seed(password
)
661 return ' '.join(mnemonic
.mn_encode(s
))
663 # former WalletFactory
664 class Wallet(object):
666 def __new__(self
, storage
):
667 config
= storage
.config
669 if not storage
.file_exists
:
670 print ("Wallet does no exist")
673 seed_version
= storage
.get('seed_version')
675 seed_version
= OLD_SEED_VERSION
if len(storage
.get('master_public_key')) == 128 else NEW_SEED_VERSION
677 if seed_version
== OLD_SEED_VERSION
:
678 return OldWallet(storage
)
679 elif seed_version
== NEW_SEED_VERSION
:
680 return NewWallet(storage
)
682 msg
= "This wallet seed is not supported."
683 if seed_version
in [5]:
684 msg
+= "\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version