monero/tests/functional_tests/transfer.py
j-berman e71c8bf190 wallet: background sync with just the view key
- When background syncing, the wallet wipes the spend key
from memory and processes all new transactions. The wallet saves
all receives, spends, and "plausible" spends of receives the
wallet does not know key images for.
- When background sync disabled, the wallet processes all
background synced txs and then clears the background sync cache.
- Adding "plausible" spends to the background sync cache ensures
that the wallet does not need to query the daemon to see if any
received outputs were spent while background sync was enabled.
This would harm privacy especially for users of 3rd party daemons.
- To enable the feature in the CLI wallet, the user can set
background-sync to reuse-wallet-password or
custom-background-password and the wallet automatically syncs in
the background when the wallet locks, then processes all
background synced txs when the wallet is unlocked.
- The custom-background-password option enables the user to
open a distinct background wallet that only has a view key saved
and can be opened/closed/synced separately from the main wallet.
When the main wallet opens, it processes the background wallet's
cache.
- To enable the feature in the RPC wallet, there is a new
`/setup_background_sync` endpoint.
- HW, multsig and view-only wallets cannot background sync.
2024-05-24 23:11:58 -07:00

1555 lines
74 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2019-2023, The Monero Project
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of
# conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
# of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import json
import util_resources
import pprint
from deepdiff import DeepDiff
pp = pprint.PrettyPrinter(indent=2)
"""Test simple transfers
"""
from framework.daemon import Daemon
from framework.wallet import Wallet
seeds = [
'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted',
'peeled mixture ionic radar utopia puddle buying illness nuns gadget river spout cavernous bounced paradise drunk looking cottage jump tequila melting went winter adjust spout',
'dilute gutter certain antics pamphlet macro enjoy left slid guarded bogeys upload nineteen bomb jubilee enhanced irritate turnip eggs swung jukebox loudly reduce sedan slid',
]
def diff_transfers(actual_transfers, expected_transfers, ignore_order = True):
# The payments containers aren't ordered; re-scanning can lead to diff orders
diff = DeepDiff(actual_transfers, expected_transfers, ignore_order = ignore_order)
if diff != {}:
pp.pprint(diff)
assert diff == {}
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)
class TransferTest():
def run_test(self):
self.reset()
self.create()
self.mine()
self.transfer()
self.check_get_bulk_payments()
self.check_get_payments()
self.check_double_spend_detection()
self.sweep_dust()
self.sweep_single()
self.check_destinations()
self.check_tx_notes()
self.check_rescan()
self.check_is_key_image_spent()
self.check_multiple_submissions()
self.check_scan_tx()
self.check_subtract_fee_from_outputs()
self.check_background_sync()
self.check_background_sync_reorg_recovery()
def reset(self):
print('Resetting blockchain')
daemon = Daemon()
res = daemon.get_height()
daemon.pop_blocks(res.height - 1)
daemon.flush_txpool()
def create(self):
print('Creating wallets')
self.wallet = [None] * len(seeds)
for i in range(len(seeds)):
self.wallet[i] = Wallet(idx = i)
# close the wallet if any, will throw if none is loaded
try: self.wallet[i].close_wallet()
except: pass
res = self.wallet[i].restore_deterministic_wallet(seed = seeds[i])
def mine(self):
print("Mining some blocks")
daemon = Daemon()
res = daemon.get_info()
height = res.height
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 100)
for i in range(len(self.wallet)):
self.wallet[i].refresh()
res = self.wallet[i].get_height()
assert res.height == height + 100
def transfer(self):
daemon = Daemon()
print("Creating transfer to self")
dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
payment_id = '1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde'
start_balances = [0] * len(self.wallet)
running_balances = [0] * len(self.wallet)
for i in range(len(self.wallet)):
res = self.wallet[i].get_balance()
start_balances[i] = res.balance
running_balances[i] = res.balance
assert res.unlocked_balance <= res.balance
if i == 0:
assert res.blocks_to_unlock == 59 # we've been mining to it
else:
assert res.blocks_to_unlock == 0
print ('Checking short payment IDs cannot be used when not in an integrated address')
ok = False
try: self.wallet[0].transfer([dst], ring_size = 16, payment_id = '1234567812345678', get_tx_key = False)
except: ok = True
assert ok
print ('Checking long payment IDs are rejected')
ok = False
try: self.wallet[0].transfer([dst], ring_size = 16, payment_id = payment_id, get_tx_key = False, get_tx_hex = True)
except: ok = True
assert ok
print ('Checking empty destination is rejected')
ok = False
try: self.wallet[0].transfer([], ring_size = 16, get_tx_key = False)
except: ok = True
assert ok
res = self.wallet[0].transfer([dst], ring_size = 16, get_tx_key = False, get_tx_hex = True)
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert len(res.tx_key) == 0
assert res.amount > 0
amount = res.amount
assert res.fee > 0
fee = res.fee
assert len(res.tx_blob) > 0
tx_weight = res.weight
assert len(res.tx_metadata) == 0
assert len(res.multisig_txset) == 0
assert len(res.unsigned_txset) == 0
unsigned_txset = res.unsigned_txset
res = daemon.get_fee_estimate(10)
assert res.fee > 0
assert res.quantization_mask > 0
expected_fee = (res.fee * 1 * tx_weight + res.quantization_mask - 1) // res.quantization_mask * res.quantization_mask
assert abs(1 - fee / expected_fee) < 0.01
self.wallet[0].refresh()
res = daemon.get_info()
height = res.height
res = self.wallet[0].get_transfers()
assert len(res['in']) == height - 1 # coinbases
assert not 'out' in res or len(res.out) == 0 # not mined yet
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
for e in res['in']:
assert e.type == 'block'
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
running_balances[0] -= fee
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
res = daemon.getlastblockheader()
running_balances[0] += res.block_header.reward
self.wallet[0].refresh()
res = self.wallet[0].get_transfers()
assert len(res['in']) == height # coinbases
assert len(res.out) == 1 # not mined yet
assert not 'pending' in res or len(res.pending) == 0
assert not 'pool' in res or len(res.pool) == 0
assert not 'failed' in res or len(res.failed) == 0
for e in res['in']:
assert e.type == 'block'
e = res.out[0]
assert e.txid == txid
assert e.payment_id in ['', '0000000000000000']
assert e.type == 'out'
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 e.confirmations == 1
res = self.wallet[0].get_height()
wallet_height = res.height
res = self.wallet[0].get_transfer_by_txid(txid)
assert len(res.transfers) == 1
assert res.transfers[0] == res.transfer
t = res.transfer
assert t.txid == txid
assert t.payment_id in ['', '0000000000000000']
assert t.height == wallet_height - 1
assert t.timestamp > 0
assert t.amount == 0 # to self, so it's just "pay a fee" really
assert t.fee == fee
assert t.note == ''
assert len(t.destinations) == 1
assert t.destinations[0] == {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
assert t.type == 'out'
assert t.unlock_time == 0
assert t.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
assert t.double_spend_seen == False
assert t.confirmations == 1
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
print("Creating transfer to another, manual relay")
dst = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': 1000000000000}
res = self.wallet[0].transfer([dst], ring_size = 16, get_tx_key = True, do_not_relay = True, get_tx_hex = True)
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert len(res.tx_key) == 32*2
assert res.amount == 1000000000000
amount = res.amount
assert res.fee > 0
fee = res.fee
assert len(res.tx_blob) > 0
assert len(res.tx_metadata) == 0
assert len(res.multisig_txset) == 0
assert len(res.unsigned_txset) == 0
tx_blob = res.tx_blob
res = daemon.send_raw_transaction(tx_blob)
assert res.not_relayed == False
assert res.low_mixin == False
assert res.double_spend == False
assert res.invalid_input == False
assert res.invalid_output == False
assert res.too_big == False
assert res.overspend == False
assert res.fee_too_low == False
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
self.wallet[1].refresh()
res = self.wallet[1].get_transfers()
assert not 'in' in res or len(res['in']) == 0
assert not 'out' in res or len(res.out) == 0
assert not 'pending' in res or len(res.pending) == 0
assert len(res.pool) == 1
assert not 'failed' in res or len(res.failed) == 0
e = res.pool[0]
assert e.txid == txid
assert e.payment_id in ["", "0000000000000000"] # long payment IDs are now ignored
assert e.type == 'pool'
assert e.unlock_time == 0
assert e.subaddr_index.major == 0
assert e.subaddr_indices == [{'major': 0, 'minor': 0}]
assert e.address == '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW'
assert e.double_spend_seen == False
assert not 'confirmations' in e or e.confirmations == 0
assert e.amount == amount
assert e.fee == fee
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
res = self.wallet[1].get_transfers()
assert len(res['in']) == 1
assert not 'out' in res or len(res.out) == 0
assert not 'pending' in res or len(res.pending) == 0
assert not 'pool' in res or len(res.pool) == 0
assert not 'failed' in res or len(res.failed) == 0
e = res['in'][0]
assert e.txid == txid
assert e.payment_id in ["", "0000000000000000"] # long payment IDs are now ignored
assert e.type == 'in'
assert e.unlock_time == 0
assert e.subaddr_index.major == 0
assert e.subaddr_indices == [{'major': 0, 'minor': 0}]
assert e.address == '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW'
assert e.double_spend_seen == False
assert e.confirmations == 1
assert e.amount == amount
assert e.fee == fee
res = self.wallet[1].get_balance()
assert res.balance == running_balances[1]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 9
print('Creating multi out transfer')
self.wallet[0].refresh()
dst0 = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
dst1 = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': 1100000000000}
dst2 = {'address': '46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 'amount': 1200000000000}
res = self.wallet[0].transfer([dst0, dst1, dst2], ring_size = 16, get_tx_key = True)
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert len(res.tx_key) == 32*2
assert res.amount == 1000000000000 + 1100000000000 + 1200000000000
amount = res.amount
assert res.fee > 0
fee = res.fee
assert len(res.tx_blob) == 0
assert len(res.tx_metadata) == 0
assert len(res.multisig_txset) == 0
assert len(res.unsigned_txset) == 0
unsigned_txset = res.unsigned_txset
running_balances[0] -= 1100000000000 + 1200000000000 + fee
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
res = daemon.getlastblockheader()
running_balances[0] += res.block_header.reward
running_balances[1] += 1100000000000
running_balances[2] += 1200000000000
self.wallet[0].refresh()
res = self.wallet[0].get_transfers()
assert len(res['in']) == height + 2
assert len(res.out) == 3
assert not 'pending' in res or len(res.pending) == 0
assert not 'pool' in res or len(res.pool) == 1
assert not 'failed' in res or len(res.failed) == 0
e = [o for o in res.out if o.txid == txid]
assert len(e) == 1
e = e[0]
assert e.txid == txid
assert e.payment_id in ["", "0000000000000000"] # long payment IDs are now ignored
assert e.type == 'out'
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 e.confirmations == 1
assert e.amount == amount
assert e.fee == fee
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
self.wallet[1].refresh()
res = self.wallet[1].get_transfers()
assert len(res['in']) == 2
assert not 'out' in res or len(res.out) == 0
assert not 'pending' in res or len(res.pending) == 0
assert not 'pool' in res or len(res.pool) == 0
assert not 'failed' in res or len(res.failed) == 0
e = [o for o in res['in'] if o.txid == txid]
assert len(e) == 1
e = e[0]
assert e.txid == txid
assert e.payment_id in ["", "0000000000000000"] # long payment IDs are now ignored
assert e.type == 'in'
assert e.unlock_time == 0
assert e.subaddr_index.major == 0
assert e.subaddr_indices == [{'major': 0, 'minor': 0}]
assert e.address == '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW'
assert e.double_spend_seen == False
assert e.confirmations == 1
assert e.amount == 1100000000000
assert e.fee == fee
res = self.wallet[1].get_balance()
assert res.balance == running_balances[1]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 9
self.wallet[2].refresh()
res = self.wallet[2].get_transfers()
assert len(res['in']) == 1
assert not 'out' in res or len(res.out) == 0
assert not 'pending' in res or len(res.pending) == 0
assert not 'pool' in res or len(res.pool) == 0
assert not 'failed' in res or len(res.failed) == 0
e = [o for o in res['in'] if o.txid == txid]
assert len(e) == 1
e = e[0]
assert e.txid == txid
assert e.payment_id in ["", "0000000000000000"] # long payment IDs are now ignored
assert e.type == 'in'
assert e.unlock_time == 0
assert e.subaddr_index.major == 0
assert e.subaddr_indices == [{'major': 0, 'minor': 0}]
assert e.address == '46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK'
assert e.double_spend_seen == False
assert e.confirmations == 1
assert e.amount == 1200000000000
assert e.fee == fee
res = self.wallet[2].get_balance()
assert res.balance == running_balances[2]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 9
print('Sending to integrated address')
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
i_pid = '1111111122222222'
res = self.wallet[0].make_integrated_address(standard_address = '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', payment_id = i_pid)
i_address = res.integrated_address
res = self.wallet[0].transfer([{'address': i_address, 'amount': 200000000}])
assert len(res.tx_hash) == 32*2
i_txid = res.tx_hash
assert len(res.tx_key) == 32*2
assert res.amount == 200000000
i_amount = res.amount
assert res.fee > 0
fee = res.fee
assert len(res.tx_blob) == 0
assert len(res.tx_metadata) == 0
assert len(res.multisig_txset) == 0
assert len(res.unsigned_txset) == 0
running_balances[0] -= 200000000 + fee
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
res = daemon.getlastblockheader()
running_balances[0] += res.block_header.reward
running_balances[1] += 200000000
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
self.wallet[1].refresh()
res = self.wallet[1].get_balance()
assert res.balance == running_balances[1]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 9
self.wallet[2].refresh()
res = self.wallet[2].get_balance()
assert res.balance == running_balances[2]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 8
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
res = daemon.getlastblockheader()
running_balances[0] += res.block_header.reward
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
assert res.balance == running_balances[0]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 59
self.wallet[1].refresh()
res = self.wallet[1].get_balance()
assert res.balance == running_balances[1]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 8
self.wallet[2].refresh()
res = self.wallet[2].get_balance()
assert res.balance == running_balances[2]
assert res.unlocked_balance <= res.balance
assert res.blocks_to_unlock == 7
def check_get_bulk_payments(self):
print('Checking get_bulk_payments')
daemon = Daemon()
res = daemon.get_info()
height = res.height
self.wallet[0].refresh()
res = self.wallet[0].get_bulk_payments()
assert len(res.payments) >= 83 # at least 83 coinbases
res = self.wallet[0].get_bulk_payments(payment_ids = ['1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde'])
assert 'payments' not in res or len(res.payments) == 0
res = self.wallet[0].get_bulk_payments(min_block_height = height)
assert 'payments' not in res or len(res.payments) == 0
res = self.wallet[0].get_bulk_payments(min_block_height = height - 40)
assert len(res.payments) >= 39 # coinbases
self.wallet[1].refresh()
res = self.wallet[1].get_bulk_payments()
assert len(res.payments) >= 3 # two txes to standard address were sent, plus one to integrated address
res = self.wallet[1].get_bulk_payments(payment_ids = ['1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde'])
assert not 'payments' in res or len(res.payments) == 0 # long payment IDs are now ignored on receipt
res = self.wallet[1].get_bulk_payments(payment_ids = ['ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'])
assert 'payments' not in res or len(res.payments) == 0 # none with that payment id
res = self.wallet[1].get_bulk_payments(payment_ids = ['1111111122222222' + '0'*48])
assert len(res.payments) >= 1 # one tx to integrated address
self.wallet[2].refresh()
res = self.wallet[2].get_bulk_payments()
assert len(res.payments) >= 1 # one tx was sent
res = self.wallet[2].get_bulk_payments(payment_ids = ['1'*64, '1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde', '2'*64])
assert not 'payments' in res or len(res.payments) == 0 # long payment IDs are now ignored
res = self.wallet[1].get_bulk_payments(["1111111122222222"])
assert len(res.payments) >= 1 # we have one of these
def check_get_payments(self):
print('Checking get_payments')
daemon = Daemon()
res = daemon.get_info()
height = res.height
self.wallet[0].refresh()
self.wallet[1].refresh()
res = self.wallet[0].get_payments('1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde')
assert 'payments' not in res or len(res.payments) == 0
res = self.wallet[1].get_payments('1234500000012345abcde00000abcdeff1234500000012345abcde00000abcde')
assert 'payments' not in res or len(res.payments) == 0
res = self.wallet[1].get_payments('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
assert 'payments' not in res or len(res.payments) == 0
res = self.wallet[1].get_payments(payment_id = '1111111122222222' + '0'*48)
assert len(res.payments) >= 1 # one tx to integrated address
def check_double_spend_detection(self):
print('Checking double spend detection')
txes = [[None, None], [None, None]]
for i in range(2):
self.wallet[0].restore_deterministic_wallet(seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted')
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
unlocked_balance = res.unlocked_balance
res = self.wallet[0].sweep_all(address = '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', do_not_relay = True, get_tx_hex = True)
assert len(res.tx_hash_list) == 1
assert len(res.tx_hash_list[0]) == 32*2
txes[i][0] = res.tx_hash_list[0]
assert len(res.fee_list) == 1
assert res.fee_list[0] > 0
assert len(res.amount_list) == 1
assert res.amount_list[0] == unlocked_balance - res.fee_list[0]
assert len(res.tx_blob_list) > 0
assert len(res.tx_blob_list[0]) > 0
assert not 'tx_metadata_list' in res or len(res.tx_metadata_list) == 0
assert not 'multisig_txset' in res or len(res.multisig_txset) == 0
assert not 'unsigned_txset' in res or len(res.unsigned_txset) == 0
assert len(res.tx_blob_list) == 1
txes[i][1] = res.tx_blob_list[0]
daemon = Daemon()
res = daemon.send_raw_transaction(txes[0][1])
assert res.not_relayed == False
assert res.low_mixin == False
assert res.double_spend == False
assert res.invalid_input == False
assert res.invalid_output == False
assert res.too_big == False
assert res.overspend == False
assert res.fee_too_low == False
res = daemon.get_transactions([txes[0][0]])
assert len(res.txs) >= 1
tx = [tx for tx in res.txs if tx.tx_hash == txes[0][0]][0]
assert tx.in_pool
assert not tx.double_spend_seen
res = daemon.send_raw_transaction(txes[1][1])
assert res.not_relayed == False
assert res.low_mixin == False
assert res.double_spend == True
assert res.invalid_input == False
assert res.invalid_output == False
assert res.too_big == False
assert res.overspend == False
assert res.fee_too_low == False
assert res.too_few_outputs == False
res = daemon.get_transactions([txes[0][0]])
assert len(res.txs) >= 1
tx = [tx for tx in res.txs if tx.tx_hash == txes[0][0]][0]
assert tx.in_pool
assert tx.double_spend_seen
def sweep_dust(self):
print("Sweeping dust")
daemon = Daemon()
self.wallet[0].refresh()
res = self.wallet[0].sweep_dust()
assert not 'tx_hash_list' in res or len(res.tx_hash_list) == 0 # there's just one, but it cannot meet the fee
def sweep_single(self):
daemon = Daemon()
print("Sending single output")
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
self.wallet[0].refresh()
res = self.wallet[0].incoming_transfers(transfer_type = 'available')
for t in res.transfers:
assert not t.spent
assert len(res.transfers) > 8 # we mined a lot
index = 8
assert not res.transfers[index].spent
assert res.transfers[index].amount > 0
ki = res.transfers[index].key_image
amount = res.transfers[index].amount
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 10) # ensure unlocked
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
balance = res.balance
res = daemon.is_key_image_spent([ki])
assert len(res.spent_status) == 1
assert res.spent_status[0] == 0
res = self.wallet[0].sweep_single('44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', key_image = ki)
assert len(res.tx_hash) == 64
tx_hash = res.tx_hash
res = daemon.is_key_image_spent([ki])
assert len(res.spent_status) == 1
assert res.spent_status[0] == 2
daemon.generateblocks('44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 1)
res = daemon.is_key_image_spent([ki])
assert len(res.spent_status) == 1
assert res.spent_status[0] == 1
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
new_balance = res.balance
res = daemon.get_transactions([tx_hash], decode_as_json = True)
assert len(res.txs) == 1
tx = res.txs[0]
assert tx.tx_hash == tx_hash
assert not tx.in_pool
assert len(tx.as_json) > 0
try:
j = json.loads(tx.as_json)
except:
j = None
assert j
assert new_balance == balance - amount
assert len(j['vin']) == 1
assert j['vin'][0]['key']['k_image'] == ki
self.wallet[0].refresh()
res = self.wallet[0].incoming_transfers(transfer_type = 'available')
assert len([t for t in res.transfers if t.key_image == ki]) == 0
res = self.wallet[0].incoming_transfers(transfer_type = 'unavailable')
assert len([t for t in res.transfers if t.key_image == ki]) == 1
def check_destinations(self):
daemon = Daemon()
print("Checking transaction destinations")
dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
res = self.wallet[0].transfer([dst])
assert len(res.tx_hash) == 64
tx_hash = res.tx_hash
for i in range(2):
res = self.wallet[0].get_transfers(pending = True, out = True)
l = [x for x in (res.pending if i == 0 else res.out) if x.txid == tx_hash]
assert len(l) == 1
e = l[0]
assert len(e.destinations) == 1
assert e.destinations[0].amount == 1000000000000
assert e.destinations[0].address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
if i == 0:
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
self.wallet[0].refresh()
dst = {'address': '8AsN91rznfkBGTY8psSNkJBg9SZgxxGGRUhGwRptBhgr5XSQ1XzmA9m8QAnoxydecSh5aLJXdrgXwTDMMZ1AuXsN1EX5Mtm', 'amount': 1000000000000}
res = self.wallet[0].transfer([dst])
assert len(res.tx_hash) == 64
tx_hash = res.tx_hash
for i in range(2):
res = self.wallet[0].get_transfers(pending = True, out = True)
l = [x for x in (res.pending if i == 0 else res.out) if x.txid == tx_hash]
assert len(l) == 1
e = l[0]
assert len(e.destinations) == 1
assert e.destinations[0].amount == 1000000000000
assert e.destinations[0].address == '8AsN91rznfkBGTY8psSNkJBg9SZgxxGGRUhGwRptBhgr5XSQ1XzmA9m8QAnoxydecSh5aLJXdrgXwTDMMZ1AuXsN1EX5Mtm'
if i == 0:
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
self.wallet[0].refresh()
dst = {'address': '4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY', 'amount': 1000000000000}
res = self.wallet[0].transfer([dst])
assert len(res.tx_hash) == 64
tx_hash = res.tx_hash
for i in range(2):
res = self.wallet[0].get_transfers(pending = True, out = True)
l = [x for x in (res.pending if i == 0 else res.out) if x.txid == tx_hash]
assert len(l) == 1
e = l[0]
assert len(e.destinations) == 1
assert e.destinations[0].amount == 1000000000000
assert e.destinations[0].address == '4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY'
if i == 0:
daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
self.wallet[0].refresh()
def check_tx_notes(self):
daemon = Daemon()
print('Testing tx notes')
res = self.wallet[0].get_transfers()
assert len(res['in']) > 0
in_txid = res['in'][0].txid
assert len(res['out']) > 0
out_txid = res['out'][0].txid
res = self.wallet[0].get_tx_notes([in_txid, out_txid])
assert res.notes == ['', '']
res = self.wallet[0].set_tx_notes([in_txid, out_txid], ['in txid', 'out txid'])
res = self.wallet[0].get_tx_notes([in_txid, out_txid])
assert res.notes == ['in txid', 'out txid']
res = self.wallet[0].get_tx_notes([out_txid, in_txid])
assert res.notes == ['out txid', 'in txid']
def check_rescan(self):
daemon = Daemon()
print('Testing rescan_spent')
res = self.wallet[0].incoming_transfers(transfer_type = 'all')
transfers = res.transfers
res = self.wallet[0].rescan_spent()
res = self.wallet[0].incoming_transfers(transfer_type = 'all')
assert transfers == res.transfers
for hard in [False, True]:
print('Testing %s rescan_blockchain' % ('hard' if hard else 'soft'))
res = self.wallet[0].incoming_transfers(transfer_type = 'all')
transfers = res.transfers
res = self.wallet[0].get_transfers()
t_in = res['in']
t_out = res.out
res = self.wallet[0].rescan_blockchain(hard = hard)
res = self.wallet[0].incoming_transfers(transfer_type = 'all')
assert transfers == res.transfers
res = self.wallet[0].get_transfers()
assert t_in == res['in']
# some information can not be recovered for out txes
unrecoverable_fields = ['payment_id', 'destinations', 'note']
old_t_out = []
for x in t_out:
e = {}
for k in x.keys():
if not k in unrecoverable_fields:
e[k] = x[k]
old_t_out.append(e)
new_t_out = []
for x in res.out:
e = {}
for k in x.keys():
if not k in unrecoverable_fields:
e[k] = x[k]
new_t_out.append(e)
assert sorted(old_t_out, key = lambda k: k['txid']) == sorted(new_t_out, key = lambda k: k['txid'])
def check_is_key_image_spent(self):
daemon = Daemon()
print('Testing is_key_image_spent')
res = self.wallet[0].incoming_transfers(transfer_type = 'all')
transfers = res.transfers
ki = [x.key_image for x in transfers]
expected = [1 if x.spent else 0 for x in transfers]
res = daemon.is_key_image_spent(ki)
assert res.spent_status == expected
res = self.wallet[0].incoming_transfers(transfer_type = 'available')
transfers = res.transfers
ki = [x.key_image for x in transfers]
expected = [0 for x in transfers]
res = daemon.is_key_image_spent(ki)
assert res.spent_status == expected
res = self.wallet[0].incoming_transfers(transfer_type = 'unavailable')
transfers = res.transfers
ki = [x.key_image for x in transfers]
expected = [1 for x in transfers]
res = daemon.is_key_image_spent(ki)
assert res.spent_status == expected
ki = [ki[-1]] * 5
expected = [1] * len(ki)
res = daemon.is_key_image_spent(ki)
assert res.spent_status == expected
ki = ['2'*64, '1'*64]
expected = [0, 0]
res = daemon.is_key_image_spent(ki)
assert res.spent_status == expected
def check_multiple_submissions(self):
daemon = Daemon()
print('Testing multiple submissions')
dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
balance = res.balance
res = self.wallet[0].transfer([dst], ring_size = 16, get_tx_key = False, get_tx_hex = False, get_tx_metadata = True)
tx_hex = res.tx_metadata
tx_fee = res.fee
res = self.wallet[0].relay_tx(tx_hex)
# submit again before mined
res = self.wallet[0].relay_tx(tx_hex)
daemon.generateblocks('44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 1)
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
assert res.balance == balance - tx_fee
balance = res.balance
# submit again after mined
res = self.wallet[0].relay_tx(tx_hex)
daemon.generateblocks('44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 1)
self.wallet[0].refresh()
res = self.wallet[0].get_balance()
assert res.balance == balance
def check_scan_tx(self):
daemon = Daemon()
print('Testing scan_tx')
# set up sender_wallet
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)
sender_wallet.refresh()
res = sender_wallet.get_transfers()
out_len = 0 if 'out' not in res else len(res.out)
sender_starting_balance = sender_wallet.get_balance().balance
amount = 1000000000000
assert sender_starting_balance > amount
# 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)
receiver_wallet.refresh()
res = receiver_wallet.get_transfers()
in_len = 0 if 'in' not in res else len(res['in'])
receiver_starting_balance = receiver_wallet.get_balance().balance
# transfer from sender_wallet to receiver_wallet
dst = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': amount}
res = sender_wallet.transfer([dst])
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert res.amount == amount
assert res.fee > 0
fee = res.fee
expected_sender_balance = sender_starting_balance - (amount + fee)
expected_receiver_balance = receiver_starting_balance + amount
test = 'Checking scan_tx on outgoing pool tx'
for attempt in range(2): # test re-scanning
print(test + ' (' + ('first attempt' if attempt == 0 else 're-scanning tx') + ')')
sender_wallet.scan_tx([txid])
res = sender_wallet.get_transfers()
assert 'pool' not in res or len(res.pool) == 0
if out_len == 0:
assert 'out' not in res
else:
assert len(res.out) == out_len
assert len(res.pending) == 1
tx = [x for x in res.pending if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount
assert tx.fee == fee
assert len(tx.destinations) == 1
assert tx.destinations[0].amount == amount
assert tx.destinations[0].address == dst['address']
assert sender_wallet.get_balance().balance == expected_sender_balance
test = 'Checking scan_tx on incoming pool tx'
for attempt in range(2): # test re-scanning
print(test + ' (' + ('first attempt' if attempt == 0 else 're-scanning tx') + ')')
receiver_wallet.scan_tx([txid])
res = receiver_wallet.get_transfers()
assert 'pending' not in res or len(res.pending) == 0
if in_len == 0:
assert 'in' not in res
else:
assert len(res['in']) == in_len
assert 'pool' in res and len(res.pool) == 1
tx = [x for x in res.pool if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount
assert tx.fee == fee
assert receiver_wallet.get_balance().balance == expected_receiver_balance
# mine the tx
height = daemon.generateblocks(dst['address'], 1).height
block_header = daemon.getblockheaderbyheight(height = height).block_header
miner_txid = block_header.miner_tx_hash
expected_receiver_balance += block_header.reward
print('Checking scan_tx on outgoing tx before refresh')
sender_wallet.scan_tx([txid])
res = sender_wallet.get_transfers()
assert 'pending' not in res or len(res.pending) == 0
assert 'pool' not in res or len (res.pool) == 0
assert len(res.out) == out_len + 1
tx = [x for x in res.out if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount
assert tx.fee == fee
assert len(tx.destinations) == 1
assert tx.destinations[0].amount == amount
assert tx.destinations[0].address == dst['address']
assert sender_wallet.get_balance().balance == expected_sender_balance
print('Checking scan_tx on outgoing tx after refresh')
sender_wallet.refresh()
sender_wallet.scan_tx([txid])
diff_transfers(sender_wallet.get_transfers(), res)
assert sender_wallet.get_balance().balance == expected_sender_balance
print("Checking scan_tx on outgoing wallet's earliest tx")
earliest_height = height
earliest_txid = txid
for x in res['in']:
if x.height < earliest_height:
earliest_height = x.height
earliest_txid = x.txid
sender_wallet.scan_tx([earliest_txid])
diff_transfers(sender_wallet.get_transfers(), res)
assert sender_wallet.get_balance().balance == expected_sender_balance
test = 'Checking scan_tx on outgoing wallet restored at current height'
for i, out_tx in enumerate(res.out):
if 'destinations' in out_tx:
del res.out[i]['destinations'] # destinations are not expected after wallet restore
out_txids = [x.txid for x in res.out]
in_txids = [x.txid for x in res['in']]
all_txs = out_txids + in_txids
for test_type in ["all txs", "incoming first", "duplicates within", "duplicates across"]:
print(test + ' (' + test_type + ')')
sender_wallet.close_wallet()
sender_wallet.restore_deterministic_wallet(seed = seeds[0], restore_height = height)
assert sender_wallet.get_transfers() == {}
if test_type == "all txs":
sender_wallet.scan_tx(all_txs)
elif test_type == "incoming first":
sender_wallet.scan_tx(in_txids)
sender_wallet.scan_tx(out_txids)
# TODO: test_type == "outgoing first"
elif test_type == "duplicates within":
sender_wallet.scan_tx(all_txs + all_txs)
elif test_type == "duplicates across":
sender_wallet.scan_tx(all_txs)
sender_wallet.scan_tx(all_txs)
else:
assert True == False
diff_transfers(sender_wallet.get_transfers(), res)
assert sender_wallet.get_balance().balance == expected_sender_balance
print('Sanity check against outgoing wallet restored at height 0')
sender_wallet.close_wallet()
sender_wallet.restore_deterministic_wallet(seed = seeds[0], restore_height = 0)
sender_wallet.refresh()
diff_transfers(sender_wallet.get_transfers(), res)
assert sender_wallet.get_balance().balance == expected_sender_balance
print('Checking scan_tx on incoming txs before refresh')
receiver_wallet.scan_tx([txid, miner_txid])
res = receiver_wallet.get_transfers()
assert 'pending' not in res or len(res.pending) == 0
assert 'pool' not in res or len (res.pool) == 0
assert len(res['in']) == in_len + 2
tx = [x for x in res['in'] if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount
assert tx.fee == fee
assert receiver_wallet.get_balance().balance == expected_receiver_balance
print('Checking scan_tx on incoming txs after refresh')
receiver_wallet.refresh()
receiver_wallet.scan_tx([txid, miner_txid])
diff_transfers(receiver_wallet.get_transfers(), res)
assert receiver_wallet.get_balance().balance == expected_receiver_balance
print("Checking scan_tx on incoming wallet's earliest tx")
earliest_height = height
earliest_txid = txid
for x in res['in']:
if x.height < earliest_height:
earliest_height = x.height
earliest_txid = x.txid
receiver_wallet.scan_tx([earliest_txid])
diff_transfers(receiver_wallet.get_transfers(), res)
assert receiver_wallet.get_balance().balance == expected_receiver_balance
print('Checking scan_tx on incoming wallet restored at current height')
txids = [x.txid for x in res['in']]
if 'out' in res:
txids = txids + [x.txid for x in res.out]
receiver_wallet.close_wallet()
receiver_wallet.restore_deterministic_wallet(seed = seeds[1], restore_height = height)
assert receiver_wallet.get_transfers() == {}
receiver_wallet.scan_tx(txids)
if 'out' in res:
for i, out_tx in enumerate(res.out):
if 'destinations' in out_tx:
del res.out[i]['destinations'] # destinations are not expected after wallet restore
diff_transfers(receiver_wallet.get_transfers(), res)
assert receiver_wallet.get_balance().balance == expected_receiver_balance
print('Sanity check against incoming wallet restored at height 0')
receiver_wallet.close_wallet()
receiver_wallet.restore_deterministic_wallet(seed = seeds[1], restore_height = 0)
receiver_wallet.refresh()
diff_transfers(receiver_wallet.get_transfers(), res)
assert receiver_wallet.get_balance().balance == expected_receiver_balance
def check_subtract_fee_from_outputs(self):
daemon = Daemon()
print('Testing fee-included transfers')
def inner_test_external_transfer(dsts, subtract_fee_from_outputs):
# refresh wallet and get balance
self.wallet[0].refresh()
balance1 = self.wallet[0].get_balance().balance
# Check that this transaction is possible with our current balance + other preconditions
dst_sum = sum(map(lambda x: x['amount'], dsts))
assert balance1 >= dst_sum
if subtract_fee_from_outputs:
assert max(subtract_fee_from_outputs) < len(dsts)
# transfer with subtractfeefrom=all
transfer_res = self.wallet[0].transfer(dsts, subtract_fee_from_outputs = subtract_fee_from_outputs, get_tx_metadata = True)
tx_hex = transfer_res.tx_metadata
tx_fee = transfer_res.fee
amount_spent = transfer_res.amount
amounts_by_dest = transfer_res.amounts_by_dest.amounts
# Assert that fee and amount spent to outputs adds up
assert tx_fee != 0
if subtract_fee_from_outputs:
assert tx_fee + amount_spent == dst_sum
else:
assert amount_spent == dst_sum
# Check the amounts by each destination that only the destinations set as subtractable
# got subtracted and that the subtracted dests are approximately correct
assert len(amounts_by_dest) == len(dsts) # if this fails... idk
for i in range(len(dsts)):
if i in subtract_fee_from_outputs: # dest is subtractable
approx_subtraction = tx_fee // len(subtract_fee_from_outputs)
assert amounts_by_dest[i] < dsts[i]['amount']
assert dsts[i]['amount'] - amounts_by_dest[i] - approx_subtraction <= 1
else:
assert amounts_by_dest[i] == dsts[i]['amount']
# relay tx and generate block (not to us, to simplify balance change calculations)
relay_res = self.wallet[0].relay_tx(tx_hex)
daemon.generateblocks('44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 1)
# refresh and get balance again
self.wallet[0].refresh()
balance2 = self.wallet[0].get_balance().balance
# Check that the wallet balance dropped by the correct amount
balance_drop = balance1 - balance2
if subtract_fee_from_outputs:
assert balance_drop == dst_sum
else:
assert balance_drop == dst_sum + tx_fee
dst1 = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': 1100000000001}
dst2 = {'address': '46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 'amount': 1200000000000}
dst3 = {'address': '46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 'amount': 1}
inner_test_external_transfer([dst1, dst2], [0, 1])
inner_test_external_transfer([dst1, dst2], [0])
inner_test_external_transfer([dst1, dst2], [1])
inner_test_external_transfer([dst1, dst2], [])
inner_test_external_transfer([dst1], [0])
inner_test_external_transfer([dst1], [])
inner_test_external_transfer([dst3], [])
try:
inner_test_external_transfer([dst1, dst3], [0, 1]) # Test subtractfeefrom if one of the outputs would underflow w/o good checks
raise ValueError('transfer request with tiny subtractable destination should have thrown')
except:
pass
# Check for JSONRPC error on bad index
try:
transfer_res = self.wallet[0].transfer([dst1], subtract_fee_from_outputs = [1])
raise ValueError('transfer request with index should have thrown')
except AssertionError:
pass
def check_background_sync(self):
daemon = Daemon()
print('Testing background sync')
# Some helper functions
def stop_with_wrong_inputs(wallet, wallet_password, seed = ''):
invalid = False
try: wallet.stop_background_sync(wallet_password = wallet_password, seed = seed)
except: invalid = True
assert invalid
def open_with_wrong_password(wallet, filename, password):
invalid_password = False
try: wallet.open_wallet(filename, password = password)
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)
assert wallet.get_balance().balance == expected_balance
# Set up sender_wallet. Prepare to sweep single output to receiver.
# 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)
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]
assert len(unlocked) > 0
ki = unlocked[0].key_image
amount = unlocked[0].amount
spent_txid = unlocked[0].tx_hash
sender_wallet.refresh()
res = sender_wallet.get_transfers()
out_len = 0 if 'out' not in res else len(res.out)
sender_starting_balance = sender_wallet.get_balance().balance
# Background sync type options
reuse_password = sender_wallet.background_sync_options.reuse_password
custom_password = sender_wallet.background_sync_options.custom_password
# 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)
receiver_wallet.refresh()
res = receiver_wallet.get_transfers()
in_len = 0 if 'in' not in res else len(res['in'])
receiver_starting_balance = receiver_wallet.get_balance().balance
# transfer from sender_wallet to receiver_wallet
dst = '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW'
res = sender_wallet.sweep_single(dst, key_image = ki)
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
assert res.fee > 0
fee = res.fee
assert res.amount == amount - fee
expected_sender_balance = sender_starting_balance - amount
expected_receiver_balance = receiver_starting_balance + (amount - fee)
print('Checking background sync on outgoing wallet')
sender_wallet.setup_background_sync(background_sync_type = reuse_password)
sender_wallet.start_background_sync()
# Mine block to an uninvolved wallet
daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
# sender should still be able to scan the transfer normally because we
# spent an output that had a known key image
sender_wallet.refresh()
transfers = sender_wallet.get_transfers()
assert 'pending' not in transfers or len(transfers.pending) == 0
assert 'pool' not in transfers or len (transfers.pool) == 0
assert len(transfers.out) == out_len + 1
tx = [x for x in transfers.out if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount - fee
assert tx.fee == fee
assert len(tx.destinations) == 1
assert tx.destinations[0].amount == amount - fee
assert tx.destinations[0].address == dst
incoming_transfers = sender_wallet.incoming_transfers(transfer_type = 'all')
assert len([x for x in incoming_transfers.transfers if x.tx_hash == spent_txid and x.key_image == ki and x.spent]) == 1
assert sender_wallet.get_balance().balance == expected_sender_balance
# Restore and check background syncing outgoing wallet
restore_wallet(sender_wallet, seeds[0])
sender_wallet.setup_background_sync(background_sync_type = reuse_password)
sender_wallet.start_background_sync()
sender_wallet.refresh()
for i, out_tx in enumerate(transfers.out):
if 'destinations' in out_tx:
del transfers.out[i]['destinations'] # destinations are not expected after wallet restore
# sender's balance should be higher because can't detect spends while
# background sync enabled, only receives
background_bal = sender_wallet.get_balance().balance
assert background_bal > expected_sender_balance
background_transfers = sender_wallet.get_transfers()
assert 'out' not in background_transfers or len(background_transfers.out) == 0
assert 'in' in background_transfers and len(background_transfers['in']) > 0
background_incoming_transfers = sender_wallet.incoming_transfers(transfer_type = 'all')
assert len(background_incoming_transfers) == len(incoming_transfers)
assert len([x for x in background_incoming_transfers.transfers if x.spent or x.key_image != '']) == 0
assert len([x for x in background_incoming_transfers.transfers if x.tx_hash == spent_txid]) == 1
# Try to stop background sync with the wrong seed
stop_with_wrong_inputs(sender_wallet, wallet_password = '', seed = seeds[1])
# Stop background sync and check transfers update correctly
sender_wallet.stop_background_sync(wallet_password = '', seed = seeds[0])
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# 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')
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()
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
stop_with_wrong_inputs(sender_wallet, 'wrong_password')
sender_wallet.stop_background_sync(wallet_password = 'test_password')
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# 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')
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()
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
sender_wallet.close_wallet()
open_with_wrong_password(sender_wallet, 'test1', 'wrong_password')
sender_wallet.open_wallet('test1', password = 'test_password')
# It should reopen with spend key loaded and correctly scan all transfers
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# 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')
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()
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
sender_wallet.close_wallet()
open_with_wrong_password(sender_wallet, 'test1', 'wrong_password')
sender_wallet.open_wallet('test1', password = 'test_password')
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
# 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')
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)
assert util_resources.file_exists('test1.background')
assert util_resources.file_exists('test1.background.keys')
sender_wallet.close_wallet()
open_with_wrong_password(sender_wallet, 'test1.background', 'test_password')
sender_wallet.open_wallet('test1.background', password = background_cache_password)
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
sender_wallet.close_wallet()
sender_wallet.open_wallet('test1', password = 'test_password')
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')
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)
sender_wallet.close_wallet()
sender_wallet.open_wallet('test1.background', password = 'background_password')
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')
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
same_password = False
try: sender_wallet.setup_background_sync(background_sync_type = custom_password, wallet_password = 'test_password', background_cache_password = 'test_password')
except: same_password = True
assert same_password
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
# Turn off background sync
for background_sync_type in [reuse_password, custom_password]:
restore_wallet(sender_wallet, seeds[0], 'test1', '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:
assert util_resources.file_exists('test1.background')
assert util_resources.file_exists('test1.background.keys')
sender_wallet.close_wallet()
assert util_resources.file_exists('test1.background')
assert util_resources.file_exists('test1.background.keys')
else:
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
sender_wallet.close_wallet()
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
sender_wallet.open_wallet('test1', password = 'test_password')
sender_wallet.setup_background_sync(background_sync_type = sender_wallet.background_sync_options.off, wallet_password = 'test_password')
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
sender_wallet.close_wallet()
assert not util_resources.file_exists('test1.background')
assert not util_resources.file_exists('test1.background.keys')
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)
sender_wallet.refresh()
assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
print('Checking background sync on incoming wallet')
receiver_wallet.setup_background_sync(background_sync_type = reuse_password)
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
transfers = receiver_wallet.get_transfers()
assert 'pending' not in transfers or len(transfers.pending) == 0
assert 'pool' not in transfers or len (transfers.pool) == 0
assert len(transfers['in']) == in_len + 1
tx = [x for x in transfers['in'] if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert tx.amount == amount - fee
assert tx.fee == fee
incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
assert len([x for x in incoming_transfers.transfers if x.tx_hash == txid and x.key_image == '' and not x.spent]) == 1
assert receiver_wallet.get_balance().balance == expected_receiver_balance
# Restore and check background syncing incoming wallet
restore_wallet(receiver_wallet, seeds[1])
receiver_wallet.setup_background_sync(background_sync_type = reuse_password)
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
if 'out' in transfers:
for i, out_tx in enumerate(transfers.out):
if 'destinations' in out_tx:
del transfers.out[i]['destinations'] # destinations are not expected after wallet restore
background_bal = receiver_wallet.get_balance().balance
assert background_bal >= expected_receiver_balance
background_transfers = receiver_wallet.get_transfers()
assert 'out' not in background_transfers or len(background_transfers.out) == 0
assert 'in' in background_transfers and len(background_transfers['in']) > 0
background_incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
assert len(background_incoming_transfers) == len(incoming_transfers)
assert len([x for x in background_incoming_transfers.transfers if x.spent or x.key_image != '']) == 0
assert len([x for x in background_incoming_transfers.transfers if x.tx_hash == txid]) == 1
# Stop background sync and check transfers update correctly
receiver_wallet.stop_background_sync(wallet_password = '', seed = seeds[1])
diff_transfers(receiver_wallet.get_transfers(), transfers)
incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
assert len(background_incoming_transfers) == len(incoming_transfers)
assert len([x for x in incoming_transfers.transfers if x.tx_hash == txid and x.key_image != '' and not x.spent]) == 1
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')
receiver_wallet.setup_background_sync(background_sync_type = reuse_password, wallet_password = 'test_password')
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
assert_correct_transfers(receiver_wallet, background_transfers, background_incoming_transfers, background_bal)
stop_with_wrong_inputs(receiver_wallet, 'wrong_password')
receiver_wallet.stop_background_sync(wallet_password = 'test_password')
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')
receiver_wallet.setup_background_sync(background_sync_type = reuse_password, wallet_password = 'test_password')
receiver_wallet.start_background_sync()
receiver_wallet.refresh()
diff_transfers(receiver_wallet.get_transfers(), background_transfers)
diff_incoming_transfers(receiver_wallet.incoming_transfers(transfer_type = 'all'), background_incoming_transfers)
assert receiver_wallet.get_balance().balance == background_bal
receiver_wallet.close_wallet()
receiver_wallet.open_wallet('test2', password = 'test_password')
# It should reopen with spend key loaded and correctly scan all transfers
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)
receiver_wallet.refresh()
assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
# Clean up
util_resources.remove_wallet_files('test1')
util_resources.remove_wallet_files('test2')
for i in range(2):
self.wallet[i].close_wallet()
self.wallet[i].restore_deterministic_wallet(seed = seeds[i])
def check_background_sync_reorg_recovery(self):
daemon = Daemon()
print('Testing background sync reorg recovery')
# Disconnect daemon from peers
daemon.out_peers(0)
# Background sync type options
sender_wallet = self.wallet[0]
reuse_password = sender_wallet.background_sync_options.reuse_password
custom_password = sender_wallet.background_sync_options.custom_password
for background_sync_type in [reuse_password, custom_password]:
# Set up wallet saved to disk
sender_wallet.close_wallet()
util_resources.remove_wallet_files('test1')
sender_wallet.restore_deterministic_wallet(seed = seeds[0], filename = 'test1', password = '')
sender_wallet.auto_refresh(enable = False)
sender_wallet.refresh()
sender_starting_balance = sender_wallet.get_balance().balance
# Send tx and mine a block
amount = 1000000000000
assert sender_starting_balance > amount
dst = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': amount}
res = sender_wallet.transfer([dst])
assert len(res.tx_hash) == 32*2
txid = res.tx_hash
daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
# Make sure the wallet can see the tx
sender_wallet.refresh()
transfers = sender_wallet.get_transfers()
assert 'pool' not in transfers or len (transfers.pool) == 0
tx = [x for x in transfers.out if x.txid == txid]
assert len(tx) == 1
tx = tx[0]
assert sender_wallet.get_balance().balance < (sender_starting_balance - amount)
# Pop the block while background syncing
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 = '', background_cache_password = background_cache_password)
sender_wallet.start_background_sync()
daemon.pop_blocks(1)
daemon.flush_txpool()
daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
# Make sure the wallet can no longer see the tx
sender_wallet.refresh()
sender_wallet.stop_background_sync(wallet_password = '', seed = seeds[0])
transfers = sender_wallet.get_transfers()
no_tx = [x for x in transfers.out if x.txid == txid]
assert len(no_tx) == 0
assert sender_wallet.get_balance().balance == sender_starting_balance
# Clean up
daemon.out_peers(12)
util_resources.remove_wallet_files('test1')
self.wallet[0].close_wallet()
self.wallet[0].restore_deterministic_wallet(seed = seeds[0])
if __name__ == '__main__':
TransferTest().run_test()