timestamped top-level wallet preferences

This commit is contained in:
Lex Berezhny 2019-10-12 19:33:16 -04:00
parent c51cc02a87
commit 37ae302fc6
10 changed files with 211 additions and 158 deletions

View file

@ -999,7 +999,7 @@ class Daemon(metaclass=JSONRPCServerType):
Preferences management.
"""
def jsonrpc_preference_get(self, key=None, account_id=None, wallet_id=None):
def jsonrpc_preference_get(self, key=None, wallet_id=None):
"""
Get preference value for key or all values if not key is passed in.
@ -1008,21 +1008,19 @@ class Daemon(metaclass=JSONRPCServerType):
Options:
--key=<key> : (str) key associated with value
--account_id=<account_id> : (str) id of the account containing value
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
Returns:
(dict) Dictionary of preference(s)
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_default(account_id)
if key:
if key in account.preferences:
return {key: account.preferences[key]}
if key in wallet.preferences:
return {key: wallet.preferences[key]}
return
return account.preferences
return wallet.preferences.to_dict_without_ts()
def jsonrpc_preference_set(self, key, value, account_id=None, wallet_id=None):
def jsonrpc_preference_set(self, key, value, wallet_id=None):
"""
Set preferences
@ -1032,18 +1030,15 @@ class Daemon(metaclass=JSONRPCServerType):
Options:
--key=<key> : (str) key associated with value
--value=<key> : (str) key associated with value
--account_id=<account_id> : (str) id of the account containing value
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
Returns:
(dict) Dictionary with key/value of new preference
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_default(account_id)
if value and isinstance(value, str) and value[0] in ('[', '{'):
value = json.loads(value)
account.preferences[key] = value
account.modified_on = time.time()
wallet.preferences[key] = value
wallet.save()
return {key: value}
@ -1342,7 +1337,7 @@ class Daemon(metaclass=JSONRPCServerType):
account.name = new_name
change_made = True
if default:
if default and wallet.default_account != account:
wallet.accounts.remove(account)
wallet.accounts.insert(0, account)
change_made = True
@ -1578,14 +1573,14 @@ class Daemon(metaclass=JSONRPCServerType):
return hexlify(wallet.hash).decode()
@requires("wallet")
def jsonrpc_sync_apply(self, password, data=None, encrypt_password=None, wallet_id=None):
async def jsonrpc_sync_apply(self, password, data=None, encrypt_password=None, wallet_id=None, blocking=False):
"""
Apply incoming synchronization data, if provided, and then produce a sync hash and
an encrypted wallet.
Usage:
sync_apply <password> [--data=<data>] [--encrypt-password=<encrypt_password>]
[--wallet_id=<wallet_id>]
[--wallet_id=<wallet_id>] [--blocking]
Options:
--password=<password> : (str) password to decrypt incoming and encrypt outgoing data
@ -1593,6 +1588,7 @@ class Daemon(metaclass=JSONRPCServerType):
--encrypt-password=<encrypt_password> : (str) password to encrypt outgoing data if different
from the decrypt password, used during password changes
--wallet_id=<wallet_id> : (str) wallet being sync'ed
--blocking : (bool) wait until any new accounts have sync'ed
Returns:
(map) sync hash and data
@ -1600,23 +1596,16 @@ class Daemon(metaclass=JSONRPCServerType):
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if data is not None:
decrypted_data = Wallet.unpack(password, data)
for account_data in decrypted_data['accounts']:
_, _, pubkey = LBCAccount.keys_from_dict(self.ledger, account_data)
account_id = pubkey.address
local_match = None
for local_account in wallet.accounts:
if account_id == local_account.id:
local_match = local_account
break
if local_match is not None:
local_match.apply(account_data)
added_accounts = wallet.merge(self.wallet_manager, password, data)
if added_accounts and self.ledger.network.is_connected:
if blocking:
await asyncio.wait([
a.ledger.subscribe_account(a) for a in added_accounts
])
else:
new_account = LBCAccount.from_dict(self.ledger, wallet, account_data)
if self.ledger.network.is_connected:
for new_account in added_accounts:
asyncio.create_task(self.ledger.subscribe_account(new_account))
wallet.save()
encrypted = wallet.pack(encrypt_password or password)
return {
'hash': self.jsonrpc_sync_hash(wallet_id),

View file

@ -151,11 +151,11 @@ class CommandTestCase(IntegrationTestCase):
wallet_node.manager.old_db = daemon.storage
return daemon
async def confirm_tx(self, txid):
async def confirm_tx(self, txid, ledger=None):
""" Wait for tx to be in mempool, then generate a block, wait for tx to be in a block. """
await self.on_transaction_id(txid)
await self.on_transaction_id(txid, ledger)
await self.generate(1)
await self.on_transaction_id(txid)
await self.on_transaction_id(txid, ledger)
return txid
async def on_transaction_dict(self, tx):

View file

@ -28,7 +28,6 @@ class Account(BaseAccount):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_keys = {}
self.preferences = {}
@property
def hash(self) -> bytes:
@ -37,10 +36,9 @@ class Account(BaseAccount):
h.update(cert.encode())
return h.digest()
def apply(self, d: dict):
super().apply(d)
def merge(self, d: dict):
super().merge(d)
self.channel_keys.update(d.get('certificates', {}))
self.preferences.update(d.get('preferences', {}))
def add_channel_private_key(self, private_key):
public_key_bytes = private_key.get_verifying_key().to_der()
@ -120,22 +118,17 @@ class Account(BaseAccount):
def from_dict(cls, ledger, wallet, d: dict) -> 'Account':
account = super().from_dict(ledger, wallet, d)
account.channel_keys = d.get('certificates', {})
account.preferences = d.get('preferences', {})
return account
def to_dict(self, include_channel_keys=True, include_preferences=True):
def to_dict(self, include_channel_keys=True):
d = super().to_dict()
if include_channel_keys:
d['certificates'] = self.channel_keys
if include_preferences and self.preferences:
d['preferences'] = self.preferences
return d
async def get_details(self, **kwargs):
details = await super().get_details(**kwargs)
details['certificates'] = len(self.channel_keys)
if self.preferences:
details['preferences'] = self.preferences
return details
def get_transaction_history(self, **constraints):

View file

@ -1,114 +1,66 @@
from unittest import mock
from torba.orchstr8.node import WalletNode, SPVNode
from torba.testcase import AsyncioTestCase
from lbry.conf import Config
from lbry.wallet import LbryWalletManager, RegTestLedger
from lbry.extras.daemon.Daemon import Daemon
from lbry.extras.daemon.Components import WalletComponent
from lbry.extras.daemon.Components import (
DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,
UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT
)
from lbry.extras.daemon.ComponentManager import ComponentManager
import asyncio
from lbry.testcase import CommandTestCase
from binascii import unhexlify
class AccountSynchronization(AsyncioTestCase):
class WalletSynchronization(CommandTestCase):
SEED = "carbon smart garage balance margin twelve chest sword toast envelope bottom stomach absent"
async def asyncSetUp(self):
self.wallet_node = WalletNode(LbryWalletManager, RegTestLedger)
await self.wallet_node.start(
SPVNode(None),
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach absent",
False
async def test_sync(self):
daemon = self.daemon
daemon2 = await self.add_daemon(
seed="chest sword toast envelope bottom stomach absent "
"carbon smart garage balance margin twelve"
)
self.account = self.wallet_node.account
address = (await daemon2.wallet_manager.default_account.receiving.get_addresses(limit=1, only_usable=True))[0]
sendtxid = await self.blockchain.send_to_address(address, 1)
await self.confirm_tx(sendtxid, daemon2.ledger)
conf = Config()
conf.data_dir = self.wallet_node.data_path
conf.wallet_dir = self.wallet_node.data_path
conf.download_dir = self.wallet_node.data_path
conf.share_usage_data = False
conf.use_upnp = False
conf.reflect_streams = False
conf.blockchain_name = 'lbrycrd_regtest'
conf.lbryum_servers = [('localhost', 50001)]
conf.reflector_servers = []
conf.known_dht_nodes = []
# Preferences
self.assertFalse(daemon.jsonrpc_preference_get())
self.assertFalse(daemon2.jsonrpc_preference_get())
def wallet_maker(component_manager):
self.wallet_component = WalletComponent(component_manager)
self.wallet_component.wallet_manager = self.wallet_node.manager
self.wallet_component._running = True
return self.wallet_component
daemon.jsonrpc_preference_set("one", "1")
daemon.jsonrpc_preference_set("conflict", "1")
daemon.jsonrpc_preference_set("fruit", '["peach", "apricot"]')
await asyncio.sleep(1)
daemon2.jsonrpc_preference_set("two", "2")
daemon2.jsonrpc_preference_set("conflict", "2")
conf.components_to_skip = [
DHT_COMPONENT, UPNP_COMPONENT, HASH_ANNOUNCER_COMPONENT,
PEER_PROTOCOL_SERVER_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT
]
self.daemon = Daemon(conf, ComponentManager(
conf, skip_components=conf.components_to_skip, wallet=wallet_maker
))
await self.daemon.initialize()
self.assertDictEqual(daemon.jsonrpc_preference_get(), {
"one": "1", "conflict": "1", "fruit": ["peach", "apricot"]
})
self.assertDictEqual(daemon2.jsonrpc_preference_get(), {"two": "2", "conflict": "2"})
async def asyncTearDown(self):
self.wallet_component._running = False
await self.daemon.stop(shutdown_runner=False)
self.assertEqual(len((await daemon.jsonrpc_account_list())['lbc_regtest']), 1)
@mock.patch('time.time', mock.Mock(return_value=12345))
def test_sync(self):
starting_hash = '69afcd60a300f47933917d77ef011beeeb4decfafebbda91c144c84282c6814f'
self.account.modified_on = 123.456
self.assertEqual(self.daemon.jsonrpc_sync_hash(), starting_hash)
self.assertEqual(self.daemon.jsonrpc_sync_apply('password')['hash'], starting_hash)
self.assertFalse(self.account.channel_keys)
data = await daemon2.jsonrpc_sync_apply('password')
await daemon.jsonrpc_sync_apply('password', data=data['data'], blocking=True)
hash_w_cert = '974721f42dab42657b5911b7caf4af98ce4d3879eea6ac23d50c1d79bc5020ef'
add_cert = (
'czo4MTkyOjE2OjE6qs3JRvS/bhX8p1JD68sqyA2Qhx3EVTqskhqEAwtfAUfUsQqeJ1rtMdRf40vkGKnpt4NT0b'
'XEqb5O+lba4nkLF7vZENhc2zuOrjobCPVbiVHNwOfH56Ayrh1ts5LMcnl5+Mk1BUyGCwXcqEg2KiUkd3YZpiHQ'
'T7WfcODcU6l7IRivb8iawCebZJx9waVyQoqEDKwZUY1i5HA0VLC+s5cV7it1AWbewiyWOQtZdEPzNY44oXLJex'
'SirElQqDqNZyl3Hjy8YqacBbSYoejIRnmXpC9y25keP6hep3f9i1K2HDNwhwns1W1vhuzuO2Gy9+a0JlVm5mwc'
'N2pqO4tCZr6tE3aym2FaSAunOi7QYVFMI6arb9Gvn9P+T+WRiFYfzwDFVR+j5ZPmUDXxHisy5OF163jH61wbBY'
'pPienjlVtDOxoZmA8+AwWXKRdINsRcull9pu7EVCq5yQmrmxoPbLxNh5pRGrBB0JwCCOMIf+KPwS+7Z6dDbiwO'
'2NUpk8USJMTmXmFDCr2B0PJiG6Od2dD2oGN0F7aYZvUuKbqj8eDrJMe/zLbhq47jUjkJFCvtxUioo63ORk1pzH'
'S0/X4/6/95PRSMaXm4DcZ9BdyxR2E/AKc8UN6AL5rrn6quXkC6R3ZhKgN3Si2S9y6EGFsL7dgzX331U08ZviLj'
'NsrG0EKUnf+TGQ42MqnLQBOiO/ZoAwleOzNZnCYOQQ14Mm8y17xUpmdWRDiRKpAOJU22jKnxtqQ='
self.assertEqual(len((await daemon.jsonrpc_account_list())['lbc_regtest']), 2)
self.assertDictEqual(
# "two" key added and "conflict" value changed to "2"
daemon.jsonrpc_preference_get(),
{"one": "1", "two": "2", "conflict": "2", "fruit": ["peach", "apricot"]}
)
self.daemon.jsonrpc_sync_apply('password', data=add_cert)
self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert)
self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'})
# applying the same diff is idempotent
self.daemon.jsonrpc_sync_apply('password', data=add_cert)
self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert)
self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'})
# Channel Certificate
channel = await daemon2.jsonrpc_channel_create('@foo', '0.1')
await daemon2.ledger.wait(channel)
await self.generate(1)
await daemon2.ledger.wait(channel)
@mock.patch('time.time', mock.Mock(return_value=12345))
def test_account_preferences_syncing(self):
starting_hash = '69afcd60a300f47933917d77ef011beeeb4decfafebbda91c144c84282c6814f'
self.account.modified_on = 123.456
self.assertEqual(self.daemon.jsonrpc_sync_hash(), starting_hash)
self.assertEqual(self.daemon.jsonrpc_sync_apply('password')['hash'], starting_hash)
self.assertFalse(self.daemon.jsonrpc_preference_get())
# both daemons will have the channel but only one has the cert so far
self.assertEqual(len(await daemon.jsonrpc_channel_list()), 1)
self.assertEqual(len(daemon.wallet_manager.default_wallet.accounts[1].channel_keys), 0)
self.assertEqual(len(await daemon2.jsonrpc_channel_list()), 1)
self.assertEqual(len(daemon2.wallet_manager.default_account.channel_keys), 1)
hash_w_pref = '2fe43f0b2f8bbf1fbb55537f862d8bcb0823791019a7151c848bd5f5bd32d336'
add_pref = (
'czo4MTkyOjE2OjE6Jgn3nAGrfYP2usMA4KQ/73+YHAwMyiGdSWuxCmgZKlpwSpnfQv8R7R0tum/n2oTSBQxjdL'
'OlTW+tv/G5L2GfQ5op3xaT89gN+F/JJnvf3cdWvYH7Nc+uTUMb7cKhJP7hQvFW5bb1Y3jX3EBBY00Jkqyj9RCR'
'XPtbLVu71KbVRvCAR/oAnMEsgD+ITsC3WkXMwE3BS2LjJDQmeqbH4YXNdcjJN/JzQ6fxOmr3Uk1GqnpuhFsta8'
'H14ViRilq1pLKOSZIN80rrm5cKq45nFO5kFeoqBCEaal4u2/OkX9nOnpQlO3E95wD8hkCmZ3i20aSte6nqwqXx'
'ZKVRZqR2a0TjwVWB8kPXPA2ewKvPILaj190bXPl8EVu+TAnTCQwMgytinYjtcKNZmMz3ENJyI2mCANwpWlX7xl'
'y/J+qLi5b9N+agghTxggs5rVJ/hkaue7GS542dXDrwMrw9nwGqNw3dS/lcU+1wRUQ0fnHwb/85XbbwyO2aDj2i'
'DFNkdyLyUIiIUvB1JfWAnWqX3vQcL1REK1ePgUei7dCHJ3WyWdsRx3cVXzlK8yOPkf0N6d3AKrZQWVebwDC7Nd'
'eL4sDW8AkaXuBIrbuZw6XUHd6WI0NvU/q10j2qMm0YoXSu+dExou1/1THwx5g86MxcX5nwodKUEVCOTzKMyrLz'
'CRsitH/+dAXhZNRp/FbnDCGBMyD3MOYCjZvAFbCZUasoRwqponxILw=='
data = await daemon2.jsonrpc_sync_apply('password')
await daemon.jsonrpc_sync_apply('password', data=data['data'], blocking=True)
# both daemons have the cert after sync'ing
self.assertEqual(
daemon2.wallet_manager.default_account.channel_keys,
daemon.wallet_manager.default_wallet.accounts[1].channel_keys
)
self.daemon.jsonrpc_sync_apply('password', data=add_pref)
self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_pref)
self.assertEqual(self.daemon.jsonrpc_preference_get(), {"fruit": ["apple", "orange"]})
self.daemon.jsonrpc_preference_set("fruit", ["peach", "apricot"])
self.assertEqual(self.daemon.jsonrpc_preference_get(), {"fruit": ["peach", "apricot"]})
self.assertNotEqual(self.daemon.jsonrpc_sync_hash(), hash_w_pref)

View file

@ -178,7 +178,7 @@ class TestHierarchicalDeterministicAccount(AsyncioTestCase):
account_data['ledger'] = 'btc_mainnet'
self.assertDictEqual(account_data, account.to_dict())
def test_apply_diff(self):
def test_merge_diff(self):
account_data = {
'name': 'My Account',
'modified_on': 123.456,
@ -213,13 +213,13 @@ class TestHierarchicalDeterministicAccount(AsyncioTestCase):
account_data['address_generator']['receiving']['gap'] = 8
account_data['address_generator']['receiving']['maximum_uses_per_address'] = 9
account.apply(account_data)
account.merge(account_data)
# no change because modified_on is not newer
self.assertEqual(account.name, 'My Account')
account_data['modified_on'] = 200.00
account.apply(account_data)
account.merge(account_data)
self.assertEqual(account.name, 'Changed Name')
self.assertEqual(account.change.gap, 6)
self.assertEqual(account.change.maximum_uses_per_address, 7)

View file

@ -1,12 +1,13 @@
import tempfile
from binascii import hexlify
from unittest import TestCase, mock
from torba.testcase import AsyncioTestCase
from torba.coin.bitcoinsegwit import MainNetLedger as BTCLedger
from torba.coin.bitcoincash import MainNetLedger as BCHLedger
from torba.client.basemanager import BaseWalletManager
from torba.client.wallet import Wallet, WalletStorage
from torba.client.wallet import Wallet, WalletStorage, TimestampedPreferences
class TestWalletCreation(AsyncioTestCase):
@ -32,6 +33,7 @@ class TestWalletCreation(AsyncioTestCase):
wallet_dict = {
'version': 1,
'name': 'Main Wallet',
'preferences': {},
'accounts': [
{
'name': 'An Account',
@ -60,7 +62,7 @@ class TestWalletCreation(AsyncioTestCase):
wallet = Wallet.from_storage(storage, self.manager)
self.assertEqual(wallet.name, 'Main Wallet')
self.assertEqual(
hexlify(wallet.hash), b'9f462b8dd802eb8c913e54f09a09827ebc14abbc13f33baa90d8aec5ae920fc7'
hexlify(wallet.hash), b'1bd61fbe18875cb7828c466022af576104ed861c8a1fdb1dadf5e39417a68483'
)
self.assertEqual(len(wallet.accounts), 1)
account = wallet.default_account
@ -91,3 +93,56 @@ class TestWalletCreation(AsyncioTestCase):
wallet = Wallet.from_storage(wallet_storage, manager)
self.assertEqual(account.public_key.address, wallet.default_account.public_key.address)
def test_merge(self):
wallet1 = Wallet()
wallet1.preferences['one'] = 1
wallet1.preferences['conflict'] = 1
wallet1.generate_account(self.btc_ledger)
wallet2 = Wallet()
wallet2.preferences['two'] = 2
wallet2.preferences['conflict'] = 2 # will be more recent
wallet2.generate_account(self.btc_ledger)
self.assertEqual(len(wallet1.accounts), 1)
self.assertEqual(wallet1.preferences, {'one': 1, 'conflict': 1})
added = wallet1.merge(self.manager, 'password', wallet2.pack('password'))
self.assertEqual(added[0].id, wallet2.default_account.id)
self.assertEqual(len(wallet1.accounts), 2)
self.assertEqual(wallet1.accounts[1].id, wallet2.default_account.id)
self.assertEqual(wallet1.preferences, {'one': 1, 'two': 2, 'conflict': 2})
class TestTimestampedPreferences(TestCase):
def test_hash(self):
p = TimestampedPreferences()
self.assertEqual(
hexlify(p.hash), b'44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
)
with mock.patch('time.time', mock.Mock(return_value=12345)):
p['one'] = 1
self.assertEqual(
hexlify(p.hash), b'c9e82bf4cb099dd0125f78fa381b21a8131af601917eb531e1f5f980f8f3da66'
)
def test_merge(self):
p1 = TimestampedPreferences()
p2 = TimestampedPreferences()
with mock.patch('time.time', mock.Mock(return_value=10)):
p1['one'] = 1
p1['conflict'] = 1
with mock.patch('time.time', mock.Mock(return_value=20)):
p2['two'] = 2
p2['conflict'] = 2
# conflict in p2 overrides conflict in p1
p1.merge(p2.data)
self.assertEqual(p1, {'one': 1, 'two': 2, 'conflict': 2})
# have a newer conflict in p1 so it is not overridden this time
with mock.patch('time.time', mock.Mock(return_value=21)):
p1['conflict'] = 1
p1.merge(p2.data)
self.assertEqual(p1, {'one': 1, 'two': 2, 'conflict': 1})

View file

@ -42,7 +42,7 @@ class AddressManager:
d['change'] = change_dict
return d
def apply(self, d: dict):
def merge(self, d: dict):
pass
def to_dict_instance(self) -> Optional[dict]:
@ -101,7 +101,7 @@ class HierarchicalDeterministic(AddressManager):
cls(account, 1, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1}))
)
def apply(self, d: dict):
def merge(self, d: dict):
self.gap = d.get('gap', self.gap)
self.maximum_uses_per_address = d.get('maximum_uses_per_address', self.maximum_uses_per_address)
@ -310,7 +310,7 @@ class BaseAccount:
'modified_on': self.modified_on
}
def apply(self, d: dict):
def merge(self, d: dict):
if d.get('modified_on', 0) > self.modified_on:
self.name = d['name']
self.modified_on = d.get('modified_on', time.time())
@ -318,7 +318,7 @@ class BaseAccount:
for chain_name in ('change', 'receiving'):
if chain_name in d['address_generator']:
chain_object = getattr(self, chain_name)
chain_object.apply(d['address_generator'][chain_name])
chain_object.merge(d['address_generator'][chain_name])
@property
def hash(self) -> bytes:

View file

@ -1,8 +1,10 @@
import os
import time
import stat
import json
import zlib
import typing
from collections import UserDict
from typing import List, Sequence, MutableSequence, Optional
from hashlib import sha256
from operator import attrgetter
@ -12,6 +14,36 @@ if typing.TYPE_CHECKING:
from torba.client import basemanager, baseaccount, baseledger
class TimestampedPreferences(UserDict):
def __getitem__(self, key):
return self.data[key]['value']
def __setitem__(self, key, value):
self.data[key] = {
'value': value,
'ts': time.time()
}
def __repr__(self):
return repr(self.to_dict_without_ts())
def to_dict_without_ts(self):
return {
key: value['value'] for key, value in self.data.items()
}
@property
def hash(self):
return sha256(json.dumps(self.data).encode()).digest()
def merge(self, other: dict):
for key, value in other.items():
if key in self.data and value['ts'] < self.data[key]['ts']:
continue
self.data[key] = value
class Wallet:
""" The primary role of Wallet is to encapsulate a collection
of accounts (seed/private keys) and the spending rules / settings
@ -19,11 +51,14 @@ class Wallet:
by physical files on the filesystem.
"""
preferences: TimestampedPreferences
def __init__(self, name: str = 'Wallet', accounts: MutableSequence['baseaccount.BaseAccount'] = None,
storage: 'WalletStorage' = None) -> None:
storage: 'WalletStorage' = None, preferences: dict = None) -> None:
self.name = name
self.accounts = accounts or []
self.storage = storage or WalletStorage()
self.preferences = TimestampedPreferences(preferences or {})
@property
def id(self):
@ -75,6 +110,7 @@ class Wallet:
json_dict = storage.read()
wallet = cls(
name=json_dict.get('name', 'Wallet'),
preferences=json_dict.get('preferences', {}),
storage=storage
)
account_dicts: Sequence[dict] = json_dict.get('accounts', [])
@ -87,6 +123,7 @@ class Wallet:
return {
'version': WalletStorage.LATEST_VERSION,
'name': self.name,
'preferences': self.preferences.data,
'accounts': [a.to_dict() for a in self.accounts]
}
@ -96,6 +133,7 @@ class Wallet:
@property
def hash(self) -> bytes:
h = sha256()
h.update(self.preferences.hash)
for account in sorted(self.accounts, key=attrgetter('id')):
h.update(account.hash)
return h.digest()
@ -111,6 +149,27 @@ class Wallet:
decompressed = zlib.decompress(decrypted)
return json.loads(decompressed)
def merge(self, manager: 'basemanager.BaseWalletManager',
password: str, data: str) -> List['baseaccount.BaseAccount']:
added_accounts = []
decrypted_data = self.unpack(password, data)
self.preferences.merge(decrypted_data.get('preferences', {}))
for account_dict in decrypted_data['accounts']:
ledger = manager.get_or_create_ledger(account_dict['ledger'])
_, _, pubkey = ledger.account_class.keys_from_dict(ledger, account_dict)
account_id = pubkey.address
local_match = None
for local_account in self.accounts:
if account_id == local_account.id:
local_match = local_account
break
if local_match is not None:
local_match.merge(account_dict)
else:
new_account = ledger.account_class.from_dict(ledger, self, account_dict)
added_accounts.append(new_account)
return added_accounts
class WalletStorage:
@ -121,6 +180,7 @@ class WalletStorage:
self._default = default or {
'version': self.LATEST_VERSION,
'name': 'My Wallet',
'preferences': {},
'accounts': []
}

View file

@ -68,7 +68,7 @@ def set_logging(ledger_module, level, handler=None):
class Conductor:
def __init__(self, ledger_module=None, manager_module=None, verbosity=logging.WARNING,
enable_segwit=False):
enable_segwit=False, seed=None):
self.ledger_module = ledger_module or get_ledger_from_environment()
self.manager_module = manager_module or get_manager_from_environment()
self.spv_module = get_spvserver_from_ledger(self.ledger_module)
@ -76,7 +76,9 @@ class Conductor:
self.blockchain_node = get_blockchain_node_from_ledger(self.ledger_module)
self.blockchain_node.segwit_enabled = enable_segwit
self.spv_node = SPVNode(self.spv_module)
self.wallet_node = WalletNode(self.manager_module, self.ledger_module.RegTestLedger)
self.wallet_node = WalletNode(
self.manager_module, self.ledger_module.RegTestLedger, default_seed=seed
)
set_logging(self.ledger_module, verbosity)
@ -138,7 +140,7 @@ class Conductor:
class WalletNode:
def __init__(self, manager_class: Type[BaseWalletManager], ledger_class: Type[BaseLedger],
verbose: bool = False, port: int = 5280) -> None:
verbose: bool = False, port: int = 5280, default_seed: str = None) -> None:
self.manager_class = manager_class
self.ledger_class = ledger_class
self.verbose = verbose
@ -148,6 +150,7 @@ class WalletNode:
self.account: Optional[BaseAccount] = None
self.data_path: Optional[str] = None
self.port = port
self.default_seed = default_seed
async def start(self, spv_node: 'SPVNode', seed=None, connect=True):
self.data_path = tempfile.mkdtemp()
@ -168,12 +171,12 @@ class WalletNode:
})
self.ledger = self.manager.ledgers[self.ledger_class]
self.wallet = self.manager.default_wallet
if seed is None and self.wallet is not None:
self.wallet.generate_account(self.ledger)
elif self.wallet is not None:
if seed or self.default_seed:
self.ledger.account_class.from_dict(
self.ledger, self.wallet, {'seed': seed}
self.ledger, self.wallet, {'seed': seed or self.default_seed}
)
elif self.wallet is not None:
self.wallet.generate_account(self.ledger)
else:
raise ValueError('Wallet is required.')
self.account = self.wallet.default_account

View file

@ -183,6 +183,7 @@ class AdvanceTimeTestCase(AsyncioTestCase):
class IntegrationTestCase(AsyncioTestCase):
SEED = None
LEDGER = None
MANAGER = None
ENABLE_SEGWIT = False
@ -201,7 +202,7 @@ class IntegrationTestCase(AsyncioTestCase):
async def asyncSetUp(self):
self.conductor = Conductor(
ledger_module=self.LEDGER, manager_module=self.MANAGER, verbosity=self.VERBOSITY,
enable_segwit=self.ENABLE_SEGWIT
enable_segwit=self.ENABLE_SEGWIT, seed=self.SEED
)
await self.conductor.start_blockchain()
self.addCleanup(self.conductor.stop_blockchain)