236 lines
11 KiB
Python
236 lines
11 KiB
Python
import unittest
|
|
import logging
|
|
from unittest import mock
|
|
import asyncio
|
|
import dataclasses
|
|
|
|
from aiorpcx import timeout_after
|
|
|
|
from electrum import storage, bitcoin, keystore, wallet
|
|
from electrum import SimpleConfig
|
|
from electrum import util
|
|
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_LOCAL
|
|
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput, TxOutpoint
|
|
from electrum.logging import console_stderr_handler, Logger
|
|
from electrum.submarine_swaps import SwapManager, SwapData
|
|
from electrum.lnsweep import SweepInfo
|
|
from electrum.fee_policy import FeeTimeEstimates
|
|
|
|
from . import ElectrumTestCase
|
|
from .test_wallet_vertical import WalletIntegrityHelper, read_test_vector
|
|
|
|
WALLET_DATA = read_test_vector('cause_carbon_wallet.json')
|
|
|
|
class MockNetwork(Logger):
|
|
|
|
def __init__(self, config):
|
|
Logger.__init__(self)
|
|
self.config = config
|
|
self.fee_estimates = FeeTimeEstimates()
|
|
self.asyncio_loop = util.get_asyncio_loop()
|
|
self.interface = None
|
|
self.relay_fee = 1000
|
|
self.wallets = []
|
|
self._tx_queue = asyncio.Queue()
|
|
|
|
def get_local_height(self):
|
|
return 42
|
|
|
|
def blockchain(self):
|
|
class BlockchainMock:
|
|
def is_tip_stale(self):
|
|
return True
|
|
return BlockchainMock()
|
|
|
|
async def try_broadcasting(self, tx, name):
|
|
for w in self.wallets:
|
|
w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)
|
|
self._tx_queue.put_nowait(tx)
|
|
return tx.txid()
|
|
|
|
async def next_tx(self):
|
|
tx = await util.wait_for2(self._tx_queue.get(), timeout=10)
|
|
return tx
|
|
|
|
def is_connected(self):
|
|
return True
|
|
|
|
|
|
SWAP_FUNDING_TX = "01000000000101500e9d67647481864edfb020b5c45e1c40d90f06b0130f9faed1a5149c6d26450000000000ffffffff0226080300000000002200205059c44bf57534303ab8f090f06b7bde58f5d2522440247a1ff6b41bdca9348df312c20100000000160014021d4f3b17921d790e1c022367a5bb078ce4deb402483045022100d41331089a2031396a1db8e4dec6dda9cacefe1288644b92f8e08a23325aa19b02204159230691601f7d726e4e6e0b7124d3377620f400d699a01095f0b0a09ee26a012102d60315c72c0cefd41c6d07883c20b88be3fc37aac7912f0052722a95de0de71600000000"
|
|
SWAP_CLAIM_TX = "02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e89000000000001000000017005030000000000160014b113a47f3718da3fd161339a6681c150fef2cfe30347304402204c6d40103589b1a8177a37a824f0c66a3a7b22bc570b14c9e07965b56f6ace8f02203a35cffe0ab10de00f3e15ecf5aafdd2c7f6c62da11edd9054a1bce7a9e1455c0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000"
|
|
|
|
SWAPDATA = SwapData(
|
|
is_reverse=True,
|
|
locktime=2420532,
|
|
onchain_amount=198694,
|
|
lightning_amount=200000,
|
|
redeem_script=bytes.fromhex('8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac'),
|
|
preimage=bytes.fromhex('f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a36'),
|
|
prepay_hash=None,
|
|
privkey=bytes.fromhex('58fd0018a9a2737d1d6b81d380df96bf0c858473a9592015508a270a7c9b1d8d'),
|
|
lockup_address='tb1q2pvugjl4w56rqw4c7zg0q6mmmev0t5jjy3qzg7sl766phh9fxjxsrtl77t',
|
|
receive_address='tb1ql0adrj58g88xgz375yct63rclhv29hv03u0mel',
|
|
funding_txid='897eea7f53e917323e7472d7a2e3099173f7836c57f1b6850f5cbdfe8085dbf9',
|
|
spending_txid=None,
|
|
is_redeemed=False,
|
|
)
|
|
|
|
txin = PartialTxInput(
|
|
prevout=TxOutpoint(txid=bytes.fromhex(SWAPDATA.funding_txid), out_idx=0),
|
|
)
|
|
txin._trusted_value_sats = SWAPDATA.onchain_amount
|
|
txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=SWAPDATA)
|
|
SWAP_SWEEP_INFO = SweepInfo(
|
|
txin=txin,
|
|
cltv_abs=locktime,
|
|
txout=None,
|
|
name='swap claim',
|
|
can_be_batched=True,
|
|
)
|
|
|
|
|
|
class TestTxBatcher(ElectrumTestCase):
|
|
|
|
TESTNET = True
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
console_stderr_handler.setLevel(logging.DEBUG)
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
|
self.config.FEE_POLICY = 'feerate:5000'
|
|
|
|
async def asyncSetUp(self):
|
|
await super().asyncSetUp()
|
|
self.network = MockNetwork(self.config)
|
|
|
|
def create_standard_wallet_from_seed(self, seed_words, *, config=None, gap_limit=2):
|
|
if config is None:
|
|
config = self.config
|
|
ks = keystore.from_seed(seed_words, passphrase='', for_multisig=False)
|
|
return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=gap_limit, config=config)
|
|
|
|
def _create_wallet(self):
|
|
wallet = self.create_standard_wallet_from_seed(WALLET_DATA["seed"])
|
|
wallet.start_network(self.network)
|
|
wallet.txbatcher.SLEEP_INTERVAL = 0.01
|
|
self.network.wallets.append(wallet)
|
|
return wallet
|
|
|
|
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
|
|
async def test_batch_payments(self, mock_save_db):
|
|
# output 1: tx1(o1) ---------------
|
|
# \
|
|
# output 2: tx1'(o1,o2) ----> tx2(tx1|o2)
|
|
#
|
|
# tx1 is broadcast, and replaced by tx1'
|
|
# tx1 gets mined
|
|
# txbatcher creates a new transaction tx2, child of tx1
|
|
#
|
|
OUTGOING_ADDRESS = 'tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n'
|
|
wallet = self._create_wallet()
|
|
# fund wallet
|
|
funding_tx = Transaction(WALLET_DATA["funding_tx"])
|
|
await self.network.try_broadcasting(funding_tx, 'funding')
|
|
await self.network.next_tx()
|
|
assert wallet.adb.get_transaction(funding_tx.txid()) is not None
|
|
self.logger.info(f'wallet balance {wallet.get_balance()}')
|
|
# payment 1 -> tx1(output1)
|
|
output1 = PartialTxOutput.from_address_and_value(OUTGOING_ADDRESS, 10_000)
|
|
wallet.txbatcher.add_payment_output('default', output1)
|
|
tx1 = await self.network.next_tx()
|
|
assert output1 in tx1.outputs()
|
|
# payment 2 -> tx2(output1, output2)
|
|
output2 = PartialTxOutput.from_address_and_value(OUTGOING_ADDRESS, 20_000)
|
|
wallet.txbatcher.add_payment_output('default', output2)
|
|
tx1_prime = await self.network.next_tx()
|
|
assert wallet.adb.get_transaction(tx1_prime.txid()) is not None
|
|
assert len(tx1_prime.outputs()) == 3
|
|
assert output1 in tx1_prime.outputs()
|
|
assert output2 in tx1_prime.outputs()
|
|
# tx1 gets confirmed, tx2 gets removed
|
|
wallet.adb.receive_tx_callback(tx1, tx_height=1)
|
|
tx_mined_status = wallet.adb.get_tx_height(tx1.txid())
|
|
wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))
|
|
assert wallet.adb.get_transaction(tx1.txid()) is not None
|
|
assert wallet.adb.get_transaction(tx1_prime.txid()) is None
|
|
# txbatcher creates tx2
|
|
tx2 = await self.network.next_tx()
|
|
assert output1 in tx1.outputs()
|
|
assert output2 in tx2.outputs()
|
|
# check that tx2 is child of tx1
|
|
assert len(tx2.inputs()) == 1
|
|
assert tx2.inputs()[0].prevout.txid.hex() == tx1.txid()
|
|
|
|
|
|
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
|
|
async def test_rbf_batching__cannot_batch_as_would_need_to_use_ismine_outputs_of_basetx(self, mock_save_db):
|
|
"""Wallet history contains unconf tx1 that spends all its coins to two ismine outputs,
|
|
one 'recv' address (20k sats) and one 'change' (80k sats).
|
|
The user tries to create tx2, that pays an invoice for 90k sats.
|
|
The tx batcher fails to batch, and should create a child transaction
|
|
"""
|
|
wallet = self._create_wallet()
|
|
# fund wallet
|
|
funding_tx = Transaction(WALLET_DATA['funding_tx'])
|
|
await self.network.try_broadcasting(funding_tx, 'funding')
|
|
await self.network.next_tx()
|
|
assert wallet.adb.get_transaction(funding_tx.txid()) is not None
|
|
self.logger.info(f'wallet balance1 {wallet.get_balance()}')
|
|
|
|
# to_self_payment tx1
|
|
output1 = PartialTxOutput.from_address_and_value("tb1qyfnv3y866ufedugxxxfksyratv4pz3h78g9dad", 20_000)
|
|
wallet.txbatcher.add_payment_output('default', output1)
|
|
tx1 = await self.network.next_tx()
|
|
assert len(tx1.outputs()) == 2
|
|
assert output1 in tx1.outputs()
|
|
|
|
# outgoing payment tx2
|
|
output2 = PartialTxOutput.from_address_and_value("tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h", 90_000)
|
|
wallet.txbatcher.add_payment_output('default', output2)
|
|
# before tx1 gets confirmed, txbatch.create_transaction will raise notenoughfunds
|
|
await asyncio.sleep(wallet.txbatcher.SLEEP_INTERVAL)
|
|
|
|
# tx1 gets confirmed
|
|
wallet.adb.receive_tx_callback(tx1, tx_height=1)
|
|
tx_mined_status = wallet.adb.get_tx_height(tx1.txid())
|
|
wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))
|
|
|
|
tx2 = await self.network.next_tx()
|
|
assert len(tx2.outputs()) == 2
|
|
assert output2 in tx2.outputs()
|
|
|
|
|
|
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
|
|
async def test_sweep_from_submarine_swap(self, mock_save_db):
|
|
self.maxDiff = None
|
|
# create wallet
|
|
wallet = self._create_wallet()
|
|
wallet.adb.db.transactions[SWAPDATA.funding_txid] = tx = Transaction(SWAP_FUNDING_TX)
|
|
wallet.adb.receive_tx_callback(tx, tx_height=1)
|
|
tx_mined_status = wallet.adb.get_tx_height(tx.txid())
|
|
wallet.adb.add_verified_tx(tx.txid(), dataclasses.replace(tx_mined_status, conf=1))
|
|
wallet.txbatcher.add_sweep_input('default', SWAP_SWEEP_INFO)
|
|
tx = await self.network.next_tx()
|
|
txid = tx.txid()
|
|
self.assertEqual(SWAP_CLAIM_TX, str(tx))
|
|
# add a new payment, reusing the same input
|
|
# this tests that txin.make_witness() can be called more than once
|
|
output1 = PartialTxOutput.from_address_and_value("tb1qyfnv3y866ufedugxxxfksyratv4pz3h78g9dad", 20_000)
|
|
wallet.txbatcher.add_payment_output('default', output1)
|
|
new_tx = await self.network.next_tx()
|
|
# check that we batched with previous tx
|
|
assert new_tx.inputs()[0].prevout == tx.inputs()[0].prevout == txin.prevout
|
|
assert output1 in new_tx.outputs()
|
|
|
|
async def _wait_for_base_tx(self, txbatch, should_be_none=False):
|
|
async with timeout_after(10):
|
|
while True:
|
|
base_tx = txbatch._base_tx
|
|
if (base_tx is not None) ^ should_be_none:
|
|
return base_tx
|
|
await asyncio.sleep(0.1)
|