You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
8.1 KiB
207 lines
8.1 KiB
|
|
import unittest |
|
|
|
import pytest |
|
from jmbitcoin import CMutableTxOut, CMutableTransaction |
|
from jmclient import load_test_config, jm_single,\ |
|
SegwitLegacyWallet, VolatileStorage, YieldGeneratorBasic, \ |
|
get_network, WalletService |
|
|
|
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") |
|
|
|
|
|
class CustomUtxoWallet(SegwitLegacyWallet): |
|
"""A wallet instance that makes it easy to do the tasks we need for |
|
the tests. It has convenience methods to add UTXOs with pre-defined |
|
balances for all mixdepths, and to verify if a given UTXO or address |
|
corresponds to the mixdepth it should.""" |
|
|
|
def __init__(self, balances): |
|
"""Creates the wallet, setting the balances of the mixdepths |
|
as given by the array. (And the number of mixdepths from the array |
|
elements.""" |
|
|
|
load_test_config() |
|
|
|
storage = VolatileStorage() |
|
super().initialize(storage, get_network(), max_mixdepth=len(balances)-1) |
|
super().__init__(storage) |
|
|
|
for m, b in enumerate(balances): |
|
self.add_utxo_at_mixdepth(m, b) |
|
|
|
def add_utxo_at_mixdepth(self, mixdepth, balance): |
|
txout = CMutableTxOut(balance, self.get_internal_script(mixdepth)) |
|
tx = CMutableTransaction() |
|
tx.vout = [txout] |
|
# (note: earlier requirement that txid be generated uniquely is now |
|
# automatic; tx.GetTxid() functions correctly within the wallet). |
|
self.add_new_utxos(tx, 1) |
|
|
|
def assert_utxos_from_mixdepth(self, utxos, expected): |
|
"""Asserts that the list of UTXOs (as returned from UTXO selection |
|
of the wallet) is all from the given expected mixdepth.""" |
|
|
|
for u in utxos.values(): |
|
assert self.get_addr_mixdepth(u['address']) == expected |
|
|
|
|
|
def create_yg_basic(balances, txfee_contribution=0, cjfee_a=0, cjfee_r=0, |
|
ordertype='swabsoffer', minsize=0): |
|
"""Constructs a YieldGeneratorBasic instance with a fake wallet. The |
|
wallet will have the given balances at mixdepths, and the offer params |
|
will be set as given here.""" |
|
|
|
wallet = CustomUtxoWallet(balances) |
|
offerconfig = (txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, |
|
None, None, None) |
|
|
|
yg = YieldGeneratorBasic(WalletService(wallet), offerconfig) |
|
|
|
# We don't need any of the logic from Maker, including the waiting |
|
# loop. Just stop it, so that it does not linger around and create |
|
# unclean-reactor failures. |
|
if yg.sync_wait_loop.running: |
|
yg.sync_wait_loop.stop() |
|
|
|
return yg |
|
|
|
|
|
class CreateMyOrdersTests(unittest.TestCase): |
|
"""Unit tests for YieldGeneratorBasic.create_my_orders.""" |
|
|
|
def test_no_coins(self): |
|
yg = create_yg_basic([0] * 3) |
|
self.assertEqual(yg.create_my_orders(), []) |
|
|
|
def test_abs_fee(self): |
|
jm_single().DUST_THRESHOLD = 10 |
|
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=1000, |
|
cjfee_a=10, ordertype='swabsoffer', minsize=100000) |
|
self.assertEqual(yg.create_my_orders(), [ |
|
{'oid': 0, |
|
'ordertype': 'swabsoffer', |
|
'minsize': 100000, |
|
'maxsize': 1999000, |
|
'txfee': 1000, |
|
'cjfee': '1010'}, |
|
]) |
|
|
|
def test_rel_fee(self): |
|
jm_single().DUST_THRESHOLD = 10 |
|
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=1000, |
|
cjfee_r=0.1, ordertype='sw0reloffer', minsize=10) |
|
self.assertEqual(yg.create_my_orders(), [ |
|
{'oid': 0, |
|
'ordertype': 'sw0reloffer', |
|
'minsize': 15000, |
|
'maxsize': 1999000, |
|
'txfee': 1000, |
|
'cjfee': 0.1}, |
|
]) |
|
|
|
def test_dust_threshold(self): |
|
jm_single().DUST_THRESHOLD = 1000 |
|
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=10, |
|
cjfee_a=10, ordertype='swabsoffer', minsize=100000) |
|
self.assertEqual(yg.create_my_orders(), [ |
|
{'oid': 0, |
|
'ordertype': 'swabsoffer', |
|
'minsize': 100000, |
|
'maxsize': 1999000, |
|
'txfee': 10, |
|
'cjfee': '20'}, |
|
]) |
|
|
|
def test_minsize_above_maxsize(self): |
|
jm_single().DUST_THRESHOLD = 10 |
|
yg = create_yg_basic([0, 20000, 10000], txfee_contribution=1000, |
|
cjfee_a=10, ordertype='swabsoffer', minsize=100000) |
|
self.assertEqual(yg.create_my_orders(), []) |
|
|
|
|
|
class OidToOrderTests(unittest.TestCase): |
|
"""Tests YieldGeneratorBasic.oid_to_order.""" |
|
|
|
def call_oid_to_order(self, yg, amount): |
|
"""Calls oid_to_order on the given yg instance. It passes the |
|
txfee and abs fee from yg as offer.""" |
|
offer = {'txfee': yg.txfee_contribution, |
|
'cjfee': str(yg.cjfee_a), |
|
'ordertype': 'swabsoffer'} |
|
return yg.oid_to_order(offer, amount) |
|
|
|
def test_not_enough_balance(self): |
|
yg = create_yg_basic([100], txfee_contribution=0, cjfee_a=10) |
|
self.assertEqual(self.call_oid_to_order(yg, 1000), (None, None, None)) |
|
|
|
def test_chooses_single_utxo(self): |
|
jm_single().DUST_THRESHOLD = 10 |
|
yg = create_yg_basic([10, 1000, 2000]) |
|
utxos, cj_addr, change_addr = self.call_oid_to_order(yg, 500) |
|
self.assertEqual(len(utxos), 1) |
|
yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1) |
|
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2) |
|
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1) |
|
|
|
def test_not_enough_balance_with_dust_threshold(self): |
|
# 410 is exactly the size of the change output. So it will be |
|
# right at the dust threshold. The wallet won't be able to find |
|
# any extra inputs, though. |
|
jm_single().DUST_THRESHOLD = 410 |
|
yg = create_yg_basic([10, 1000, 10], txfee_contribution=100, |
|
cjfee_a=10) |
|
self.assertEqual(self.call_oid_to_order(yg, 500), (None, None, None)) |
|
|
|
def test_extra_with_dust_threshold(self): |
|
# The output will be right at the dust threshold, so that we will |
|
# need to include the extra_utxo from the wallet as well to get |
|
# over the threshold. |
|
jm_single().DUST_THRESHOLD = 410 |
|
yg = create_yg_basic([10, 1000, 10], txfee_contribution=100, |
|
cjfee_a=10) |
|
yg.wallet_service.wallet.add_utxo_at_mixdepth(1, 500) |
|
utxos, cj_addr, change_addr = self.call_oid_to_order(yg, 500) |
|
self.assertEqual(len(utxos), 2) |
|
yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1) |
|
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2) |
|
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1) |
|
|
|
|
|
class OfferReannouncementTests(unittest.TestCase): |
|
"""Tests offer reannouncement logic from on_tx_unconfirmed.""" |
|
|
|
def call_on_tx_unconfirmed(self, yg): |
|
"""Calls yg.on_tx_unconfirmed with fake arguments.""" |
|
return yg.on_tx_unconfirmed({'cjaddr': 'addr'}, 'txid') |
|
|
|
def create_yg_and_offer(self, maxsize): |
|
"""Constructs a fake yg instance that has an offer with the given |
|
maxsize. Returns it together with the offer.""" |
|
jm_single().DUST_THRESHOLD = 10 |
|
yg = create_yg_basic([100 + maxsize], txfee_contribution=100, |
|
ordertype='swabsoffer') |
|
offers = yg.create_my_orders() |
|
self.assertEqual(len(offers), 1) |
|
self.assertEqual(offers[0]['maxsize'], maxsize) |
|
return yg, offers[0] |
|
|
|
def test_no_new_offers(self): |
|
yg = create_yg_basic([0] * 3) |
|
yg.offerlist = [{'oid': 0}] |
|
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([0], [])) |
|
|
|
def test_no_old_offers(self): |
|
yg, offer = self.create_yg_and_offer(100) |
|
yg.offerlist = [] |
|
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer])) |
|
|
|
def test_offer_unchanged(self): |
|
yg, offer = self.create_yg_and_offer(100) |
|
yg.offerlist = [offer] |
|
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [])) |
|
|
|
def test_offer_changed(self): |
|
yg, offer = self.create_yg_and_offer(100) |
|
yg.offerlist = [{'oid': 0, 'maxsize': 10}] |
|
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer]))
|
|
|