Skip to content

Commit

Permalink
commands: add is_from_self to listcoins response
Browse files Browse the repository at this point in the history
  • Loading branch information
jp1ac4 committed Nov 28, 2024
1 parent 6db2599 commit 16726f5
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 15 deletions.
1 change: 1 addition & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ A coin may have one of the following four statuses:
| `spend_info` | object | Information about the transaction spending this coin. See [Spending transaction info](#spending_transaction_info). |
| `is_immature` | bool | Whether this coin was created by a coinbase transaction that is still immature. |
| `is_change` | bool | Whether the coin deposit address was derived from the change descriptor. |
| `is_from_self` | bool | Whether the coin and all its unconfirmed ancestors, if any, are outputs of transactions from this wallet. |


##### Spending transaction info
Expand Down
4 changes: 4 additions & 0 deletions liana-gui/src/app/state/coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ mod tests {
address: dummy_address.clone(),
derivation_index: 0.into(),
is_change: false,
is_from_self: false,
},
Coin {
outpoint: bitcoin::OutPoint { txid, vout: 3 },
Expand All @@ -236,6 +237,7 @@ mod tests {
address: dummy_address.clone(),
derivation_index: 1.into(),
is_change: false,
is_from_self: false,
},
Coin {
outpoint: bitcoin::OutPoint { txid, vout: 0 },
Expand All @@ -246,6 +248,7 @@ mod tests {
address: dummy_address.clone(),
derivation_index: 2.into(),
is_change: false,
is_from_self: false,
},
Coin {
outpoint: bitcoin::OutPoint { txid, vout: 1 },
Expand All @@ -256,6 +259,7 @@ mod tests {
address: dummy_address,
derivation_index: 3.into(),
is_change: false,
is_from_self: false,
},
]);

Expand Down
1 change: 1 addition & 0 deletions liana-gui/src/app/state/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,7 @@ mod tests {
"derivation_index": 0,
"is_immature": false,
"is_change": false,
"is_from_self": false,

}]})),
),
Expand Down
3 changes: 3 additions & 0 deletions liana-gui/src/lianalite/client/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ impl Daemon for BackendWalletClient {
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
})
.collect(),
})
Expand Down Expand Up @@ -1133,6 +1134,7 @@ fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTran
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
});
}
}
Expand Down Expand Up @@ -1188,6 +1190,7 @@ fn spend_tx_from_api(
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions lianad/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ impl DaemonControl {
spend_block,
is_immature,
is_change,
is_from_self,
derivation_index,
..
} = coin;
Expand All @@ -445,6 +446,7 @@ impl DaemonControl {
spend_info,
is_immature,
is_change,
is_from_self,
}
})
.collect();
Expand Down Expand Up @@ -1229,6 +1231,10 @@ pub struct ListCoinsEntry {
pub is_immature: bool,
/// Whether the coin deposit address was derived from the change descriptor.
pub is_change: bool,
/// Whether the coin is the output of a transaction whose inputs are all from
/// this same wallet. If the coin is unconfirmed, it also means that all its
/// unconfirmed ancestors, if any, are also from self.
pub is_from_self: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
60 changes: 49 additions & 11 deletions tests/test_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,36 @@ def test_reorg_exclusion(lianad, bitcoind):
coin_b = get_coin(lianad, txid)
b_spend_tx = spend_coins(lianad, bitcoind, [coin_b])

# These are external deposits so not from self.
assert coin_a["is_from_self"] is False
assert coin_b["is_from_self"] is False

# A confirmed and spent coin
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 3)
bitcoind.generate_block(1, wait_for_mempool=txid)
txid_c = bitcoind.rpc.sendtoaddress(addr, 3)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 3)
coin_c = get_coin(lianad, txid)
c_spend_tx = spend_coins(lianad, bitcoind, [coin_c])
bitcoind.generate_block(1, wait_for_mempool=1)
# Now refresh this coin while it is unconfirmed.
res = lianad.rpc.createspend({}, [get_coin(lianad, txid_c)["outpoint"]], 1)
c_spend_psbt = PSBT.from_base64(res["psbt"])
txid_d = sign_and_broadcast_psbt(lianad, c_spend_psbt)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 4)
coin_c = get_coin(lianad, txid_c)
coin_d = get_coin(lianad, txid_d)
assert coin_c["is_from_self"] is False
assert coin_c["block_height"] is None
# Even though coin_d is from a self-send, coin_c is still unconfirmed
# and is not from self. Therefore, coin_d is not from self either.
assert coin_d["is_from_self"] is False

bitcoind.generate_block(1)
# Wait for confirmation to be detected.
wait_for(lambda: get_coin(lianad, txid_d)["block_height"] is not None)
coin_c = get_coin(lianad, txid_c)
coin_d = get_coin(lianad, txid_d)
assert coin_c["is_from_self"] is False
assert coin_c["block_height"] is not None
assert coin_d["is_from_self"] is True
assert coin_d["block_height"] is not None

# Make sure the transaction were confirmed >10 blocks ago, so bitcoind won't update the
# mempool during the reorg to the initial height.
Expand All @@ -108,7 +130,7 @@ def test_reorg_exclusion(lianad, bitcoind):
tx = bitcoind.rpc.gettransaction(txid)["hex"]
bitcoind.rpc.sendrawtransaction(tx)
bitcoind.rpc.sendrawtransaction(b_spend_tx)
bitcoind.rpc.sendrawtransaction(c_spend_tx)
sign_and_broadcast_psbt(lianad, c_spend_psbt)
bitcoind.generate_block(1, wait_for_mempool=5)
new_height = bitcoind.rpc.getblockcount()
wait_for(lambda: lianad.rpc.getinfo()["block_height"] == new_height)
Expand All @@ -128,9 +150,11 @@ def test_reorg_exclusion(lianad, bitcoind):
for c in lianad.rpc.listcoins()["coins"]
if coin_c["outpoint"] == c["outpoint"]
)
c_spend_txid = get_txid(c_spend_tx)
assert new_coin_c["spend_info"]["txid"] == c_spend_txid
assert new_coin_c["spend_info"]["txid"] == txid_d
assert new_coin_c["spend_info"]["height"] == new_height
new_coin_d = get_coin(lianad, txid_d)
assert new_coin_d["is_from_self"] is True
assert new_coin_d["block_height"] == new_height

# TODO: maybe test with some malleation for the deposit and spending txs?

Expand Down Expand Up @@ -162,17 +186,26 @@ def test_reorg_status_recovery(lianad, bitcoind):
assert initial_height > 100
wait_for(lambda: lianad.rpc.getinfo()["block_height"] == initial_height)

# Both coins are confirmed. Spend the second one then get their infos.
# Both coins are confirmed. Refresh the second one then get their infos.
wait_for(lambda: len(list_coins()) == 2)
wait_for(lambda: all(c["block_height"] is not None for c in list_coins()))
coin_b = get_coin(lianad, txids[1])
tx = spend_coins(lianad, bitcoind, [coin_b])
locktime = bitcoind.rpc.decoderawtransaction(tx)["locktime"]
# Refresh coin_b.
res = lianad.rpc.createspend({}, [coin_b["outpoint"]], 1)
b_spend_psbt = PSBT.from_base64(res["psbt"])
txid = sign_and_broadcast_psbt(lianad, b_spend_psbt)
coin_c = get_coin(lianad, txid)
# coin_c is unconfirmed and marked as from self as its parent is confirmed.
assert coin_c["block_height"] is None
assert coin_c["is_from_self"] is True

locktime = b_spend_psbt.tx.nLockTime
assert initial_height - 100 <= locktime <= initial_height
bitcoind.generate_block(1, wait_for_mempool=1)
wait_for(lambda: spend_confirmed_noticed(lianad, coin_b["outpoint"]))
coin_a = get_coin(lianad, txids[0])
coin_b = get_coin(lianad, txids[1])
coin_c = get_coin(lianad, txid)

# Reorg the chain down to the initial height without shifting nor malleating
# any transaction. The coin info should be identical (except the spend info
Expand All @@ -187,9 +220,14 @@ def test_reorg_status_recovery(lianad, bitcoind):
if locktime == initial_height:
# Cannot be mined until next block (initial_height + 1).
coin_b["spend_info"] = None
# coin_c no longer exists.
with pytest.raises(StopIteration):
get_coin(lianad, coin_c["outpoint"])
else:
# Otherwise, the tx will be mined at the height the reorg happened.
coin_b["spend_info"]["height"] = initial_height
new_coin_c = get_coin(lianad, coin_c["outpoint"])
assert new_coin_c["is_from_self"] is True
assert new_coin_b == coin_b


Expand Down
20 changes: 16 additions & 4 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,22 @@ def test_coinbase_deposit(lianad, bitcoind):
wait_for_sync()
coins = lianad.rpc.listcoins()["coins"]
assert (
len(coins) == 1 and coins[0]["is_immature"] and coins[0]["spend_info"] is None
len(coins) == 1
and coins[0]["is_immature"]
and coins[0]["spend_info"] is None
and not coins[0]["is_from_self"]
)

# Generate 100 blocks to make the coinbase mature. We should detect it as such.
# It remains as not from self.
bitcoind.generate_block(100)
wait_for_sync()
coin = lianad.rpc.listcoins()["coins"][0]
assert not coin["is_immature"] and coin["block_height"] is not None
assert (
not coin["is_immature"]
and coin["block_height"] is not None
and not coins[0]["is_from_self"]
)

# We must be able to spend the mature coin.
destinations = {bitcoind.rpc.getnewaddress(): int(0.999999 * COIN)}
Expand Down Expand Up @@ -249,13 +257,17 @@ def test_coinbase_deposit(lianad, bitcoind):
bitcoind.rpc.generatetoaddress(1, change_addr)
wait_for(lambda: any(c["is_immature"] for c in lianad.rpc.listcoins()["coins"]))
coin = next(c for c in lianad.rpc.listcoins()["coins"] if c["is_immature"])
assert coin["is_change"]
assert coin["is_change"] and not coin["is_from_self"]
bitcoind.generate_block(100)
wait_for_sync()
coin = next(
c for c in lianad.rpc.listcoins()["coins"] if c["outpoint"] == coin["outpoint"]
)
assert not coin["is_immature"] and coin["block_height"] is not None
assert (
not coin["is_immature"]
and coin["block_height"] is not None
and not coin["is_from_self"]
)


@pytest.mark.skipif(
Expand Down
Loading

0 comments on commit 16726f5

Please sign in to comment.