diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 9f3ac5630..ee621920e 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -1383,6 +1383,71 @@ class Daemon(metaclass=JSONRPCServerType): c.wallets += [wallet_id] return wallet + @requires("wallet") + async def jsonrpc_wallet_export(self, password, wallet_id=None): + """ + Export wallet data + + Wallet must be unlocked to perform this operation. + + Usage: + wallet_export [--wallet_id=] + + Options: + --password= : (str) password to decrypt incoming and encrypt outgoing data + --wallet_id= : (str) wallet being sync'ed + + Returns: + (map) sync hash and data + + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + encrypted = wallet.pack(password) + return { + 'hash': self.jsonrpc_sync_hash(wallet_id), + 'data': encrypted.decode() + } + + + @requires("wallet") + async def jsonrpc_wallet_import(self, password, data, wallet_id=None, blocking=False): + """ + Import wallet data and merge accounts, preferences. + + Wallet must be unlocked to perform this operation. + + Usage: + wallet_import ( | --data=) [--wallet_id=] [--blocking] + + Options: + --password= : (str) password to decrypt incoming and encrypt outgoing data + --data= : (str) incoming wallet data + --wallet_id= : (str) wallet being merged into + --blocking : (bool) wait until any new accounts have merged + + Returns: + (map) sync hash and data + + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + added_accounts = wallet.merge(self.wallet_manager, password, data) + for new_account in added_accounts: + await new_account.maybe_migrate_certificates() + 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: + for new_account in added_accounts: + asyncio.create_task(self.ledger.subscribe_account(new_account)) + wallet.save() + encrypted = wallet.pack(password) + return { + 'hash': self.jsonrpc_sync_hash(wallet_id), + 'data': encrypted.decode() + } + @requires("wallet") async def jsonrpc_wallet_add(self, wallet_id): """ diff --git a/tests/integration/blockchain/test_wallet_commands.py b/tests/integration/blockchain/test_wallet_commands.py index 3de00dd49..0d06929c5 100644 --- a/tests/integration/blockchain/test_wallet_commands.py +++ b/tests/integration/blockchain/test_wallet_commands.py @@ -415,3 +415,69 @@ class WalletEncryptionAndSynchronization(CommandTestCase): daemon2.jsonrpc_wallet_lock() self.assertTrue(await daemon2.jsonrpc_wallet_unlock('password3')) + + async def test_wallet_import_and_export(self): + daemon, daemon2 = self.daemon, self.daemon2 + + # Preferences + self.assertFalse(daemon.jsonrpc_preference_get()) + self.assertFalse(daemon2.jsonrpc_preference_get()) + + daemon.jsonrpc_preference_set("fruit", '["peach", "apricot"]') + daemon.jsonrpc_preference_set("one", "1") + daemon.jsonrpc_preference_set("conflict", "1") + daemon2.jsonrpc_preference_set("another", "A") + await asyncio.sleep(1) + # these preferences will win after merge since they are "newer" + daemon2.jsonrpc_preference_set("two", "2") + daemon2.jsonrpc_preference_set("conflict", "2") + daemon.jsonrpc_preference_set("another", "B") + + self.assertDictEqual(daemon.jsonrpc_preference_get(), { + "one": "1", "conflict": "1", "another": "B", "fruit": ["peach", "apricot"] + }) + self.assertDictEqual(daemon2.jsonrpc_preference_get(), { + "two": "2", "conflict": "2", "another": "A" + }) + + self.assertItemCount(await daemon.jsonrpc_account_list(), 1) + + data = await daemon2.jsonrpc_wallet_export('password') + await daemon.jsonrpc_wallet_import('password', data=data['data'], blocking=True) + + self.assertItemCount(await daemon.jsonrpc_account_list(), 2) + self.assertDictEqual( + # "two" key added and "conflict" value changed to "2" + daemon.jsonrpc_preference_get(), + {"one": "1", "two": "2", "conflict": "2", "another": "B", "fruit": ["peach", "apricot"]} + ) + + # Channel Certificate + # non-deterministic channel + self.daemon2.wallet_manager.default_account.channel_keys['mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8'] = ( + '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95' + '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh' + '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS' + 'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n' + ) + channel = await self.create_nondeterministic_channel('@foo', '0.1', unhexlify( + '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1' + '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16' + 'a97a6d6a4a8effd29d748901bb9789352519cd00b13d' + ), self.daemon2, blocking=True) + await self.confirm_tx(channel['txid'], self.daemon2.ledger) + + # both daemons will have the channel but only one has the cert so far + self.assertItemCount(await daemon.jsonrpc_channel_list(), 1) + self.assertEqual(len(daemon.wallet_manager.default_wallet.accounts[1].channel_keys), 0) + self.assertItemCount(await daemon2.jsonrpc_channel_list(), 1) + self.assertEqual(len(daemon2.wallet_manager.default_account.channel_keys), 1) + + data = await daemon2.jsonrpc_wallet_export('password') + await daemon.jsonrpc_wallet_import('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 + ) \ No newline at end of file