Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion tests/test_monero_wallet_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
WalletType, IntegrationTestUtils,
ViewOnlyAndOfflineWalletTester,
WalletNotificationCollector,
MiningUtils
MiningUtils, SendAndUpdateTxsTester
)

logger: logging.Logger = logging.getLogger("TestMoneroWalletCommon")
Expand Down Expand Up @@ -559,6 +559,58 @@ def test_sweep_dust(self, wallet: MoneroWallet) -> None:
for tx in txs:
TxUtils.test_tx_wallet(tx, ctx)

# TODO: test sending to multiple accounts
# Can update a locked tx sent from/to the same account as blocks are added to the chain
@pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled")
@pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
def test_update_locked_same_account(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
config: MoneroTxConfig = MoneroTxConfig()
config.address = wallet.get_primary_address()
config.amount = TxUtils.MAX_FEE
config.account_index = 0
config.can_split = False
config.relay = True
self._test_send_and_update_txs(daemon, wallet, config)

# Can update split locked txs sent from/to the same account as blocks are added to the chain
@pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled")
@pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
@pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled")
def test_update_locked_same_account_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
config: MoneroTxConfig = MoneroTxConfig()
config.address = wallet.get_primary_address()
config.amount = TxUtils.MAX_FEE
config.account_index = 0
config.can_split = True
config.relay = True
self._test_send_and_update_txs(daemon, wallet, config)

# Can update a locked tx sent from/to different accounts as blocks are added to the chain
@pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled")
@pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
@pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled")
def test_update_locked_different_accounts(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
config: MoneroTxConfig = MoneroTxConfig()
config.address = wallet.get_subaddress(1, 0).address
config.amount = TxUtils.MAX_FEE
config.account_index = 0
config.can_split = False
config.relay = True
self._test_send_and_update_txs(daemon, wallet, config)

# Can update locked, split txs sent from/to different accounts as blocks are added to the chain
@pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled")
@pytest.mark.skipif(TestUtils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
@pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled")
def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
config: MoneroTxConfig = MoneroTxConfig()
config.address = wallet.get_subaddress(1, 0).address
config.amount = TxUtils.MAX_FEE
config.account_index = 0
config.can_split = True
config.relay = True
self._test_send_and_update_txs(daemon, wallet, config)

#endregion

#region Non Relays Tests
Expand Down Expand Up @@ -3755,4 +3807,13 @@ def test_rescan_blockchain(self, wallet: MoneroWallet) -> None:

#endregion

#region Utils

@classmethod
def _test_send_and_update_txs(cls, daemon: MoneroDaemonRpc, wallet: MoneroWallet, config: MoneroTxConfig) -> None:
tester: SendAndUpdateTxsTester = SendAndUpdateTxsTester(daemon, wallet, config)
tester.test()

#endregion

#endregion
20 changes: 16 additions & 4 deletions tests/test_monero_wallet_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ def get_test_wallet(cls) -> MoneroWalletFull:

#endregion

#region Test Relays

@pytest.mark.skipif(Utils.TEST_RELAYS is False, reason="TEST_RELAYS disabled")
@pytest.mark.skipif(Utils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
@pytest.mark.skipif(Utils.LITE_MODE, reason="LITE_MODE enabled")
@pytest.mark.xfail(raises=RuntimeError, reason="TODO Cannot reconcile integrals: 0 vs 1. tx wallet m_is_incoming")
@override
def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
return super().test_update_locked_different_accounts_split(daemon, wallet)

#endregion

#region Test Non Relays

# Can create a random full wallet
Expand Down Expand Up @@ -660,10 +672,6 @@ def test_multisig_sample(self) -> None:
self._test_multisig_sample(2, 3)
self._test_multisig_sample(2, 4)

#endregion

#region Disabled Tests

@pytest.mark.skipif(Utils.REGTEST, reason="Cannot retrieve accurate height by date from regtest fakechain")
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
@override
Expand All @@ -676,6 +684,10 @@ def test_get_height_by_date(self, wallet: MoneroWallet):
def test_get_height_by_date_regtest(self, wallet: MoneroWallet):
return super().test_get_height_by_date(wallet)

#endregion

#region Disabled Tests

@pytest.mark.skip(reason="TODO disabled because importing key images deletes corresponding incoming transfers: https://github.com/monero-project/monero/issues/5812")
@override
def test_import_key_images(self, wallet: MoneroWallet):
Expand Down
20 changes: 20 additions & 0 deletions tests/test_monero_wallet_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,26 @@ def test_is_multisig_needed(self, wallet: MoneroWallet) -> None:
def test_rescan_spent(self, wallet: MoneroWallet) -> None:
return super().test_rescan_spent(wallet)

@pytest.mark.not_supported
@override
def test_update_locked_same_account(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
return super().test_update_locked_same_account(daemon, wallet)

@pytest.mark.not_supported
@override
def test_update_locked_same_account_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
return super().test_update_locked_same_account_split(daemon, wallet)

@pytest.mark.not_supported
@override
def test_update_locked_different_accounts(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
return super().test_update_locked_different_accounts(daemon, wallet)

@pytest.mark.not_supported
@override
def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None:
return super().test_update_locked_different_accounts_split(daemon, wallet)

#endregion

#region Tests
Expand Down
4 changes: 3 additions & 1 deletion tests/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .wallet_sync_tester import WalletSyncTester
from .sync_progress_tester import SyncProgressTester
from .sync_seed_tester import SyncSeedTester
from .send_and_update_txs_tester import SendAndUpdateTxsTester

__all__ = [
'WalletUtils',
Expand Down Expand Up @@ -65,5 +66,6 @@
'MultisigSampleCodeTester',
'WalletSyncTester',
'SyncProgressTester',
'SyncSeedTester'
'SyncSeedTester',
'SendAndUpdateTxsTester'
]
212 changes: 212 additions & 0 deletions tests/utils/send_and_update_txs_tester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import logging

from time import sleep
from monero import (
MoneroWallet, MoneroTxConfig, MoneroTxWallet,
MoneroDaemon, MoneroBlockHeader, MoneroTxQuery
)

from .test_utils import TestUtils
from .tx_utils import TxUtils
from .mining_utils import MiningUtils
from .tx_context import TxContext

logger: logging.Logger = logging.getLogger("SendAndUpdateTxsTester")


class SendAndUpdateTxsTester:
"""
Tests sending a tx with an unlock time then tracking and updating it as
blocks are added to the chain.

TODO: test wallet accounting throughout this; dedicated method? probably.

Allows sending to and from the same account which is an edge case where
incoming txs are occluded by their outgoing counterpart (issue #4500)
and also where it is impossible to discern which incoming output is
the tx amount and which is the change amount without wallet metadata.
"""

daemon: MoneroDaemon
"""Daemon test instance."""
wallet: MoneroWallet
"""The test wallet to send from."""
config: MoneroTxConfig
"""The send configuration to send and test."""
num_confirmations: int
"""Number of blockchain confirmations."""

def __init__(self, daemon: MoneroDaemon, wallet: MoneroWallet, config: MoneroTxConfig) -> None:
"""
Initialize a new send and update txs tester.

:param MoneroDaemon daemon: daemon test instance.
:param MoneroWallet wallet: wallet test instance.
:param MoneroWalletConfig config: txs send configuration to test.
"""
self.daemon = daemon
self.wallet = wallet
self.config = config
self.num_confirmations = 0

def setup(self) -> None:
"""Setup test wallet."""
# wait for txs to confirm and for sufficient unlocked balance
TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(self.wallet)
assert len(self.config.subaddress_indices) == 0
assert self.config.account_index is not None
fee: int = TxUtils.MAX_FEE * 2
TestUtils.WALLET_TX_TRACKER.wait_for_unlocked_balance(self.wallet, self.config.account_index, None, fee)

def test_unlock_tx(self, tx: MoneroTxWallet, is_send_response: bool) -> None:
"""
Test tx unlock.

:param MoneroTxWallet tx: transaction to test.
:param bool is_send_response: indicates if tx originated from send response.
"""
ctx: TxContext = TxContext()
ctx.wallet = self.wallet
ctx.config = self.config
ctx.is_send_response = is_send_response
try:
TxUtils.test_tx_wallet(tx, ctx)
except Exception as e:
logger.warning(e)
raise

def test_out_in_pair(self, tx_out: MoneroTxWallet, tx_in: MoneroTxWallet) -> None:
"""
Test transaction out / in pair.

:param MoneroTxWallet tx_out: outgoing transaction.
:param MoneroTxWallet tx_in: incoming transaction.
"""
assert tx_out.is_confirmed == tx_in.is_confirmed
assert tx_in.get_incoming_amount() == tx_out.get_outgoing_amount()

def test_out_in_pairs(self, txs: list[MoneroTxWallet], is_send_response: bool) -> None:
"""
Test transactions out / in pairs.

:param list[MoneroTxWallet] txs: transactions to test out / in pairs.
:param bool is_send_response: indicates if tx originated from send response.
"""
# for each out tx
for tx in txs:
self.test_unlock_tx(tx, is_send_response)
if tx.outgoing_transfer is not None:
continue

tx_out: MoneroTxWallet = tx

# find incoming counterpart
tx_in: MoneroTxWallet | None = None
for tx2 in txs:
if tx.is_incoming and tx.hash == tx2.hash:
tx_in = tx2
break

# test out / in pair
# TODO monero-wallet-rpc: incoming txs occluded by their outgoing counterpart #4500
if tx_in is None:
logger.warning(f"outgoing tx {tx.hash} missing incoming counterpart (issue #4500)")
else:
self.test_out_in_pair(tx_out, tx_in)

def wait_for_confirmations(self, sent_txs: list[MoneroTxWallet], num_confirmations_total: int) -> None:
"""
Wait for txs to confirm.

:param list[MoneroTxWallet] sent_txs: list of sent transactions.
:param int num_confirmations_total: number of confirmed txs required.
"""
# track resulting outgoing and incoming txs as blocks are added to the chain
updated_txs: list[MoneroTxWallet] | None = None
logger.info(f"{self.num_confirmations} < {num_confirmations_total} needed confirmations")
header: MoneroBlockHeader = self.daemon.wait_for_next_block_header()
logger.info(f"*** Block {header.height} added to chain ***")

# give wallet time to catch up, otherwise incoming tx may not appear
# TODO: this lets new block slip, okay?
sleep(TestUtils.SYNC_PERIOD_IN_MS / 1000)

# get incoming/outgoing txs with sent hashes
tx_hashes: list[str] = []
for sent_tx in sent_txs:
assert sent_tx.hash is not None
tx_hashes.append(sent_tx.hash)

query: MoneroTxQuery = MoneroTxQuery()
query.hashes = tx_hashes
fetched_txs: list[MoneroTxWallet] = TxUtils.get_and_test_txs(self.wallet, query, None, True, TestUtils.REGTEST)
assert len(fetched_txs) > 0

# test fetched txs
self.test_out_in_pairs(fetched_txs, False)

# merge fetched txs into updated txs and original sent txs
for fetched_tx in fetched_txs:
# merge with updated txs
if updated_txs is None:
updated_txs = fetched_txs
else:
for updated_tx in updated_txs:
if fetched_tx.hash != updated_tx.hash or fetched_tx.is_outgoing != updated_tx.is_outgoing:
continue
updated_tx.merge(fetched_tx.copy())
if updated_tx.block is None and fetched_tx.block is not None:
# copy block for testing
updated_tx.block = fetched_tx.block.copy()
updated_tx.block.txs = [updated_tx]

# merge with original sent txs
for sent_tx in sent_txs:
if fetched_tx.hash != sent_tx.hash or fetched_tx.is_outgoing != sent_tx.is_outgoing:
continue
# TODO: it's mergeable but tests don't account for extra info
# from send (e.g. hex) so not tested; could specify in test config
sent_tx.merge(fetched_tx.copy())

# test updated txs
assert updated_txs is not None
self.test_out_in_pairs(updated_txs, False)

# update confirmations in order to exit loop
fetched_tx = fetched_txs[0]
assert fetched_tx.num_confirmations is not None
self.num_confirmations = fetched_tx.num_confirmations

def test(self) -> None:
"""Run send and update txs test."""
self.setup()
# this test starts and stops mining, so it's wrapped in order to stop mining if anything fails
try:
# send transactions
sent_txs: list[MoneroTxWallet] = self.wallet.create_txs(self.config)

# build test context
ctx: TxContext = TxContext()
ctx.wallet = self.wallet
ctx.config = self.config
ctx.is_send_response = True

# test sent transactions
for tx in sent_txs:
TxUtils.test_tx_wallet(tx, ctx)
assert tx.is_confirmed is False
assert tx.in_tx_pool is True

# attemp to start mining to push the network along
MiningUtils.try_start_mining()

# number of confirmations to test
num_confirmations_total: int = 2
# loop to update txs through confirmations

while self.num_confirmations < num_confirmations_total:
self.wait_for_confirmations(sent_txs, num_confirmations_total)

finally:
# stop mining at end of test
MiningUtils.try_stop_mining()
Loading
Loading