Compare commits

...

2 Commits

Author SHA1 Message Date
tobtoht
3cc9d65c93
Merge pull request #10153
9239d36 wallet: identify spends in pool when scanning (j-berman)
2025-10-27 18:51:14 +00:00
j-berman
9239d36691 wallet: identify spends in pool when scanning
- Make sure to mark identified spends in the pool as spends. The
wallet might not know these have been spent if it wasn't the wallet
that relayed the tx to the daemon, or the wallet was cleared via
rescan_bc.
- Make sure to add spends to m_unconfirmed_txs if not present.
- Make sure to process the entire pool again if refreshing for
the first time. The wallet fetches pool and blocks at the same
time. The wallet scans blocks first, then pool. If the wallet
identifies received outputs in the chain, then it may have spent
those received outputs in the pool. So we make sure to re-process
the entire pool again after scanning the chain for the first time.
- Multisig wallets that know about spent key images can now detect
spend txs in the pool. Update tests for that.
2025-10-07 11:02:17 -07:00
4 changed files with 161 additions and 50 deletions

View File

@ -922,6 +922,11 @@ bool get_short_payment_id(crypto::hash8 &payment_id8, const tools::wallet2::pend
return false;
}
uint64_t get_outgoing_amount(const cryptonote::transaction &tx, const uint64_t amount_spent)
{
return tx.version == 1 ? get_outs_money_amount(tx) : (amount_spent - tx.rct_signatures.txnFee);
}
tools::wallet2::tx_construction_data get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx, hw::device &hwdev)
{
tools::wallet2::tx_construction_data construction_data = ptx.construction_data;
@ -2712,10 +2717,10 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
LOG_ERROR("spent funds are from different subaddress accounts; count of incoming/outgoing payments will be incorrect");
subaddr_account = td.m_subaddr_index.major;
subaddr_indices.insert(td.m_subaddr_index.minor);
LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid);
set_spent(it->second, height);
if (!pool)
{
LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid);
set_spent(it->second, height);
if (!ignore_callbacks && 0 != m_callback)
m_callback->on_money_spent(height, txid, tx, amount, tx, td.m_subaddr_index);
@ -2804,21 +2809,33 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
uint64_t fee = miner_tx ? 0 : tx.version == 1 ? tx_money_spent_in_ins - get_outs_money_amount(tx) : tx.rct_signatures.txnFee;
if (tx_money_spent_in_ins > 0 && !pool)
if (tx_money_spent_in_ins > 0)
{
uint64_t self_received = std::accumulate<decltype(tx_money_got_in_outs.begin()), uint64_t>(tx_money_got_in_outs.begin(), tx_money_got_in_outs.end(), 0,
[&subaddr_account] (uint64_t acc, const std::pair<cryptonote::subaddress_index, uint64_t>& p)
{
return acc + (p.first.major == *subaddr_account ? p.second : 0);
});
process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, self_received, *subaddr_account, subaddr_indices);
// if sending to yourself at the same subaddress account, set the outgoing payment amount to 0 so that it's less confusing
if (tx_money_spent_in_ins == self_received + fee)
if (!pool)
{
auto i = m_confirmed_txs.find(txid);
THROW_WALLET_EXCEPTION_IF(i == m_confirmed_txs.end(), error::wallet_internal_error,
"confirmed tx wasn't found: " + string_tools::pod_to_hex(txid));
i->second.m_change = self_received;
process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, self_received, *subaddr_account, subaddr_indices);
// if sending to yourself at the same subaddress account, set the outgoing payment amount to 0 so that it's less confusing
if (tx_money_spent_in_ins == self_received + fee)
{
auto i = m_confirmed_txs.find(txid);
THROW_WALLET_EXCEPTION_IF(i == m_confirmed_txs.end(), error::wallet_internal_error,
"confirmed tx wasn't found: " + string_tools::pod_to_hex(txid));
i->second.m_change = self_received;
}
}
else if (!m_unconfirmed_txs.count(txid))
{
// Add to unconfirmed txs if not already there (e.g. restoring wallet, or running the wallet in parallel to the sending wallet w/same seed)
add_unconfirmed_tx(txid, tx, tx_money_spent_in_ins, {}/*don't know dests*/, crypto::null_hash/*don't know payment_id*/, self_received, *subaddr_account, subaddr_indices);
auto i = m_unconfirmed_txs.find(txid);
THROW_WALLET_EXCEPTION_IF(i == m_unconfirmed_txs.end(), error::wallet_internal_error,
"unconfirmed tx wasn't found: " + string_tools::pod_to_hex(txid));
i->second.m_amount_out = get_outgoing_amount(tx, tx_money_spent_in_ins);
}
}
@ -2967,10 +2984,7 @@ void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::trans
// wallet (eg, we're a cold wallet and the hot wallet sent it). For RCT transactions,
// we only see 0 input amounts, so have to deduce amount out from other parameters.
entry.first->second.m_amount_in = spent;
if (tx.version == 1)
entry.first->second.m_amount_out = get_outs_money_amount(tx);
else
entry.first->second.m_amount_out = spent - tx.rct_signatures.txnFee;
entry.first->second.m_amount_out = get_outgoing_amount(tx, spent);
entry.first->second.m_change = received;
std::vector<tx_extra_field> tx_extra_fields;
@ -4081,6 +4095,18 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo
// Lighwallet refresh done
return;
}
if (!m_first_refresh_done)
{
// We want to process the whole pool again, in case we identify received outputs in the chain we might have spent in the pool
m_pool_info_query_time = 0;
m_scanned_pool_txs[0].clear();
m_scanned_pool_txs[1].clear();
// Clear unconfirmed (received) payments because the data is 100% recovered when scanning
m_unconfirmed_payments.clear();
// Don't clear unconfirmed (sent) txs because some data is not recover-able when scanning (dests)
}
received_money = false;
blocks_fetched = 0;
uint64_t added_blocks = 0;
@ -7475,9 +7501,9 @@ uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> un
return found_money;
}
//----------------------------------------------------------------------------------------------------
void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices)
void wallet2::add_unconfirmed_tx(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices)
{
unconfirmed_transfer_details& utd = m_unconfirmed_txs[cryptonote::get_transaction_hash(tx)];
unconfirmed_transfer_details& utd = m_unconfirmed_txs[txid];
utd.m_amount_in = amount_in;
utd.m_amount_out = 0;
for (const auto &d: dests)
@ -7590,7 +7616,7 @@ void wallet2::commit_tx(pending_tx& ptx)
for(size_t idx: ptx.selected_transfers)
amount_in += m_transfers[idx].amount();
}
add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount, ptx.construction_data.subaddr_account, ptx.construction_data.subaddr_indices);
add_unconfirmed_tx(txid, ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount, ptx.construction_data.subaddr_account, ptx.construction_data.subaddr_indices);
if (store_tx_info() && ptx.tx_key != crypto::null_skey)
{
m_tx_keys[txid] = ptx.tx_key;

View File

@ -1875,7 +1875,7 @@ private:
bool prepare_file_names(const std::string& file_path);
void process_unconfirmed(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height);
void process_outgoing(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices);
void add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices);
void add_unconfirmed_tx(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices);
void generate_genesis(cryptonote::block& b) const;
void check_genesis(const crypto::hash& genesis_hash) const; //throws
bool generate_chacha_key_from_secret_keys(crypto::chacha_key &key) const;

View File

@ -29,6 +29,7 @@
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import json
import random
"""Test multisig transfers
@ -408,10 +409,37 @@ class MultisigTest():
assert len(res.tx_hash_list) == 1
txid = res.tx_hash_list[0]
# Retrieve spent key images from daemon
res = daemon.get_transactions([txid], decode_as_json = True)
assert len(res.txs) == 1
tx = res.txs[0]
assert tx.tx_hash == txid
assert len(tx.as_json) > 0
try:
j = json.loads(tx.as_json)
except:
j = None
assert j
assert len(j['vin']) >= 1
spent_key_images = [vin['key']['k_image'] for vin in j['vin']]
assert len(spent_key_images) == len(j['vin'])
for i in range(len(self.wallet)):
# Check if the wallet knows about any spent key images (all signers *should*, non-signers *might*)
is_a_signer = len([x for x in signers if x == i]) > 0
knows_key_image = False
for ki in spent_key_images:
try:
res = self.wallet[i].frozen(ki)
knows_key_image = True
except AssertionError:
if is_a_signer:
raise ValueError('Signer should know about spent key image')
pass
self.wallet[i].refresh()
res = self.wallet[i].get_transfers()
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if i == signers[-1] else 0)
# Any wallet that knows about any spent key images should be able to detect the spend in the pool
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if knows_key_image else 0)
assert len([x for x in (res['out'] if 'out' in res else []) if x.txid == txid]) == 0
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
@ -507,9 +535,13 @@ class MultisigTest():
txid = res.tx_hash_list[0]
for i in range(len(self.wallet)):
# Make sure wallet knows about the key image
frozen = self.wallet[i].frozen(ki).frozen
assert not frozen
self.wallet[i].refresh()
res = self.wallet[i].get_transfers()
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if i == signers[-1] else 0)
# Since all wallets should have key image, all wallets should be able to detect the spend in the pool
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == 1
assert len([x for x in (res['out'] if 'out' in res else []) if x.txid == txid]) == 0
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)

View File

@ -58,6 +58,17 @@ def diff_incoming_transfers(actual_transfers, expected_transfers):
# wallet2 m_transfers container is ordered and order should be the same across rescans
diff_transfers(actual_transfers, expected_transfers, ignore_order = False)
def restore_wallet(wallet, seed, restore_height = 0, filename = '', password = ''):
try: wallet.close_wallet()
except: pass
if filename != '':
util_resources.remove_wallet_files(filename)
wallet.auto_refresh(enable = False)
wallet.restore_deterministic_wallet(seed = seed, restore_height = restore_height, filename = filename, password = password)
res = wallet.get_transfers()
assert not 'in' in res or len(res['in']) == 0
assert not 'out' in res or len(res.out) == 0
class TransferTest():
def run_test(self):
self.reset()
@ -78,6 +89,7 @@ class TransferTest():
self.check_background_sync()
self.check_background_sync_reorg_recovery()
self.check_subaddress_lookahead()
self.check_pool_scanner()
def reset(self):
print('Resetting blockchain')
@ -265,6 +277,7 @@ class TransferTest():
assert len(res.multisig_txset) == 0
assert len(res.unsigned_txset) == 0
tx_blob = res.tx_blob
running_balances[0] -= 1000000000000 + fee
res = daemon.send_raw_transaction(tx_blob)
assert res.not_relayed == False
@ -306,7 +319,6 @@ class TransferTest():
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
res = daemon.getlastblockheader()
running_balances[0] -= 1000000000000 + fee
running_balances[0] += res.block_header.reward
self.wallet[1].refresh()
running_balances[1] += 1000000000000
@ -1154,14 +1166,6 @@ class TransferTest():
except: invalid_password = True
assert invalid_password
def restore_wallet(wallet, seed, filename = '', password = ''):
wallet.close_wallet()
if filename != '':
util_resources.remove_wallet_files(filename)
wallet.restore_deterministic_wallet(seed = seed, filename = filename, password = password)
wallet.auto_refresh(enable = False)
assert wallet.get_transfers() == {}
def assert_correct_transfers(wallet, expected_transfers, expected_inc_transfers, expected_balance):
diff_transfers(wallet.get_transfers(), expected_transfers)
diff_incoming_transfers(wallet.incoming_transfers(transfer_type = 'all'), expected_inc_transfers)
@ -1171,10 +1175,7 @@ class TransferTest():
# We're testing a sweep because it makes sure background sync can
# properly pick up txs which do not have a change output back to sender.
sender_wallet = self.wallet[0]
try: sender_wallet.close_wallet()
except: pass
sender_wallet.restore_deterministic_wallet(seed = seeds[0])
sender_wallet.auto_refresh(enable = False)
restore_wallet(sender_wallet, seeds[0])
sender_wallet.refresh()
res = sender_wallet.incoming_transfers(transfer_type = 'available')
unlocked = [x for x in res.transfers if x.unlocked and x.amount > 0]
@ -1193,10 +1194,7 @@ class TransferTest():
# set up receiver_wallet
receiver_wallet = self.wallet[1]
try: receiver_wallet.close_wallet()
except: pass
receiver_wallet.restore_deterministic_wallet(seed = seeds[1])
receiver_wallet.auto_refresh(enable = False)
restore_wallet(receiver_wallet, seeds[1])
receiver_wallet.refresh()
res = receiver_wallet.get_transfers()
in_len = 0 if 'in' not in res else len(res['in'])
@ -1267,7 +1265,7 @@ class TransferTest():
# Check stopping a wallet with wallet files saved to disk
for background_sync_type in [reuse_password, custom_password]:
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
background_cache_password = None if background_sync_type == reuse_password else 'background_password'
sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
sender_wallet.start_background_sync()
@ -1279,7 +1277,7 @@ class TransferTest():
# Close wallet while background syncing, then reopen
for background_sync_type in [reuse_password, custom_password]:
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
background_cache_password = None if background_sync_type == reuse_password else 'background_password'
sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
sender_wallet.start_background_sync()
@ -1293,7 +1291,7 @@ class TransferTest():
# Close wallet while syncing normally, then reopen
for background_sync_type in [reuse_password, custom_password]:
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
background_cache_password = None if background_sync_type == reuse_password else 'background_password'
sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
sender_wallet.refresh()
@ -1305,7 +1303,7 @@ class TransferTest():
# Create background cache using custom password, then use it to sync, then reopen main wallet
for background_cache_password in ['background_password', '']:
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
sender_wallet.setup_background_sync(background_sync_type = custom_password, wallet_password = 'test_password', background_cache_password = background_cache_password)
@ -1321,7 +1319,7 @@ class TransferTest():
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# Check that main wallet keeps background cache encrypted with custom password in sync
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = 'background_password')
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
@ -1330,7 +1328,7 @@ class TransferTest():
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# Try using wallet password as custom background password
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
same_password = False
@ -1342,7 +1340,7 @@ class TransferTest():
# Turn off background sync
for background_sync_type in [reuse_password, custom_password]:
restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
restore_wallet(sender_wallet, seeds[0], filename = 'test1', password = 'test_password')
background_cache_password = None if background_sync_type == reuse_password else 'background_password'
sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
if background_sync_type == custom_password:
@ -1367,8 +1365,7 @@ class TransferTest():
sender_wallet.open_wallet('test1', password = 'test_password')
# Sanity check against outgoing wallet restored at height 0
sender_wallet.close_wallet()
sender_wallet.restore_deterministic_wallet(seed = seeds[0], restore_height = 0)
restore_wallet(sender_wallet, seeds[0], restore_height = 0)
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
@ -1417,7 +1414,7 @@ class TransferTest():
assert receiver_wallet.get_balance().balance == expected_receiver_balance
# Check a fresh incoming wallet with wallet files saved to disk and encrypted with password
restore_wallet(receiver_wallet, seeds[1], 'test2', 'test_password')
restore_wallet(receiver_wallet, seeds[1], filename = 'test2', password = 'test_password')
receiver_wallet.setup_background_sync(background_sync_type = reuse_password, wallet_password = 'test_password')
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
@ -1427,7 +1424,7 @@ class TransferTest():
assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
# Close receiver's wallet while background sync is enabled then reopen
restore_wallet(receiver_wallet, seeds[1], 'test2', 'test_password')
restore_wallet(receiver_wallet, seeds[1], filename = 'test2', password = 'test_password')
receiver_wallet.setup_background_sync(background_sync_type = reuse_password, wallet_password = 'test_password')
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
@ -1440,8 +1437,7 @@ class TransferTest():
assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
# Sanity check against incoming wallet restored at height 0
receiver_wallet.close_wallet()
receiver_wallet.restore_deterministic_wallet(seed = seeds[1], restore_height = 0)
restore_wallet(receiver_wallet, seeds[1], restore_height = 0)
receiver_wallet.refresh()
assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
@ -1558,5 +1554,62 @@ class TransferTest():
assert balance_info_0_999['blocks_to_unlock'] == 9
assert balance_info_0_999['time_to_unlock'] == 0
def check_pool_scanner(self):
daemon = Daemon()
print('Checking pool scanner')
# Sync first wallet
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
self.wallet[0].refresh()
# Open second wallet with same seed as first
restore_wallet(self.wallet[1], seeds[0])
assert self.wallet[0].get_address().address == self.wallet[1].get_address().address
# Send to another wallet, spending from first wallet
dst = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': 1000000000000}
res = self.wallet[0].transfer([dst])
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert res.fee > 0
fee = res.fee
# Sync both wallets
self.wallet[0].refresh()
self.wallet[1].refresh()
# Both wallets should be able to detect the spend tx in the pool
res_wallet0 = self.wallet[0].get_transfers()
res_wallet1 = self.wallet[1].get_transfers()
# After restoring, should still be able to detect the spend in the pool
restore_wallet(self.wallet[1], seed = seeds[0])
self.wallet[1].refresh()
res_wallet1_after_restore = self.wallet[1].get_transfers()
for res in [res_wallet0, res_wallet1, res_wallet1_after_restore]:
assert len(res.pending) == 1
assert not 'pool' in res or len(res.pool) == 0
assert not 'failed' in res or len(res.failed) == 0
e = res.pending[0]
assert e.txid == txid
assert e.payment_id in ['', '0000000000000000']
assert e.type == 'pending'
assert e.unlock_time == 0
assert e.subaddr_index.major == 0
assert e.subaddr_indices == [{'major': 0, 'minor': 0}]
assert e.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
assert e.double_spend_seen == False
assert not 'confirmations' in e or e.confirmations == 0
assert e.amount == dst['amount']
assert e.fee == fee
# Mine a block to mine the tx and reset 2nd wallet
daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
restore_wallet(self.wallet[1], seeds[1])
self.wallet[1].refresh()
self.wallet[0].refresh()
if __name__ == '__main__':
TransferTest().run_test()