398 lines
15 KiB
Python
398 lines
15 KiB
Python
from fixtures import *
|
|
from test_framework.serializations import PSBT
|
|
from test_framework.utils import wait_for, COIN, RpcError
|
|
|
|
|
|
def test_spend_change(lianad, bitcoind):
|
|
"""We can spend a coin that was received on a change address."""
|
|
# Receive a coin on a receive address
|
|
addr = lianad.rpc.getnewaddress()["address"]
|
|
txid = bitcoind.rpc.sendtoaddress(addr, 0.01)
|
|
bitcoind.generate_block(1, wait_for_mempool=txid)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
|
|
|
|
# Create a transaction that will spend this coin to 1) one of our receive
|
|
# addresses 2) an external address 3) one of our change addresses.
|
|
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins()["coins"]]
|
|
destinations = {
|
|
bitcoind.rpc.getnewaddress(): 100_000,
|
|
lianad.rpc.getnewaddress()["address"]: 100_000,
|
|
}
|
|
res = lianad.rpc.createspend(destinations, outpoints, 2)
|
|
assert "psbt" in res
|
|
|
|
# The transaction must contain a change output.
|
|
spend_psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(spend_psbt.o) == 3
|
|
assert len(spend_psbt.tx.vout) == 3
|
|
|
|
# Sign and broadcast this first Spend transaction.
|
|
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
|
lianad.rpc.updatespend(signed_psbt.to_base64())
|
|
spend_txid = signed_psbt.tx.txid().hex()
|
|
lianad.rpc.broadcastspend(spend_txid)
|
|
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 3)
|
|
|
|
# Now create a new transaction that spends the change output as well as
|
|
# the output sent to the receive address.
|
|
outpoints = [
|
|
c["outpoint"]
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if c["spend_info"] is None
|
|
]
|
|
destinations = {
|
|
bitcoind.rpc.getnewaddress(): 100_000,
|
|
}
|
|
res = lianad.rpc.createspend(destinations, outpoints, 2)
|
|
spend_psbt = PSBT.from_base64(res["psbt"])
|
|
|
|
# We can sign and broadcast it.
|
|
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
|
lianad.rpc.updatespend(signed_psbt.to_base64())
|
|
spend_txid = signed_psbt.tx.txid().hex()
|
|
lianad.rpc.broadcastspend(spend_txid)
|
|
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
|
|
|
|
|
def sign_and_broadcast_psbt(lianad, psbt):
|
|
txid = psbt.tx.txid().hex()
|
|
psbt = lianad.signer.sign_psbt(psbt)
|
|
lianad.rpc.updatespend(psbt.to_base64())
|
|
lianad.rpc.broadcastspend(txid)
|
|
return txid
|
|
|
|
|
|
def test_coin_marked_spent(lianad, bitcoind):
|
|
"""Test a spent coin is marked as such under various conditions."""
|
|
# Receive a coin in a single transaction
|
|
addr = lianad.rpc.getnewaddress()["address"]
|
|
deposit_a = bitcoind.rpc.sendtoaddress(addr, 0.01)
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit_a)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
|
|
|
|
# Receive another coin on the same address
|
|
deposit_b = bitcoind.rpc.sendtoaddress(addr, 0.02)
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit_b)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
|
|
|
|
# Receive three coins in a single deposit transaction
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: 0.03,
|
|
lianad.rpc.getnewaddress()["address"]: 0.04,
|
|
lianad.rpc.getnewaddress()["address"]: 0.05,
|
|
}
|
|
deposit_c = bitcoind.rpc.sendmany("", destinations)
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit_c)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 5)
|
|
|
|
# Receive a coin in an unconfirmed deposit transaction
|
|
addr = lianad.rpc.getnewaddress()["address"]
|
|
deposit_d = bitcoind.rpc.sendtoaddress(addr, 0.06)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 5)
|
|
|
|
def sign_and_broadcast(psbt):
|
|
txid = psbt.tx.txid().hex()
|
|
psbt = lianad.signer.sign_psbt(psbt)
|
|
lianad.rpc.updatespend(psbt.to_base64())
|
|
lianad.rpc.broadcastspend(txid)
|
|
return txid
|
|
|
|
# Spend the first coin with a change output
|
|
outpoint = next(
|
|
c["outpoint"]
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if deposit_a in c["outpoint"]
|
|
)
|
|
destinations = {
|
|
bitcoind.rpc.getnewaddress(): 500_000,
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [outpoint], 6)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
sign_and_broadcast(psbt)
|
|
|
|
# Spend the second coin without a change output
|
|
outpoint = next(
|
|
c["outpoint"]
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if deposit_b in c["outpoint"]
|
|
)
|
|
destinations = {
|
|
bitcoind.rpc.getnewaddress(): int(0.02 * COIN) - 1_000,
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [outpoint], 1)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
sign_and_broadcast(psbt)
|
|
|
|
# Spend the third coin to an address of ours, no change
|
|
outpoints = [
|
|
c["outpoint"]
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if deposit_c in c["outpoint"]
|
|
]
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: int(0.03 * COIN) - 1_000,
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [outpoints[0]], 1)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
sign_and_broadcast(psbt)
|
|
|
|
# Spend the fourth coin to an address of ours, with change
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: int(0.04 * COIN / 2),
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [outpoints[1]], 18)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
sign_and_broadcast(psbt)
|
|
|
|
# Batch spend the fourth and fifth coins
|
|
outpoint = next(
|
|
c["outpoint"]
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if deposit_d in c["outpoint"]
|
|
)
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: int(0.01 * COIN),
|
|
lianad.rpc.getnewaddress()["address"]: int(0.01 * COIN),
|
|
bitcoind.rpc.getnewaddress(): int(0.01 * COIN),
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [outpoints[2], outpoint], 2)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
sign_and_broadcast(psbt)
|
|
|
|
# All the spent coins must have been detected as such
|
|
all_deposits = (deposit_a, deposit_b, deposit_c, deposit_d)
|
|
|
|
def deposited_coins():
|
|
return (
|
|
c
|
|
for c in lianad.rpc.listcoins()["coins"]
|
|
if c["outpoint"][:-2] in all_deposits
|
|
)
|
|
|
|
def is_spent(coin):
|
|
if coin["spend_info"] is None:
|
|
return False
|
|
if coin["spend_info"]["txid"] is None:
|
|
return False
|
|
return True
|
|
|
|
wait_for(lambda: all(is_spent(c) for c in deposited_coins()))
|
|
|
|
|
|
def test_send_to_self(lianad, bitcoind):
|
|
"""Test we can use createspend with no destination to send to a change address."""
|
|
# Get 3 coins.
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: 0.03,
|
|
lianad.rpc.getnewaddress()["address"]: 0.04,
|
|
lianad.rpc.getnewaddress()["address"]: 0.05,
|
|
}
|
|
deposit_txid = bitcoind.rpc.sendmany("", destinations)
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit_txid)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 3)
|
|
|
|
# Then create a send-to-self transaction (by not providing any destination) that
|
|
# sweeps them all.
|
|
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins()["coins"]]
|
|
specified_feerate = 142
|
|
res = lianad.rpc.createspend({}, outpoints, specified_feerate)
|
|
spend_psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(spend_psbt.o) == len(spend_psbt.tx.vout) == 1
|
|
|
|
# Note they may ask for an impossible send-to-self. In this case we'll error cleanly.
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Insufficient funds. Missing \\d+ sats",
|
|
):
|
|
lianad.rpc.createspend({}, outpoints, 40500)
|
|
|
|
# Sign and broadcast the send-to-self transaction created above.
|
|
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
|
lianad.rpc.updatespend(signed_psbt.to_base64())
|
|
spend_txid = signed_psbt.tx.txid().hex()
|
|
lianad.rpc.broadcastspend(spend_txid)
|
|
|
|
# The only output is the change output so the feerate of the transaction must
|
|
# not be lower than the one provided, and only possibly slightly higher (since
|
|
# we slightly overestimate the satisfaction size).
|
|
# FIXME: a 15% increase is huge.
|
|
res = bitcoind.rpc.getmempoolentry(spend_txid)
|
|
spend_feerate = int(res["fees"]["base"] * COIN / res["vsize"])
|
|
assert specified_feerate <= spend_feerate <= int(specified_feerate * 115 / 100)
|
|
|
|
# We should by now only have one coin.
|
|
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
|
unspent_coins = lambda: (
|
|
c for c in lianad.rpc.listcoins()["coins"] if c["spend_info"] is None
|
|
)
|
|
wait_for(lambda: len(list(unspent_coins())) == 1)
|
|
|
|
|
|
def test_coin_selection(lianad, bitcoind):
|
|
"""We can create a spend using coin selection."""
|
|
# Send to an (external) address.
|
|
dest_100_000 = {bitcoind.rpc.getnewaddress(): 100_000}
|
|
# Coin selection is not possible if we have no coins.
|
|
assert len(lianad.rpc.listcoins()["coins"]) == 0
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Coin selection error: 'Insufficient funds. Missing \\d+ sats.'",
|
|
):
|
|
lianad.rpc.createspend(dest_100_000, [], 2)
|
|
|
|
# Receive a coin in an unconfirmed deposit transaction.
|
|
recv_addr = lianad.rpc.getnewaddress()["address"]
|
|
deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0008) # 80_000 sats
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
|
|
# There are still no confirmed coins to use as candidates for selection.
|
|
assert len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 0
|
|
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Coin selection error: 'Insufficient funds. Missing \\d+ sats.'",
|
|
):
|
|
lianad.rpc.createspend(dest_100_000, [], 2)
|
|
|
|
# Confirm coin.
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
|
|
|
|
# Insufficient funds for coin selection.
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Coin selection error: 'Insufficient funds. Missing \\d+ sats.'",
|
|
):
|
|
lianad.rpc.createspend(dest_100_000, [], 2)
|
|
|
|
# Reduce spend amount.
|
|
dest_30_000 = {bitcoind.rpc.getnewaddress(): 30_000}
|
|
res = lianad.rpc.createspend(dest_30_000, [], 2)
|
|
assert "psbt" in res
|
|
|
|
# The transaction must contain a change output.
|
|
spend_psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(spend_psbt.o) == 2
|
|
assert len(spend_psbt.tx.vout) == 2
|
|
|
|
# Sign and broadcast this Spend transaction.
|
|
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
|
lianad.rpc.updatespend(signed_psbt.to_base64())
|
|
spend_txid = signed_psbt.tx.txid().hex()
|
|
lianad.rpc.broadcastspend(spend_txid)
|
|
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
|
|
coins = lianad.rpc.listcoins()["coins"]
|
|
# Check that change output is unconfirmed.
|
|
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
|
|
assert len(lianad.rpc.listcoins(["spending"])["coins"]) == 1
|
|
# Check we cannot use coins as candidates if they are spending/spent or unconfirmed.
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Coin selection error: 'Insufficient funds. Missing \\d+ sats.'",
|
|
):
|
|
lianad.rpc.createspend(dest_30_000, [], 2)
|
|
|
|
# Now confirm the Spend.
|
|
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
|
|
# But its value is not enough for this Spend.
|
|
dest_60_000 = {bitcoind.rpc.getnewaddress(): 60_000}
|
|
with pytest.raises(
|
|
RpcError,
|
|
match="Coin selection error: 'Insufficient funds. Missing \\d+ sats.'",
|
|
):
|
|
lianad.rpc.createspend(dest_60_000, [], 2)
|
|
|
|
# Get another coin to check coin selection with more than one candidate.
|
|
recv_addr = lianad.rpc.getnewaddress()["address"]
|
|
deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0002) # 20_000 sats
|
|
bitcoind.generate_block(1, wait_for_mempool=deposit)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
|
|
|
|
res = lianad.rpc.createspend(dest_60_000, [], 2)
|
|
assert "psbt" in res
|
|
|
|
# The transaction must contain a change output.
|
|
auto_psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(auto_psbt.o) == 2
|
|
assert len(auto_psbt.tx.vout) == 2
|
|
|
|
# Now create a transaction with manual coin selection using the same outpoints.
|
|
outpoints = [
|
|
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin
|
|
]
|
|
res_manual = lianad.rpc.createspend(dest_60_000, outpoints, 2)
|
|
manual_psbt = PSBT.from_base64(res_manual["psbt"])
|
|
|
|
# Recipient details are the same for both.
|
|
assert auto_psbt.tx.vout[0].nValue == manual_psbt.tx.vout[0].nValue
|
|
assert auto_psbt.tx.vout[0].scriptPubKey == manual_psbt.tx.vout[0].scriptPubKey
|
|
# Change amount is the same (change address will be different).
|
|
assert auto_psbt.tx.vout[1].nValue == manual_psbt.tx.vout[1].nValue
|
|
|
|
|
|
def test_sweep(lianad, bitcoind):
|
|
"""
|
|
Test we can leverage the change_address parameter to partially or completely sweep
|
|
the wallet's coins.
|
|
"""
|
|
|
|
# Get a bunch of coins. Don't even confirm them.
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: 0.8,
|
|
lianad.rpc.getnewaddress()["address"]: 0.12,
|
|
lianad.rpc.getnewaddress()["address"]: 1.87634,
|
|
lianad.rpc.getnewaddress()["address"]: 1.124,
|
|
}
|
|
bitcoind.rpc.sendmany("", destinations)
|
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 4)
|
|
|
|
# Create a sweep transaction. This should send the whole balance to the
|
|
# sweep address.
|
|
all_coins = lianad.rpc.listcoins()["coins"]
|
|
balance = sum(c["amount"] for c in all_coins)
|
|
all_outpoints = [c["outpoint"] for c in all_coins]
|
|
destinations = {}
|
|
change_addr = bitcoind.rpc.getnewaddress()
|
|
res = lianad.rpc.createspend(destinations, all_outpoints, 1, change_addr)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(psbt.tx.vout) == 1
|
|
assert psbt.tx.vout[0].nValue > balance - 500
|
|
sign_and_broadcast_psbt(lianad, psbt)
|
|
wait_for(
|
|
lambda: all(
|
|
c["spend_info"] is not None for c in lianad.rpc.listcoins()["coins"]
|
|
)
|
|
)
|
|
|
|
# Create a partial sweep and specify some destinations to be set before the
|
|
# sweep output. To make it even more confusing, set one such destination as
|
|
# an internal (but receive) address.
|
|
destinations = {
|
|
lianad.rpc.getnewaddress()["address"]: 0.5,
|
|
lianad.rpc.getnewaddress()["address"]: 0.2,
|
|
lianad.rpc.getnewaddress()["address"]: 0.1,
|
|
}
|
|
txid = bitcoind.rpc.sendmany("", destinations)
|
|
bitcoind.generate_block(1, wait_for_mempool=txid)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 3)
|
|
received_coins = lianad.rpc.listcoins(["confirmed"])["coins"]
|
|
spent_coin = next(c for c in received_coins if c["amount"] == 0.5 * COIN)
|
|
destinations = {
|
|
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30": int(
|
|
0.1 * COIN
|
|
),
|
|
lianad.rpc.getnewaddress()["address"]: int(0.3 * COIN),
|
|
}
|
|
res = lianad.rpc.createspend(destinations, [spent_coin["outpoint"]], 1, change_addr)
|
|
psbt = PSBT.from_base64(res["psbt"])
|
|
assert len(psbt.tx.vout) == 3
|
|
sign_and_broadcast_psbt(lianad, psbt)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1)
|
|
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
|
|
balance = sum(
|
|
c["amount"] for c in lianad.rpc.listcoins(["unconfirmed", "confirmed"])["coins"]
|
|
)
|
|
assert balance == int((0.2 + 0.1 + 0.3) * COIN)
|