Skip to content

Commit

Permalink
add list transactions command to the wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
OBorce committed Feb 2, 2024
1 parent dc12c93 commit 26b6ae8
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 1 deletion.
6 changes: 6 additions & 0 deletions test/functional/test_framework/wallet_cli_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,5 +367,11 @@ async def list_pending_transactions(self) -> List[str]:
pattern = r'id: Id<Transaction>\{0x([^}]*)\}'
return re.findall(pattern, output)

async def list_transactions_by_address(self, address: Optional[str] = None, limit: int = 100) -> List[str]:
address = address if address else ''
output = await self._write_command(f"transaction-list-by-address {address} --limit {limit}\n")
pattern = r'id: Id<Transaction>\{0x([^}]*)\}'
return re.findall(pattern, output)

async def abandon_transaction(self, tx_id: str) -> str:
return await self._write_command(f"transaction-abandon {tx_id}\n")
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class UnicodeOnWindowsError(ValueError):
'feature_db_reinit.py',
'feature_lmdb_backend_test.py',
'wallet_conflict.py',
'wallet_list_txs.py',
'wallet_tx_compose.py',
'wallet_data_deposit.py',
'wallet_submit_tx.py',
Expand Down
134 changes: 134 additions & 0 deletions test/functional/wallet_list_txs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# Copyright (c) 2023 RBB S.r.l
# Copyright (c) 2017-2021 The Bitcoin Core developers
# [email protected]
# SPDX-License-Identifier: MIT
# Licensed under the MIT License;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wallet submission test
Check that:
* We can create a new wallet,
* get an address
* send coins to the wallet's address
* sync the wallet with the node
* check balance
* create a new address
* create some txs for that address
* list the txs for that address
"""

import json
from test_framework.test_framework import BitcoinTestFramework
from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN)
from test_framework.util import assert_in, assert_equal
from test_framework.mintlayer import mintlayer_hash, block_input_data_obj
from test_framework.wallet_cli_controller import WalletCliController

import asyncio
import sys
import random


class WalletListTransactions(BitcoinTestFramework):

def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
relay_fee_rate = random.randint(1, 100_000_000)
self.extra_args = [[
"--blockprod-min-peers-to-produce-blocks=0",
f"--min-tx-relay-fee-rate={relay_fee_rate}",
]]

def setup_network(self):
self.setup_nodes()
self.sync_all(self.nodes[0:1])

def generate_block(self):
node = self.nodes[0]

block_input_data = { "PoW": { "reward_destination": "AnyoneCanSpend" } }
block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:]

# create a new block, taking transactions from mempool
block = node.blockprod_generate_block(block_input_data, [], [], "FillSpaceFromMempool")
node.chainstate_submit_block(block)
block_id = node.chainstate_best_block_id()

# Wait for mempool to sync
self.wait_until(lambda: node.mempool_local_best_block_id() == block_id, timeout = 5)

return block_id

def run_test(self):
if 'win32' in sys.platform:
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
asyncio.run(self.async_test())

async def async_test(self):
node = self.nodes[0]
async with WalletCliController(node, self.config, self.log) as wallet:
# new wallet
await wallet.create_wallet()

# check it is on genesis
best_block_height = await wallet.get_best_block_height()
self.log.info(f"best block height = {best_block_height}")
assert_equal(best_block_height, '0')

# new address
pub_key_bytes = await wallet.new_public_key()
assert_equal(len(pub_key_bytes), 33)

# Get chain tip
tip_id = node.chainstate_best_block_id()
genesis_block_id = tip_id
self.log.debug(f'Tip: {tip_id}')

# Submit a valid transaction
coins_to_send = random.randint(200, 300)
output = {
'Transfer': [ { 'Coin': coins_to_send * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ],
}
encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0)

node.mempool_submit_transaction(encoded_tx, {})
assert node.mempool_contains_tx(tx_id)

self.generate_block() # Block 1
assert not node.mempool_contains_tx(tx_id)

# sync the wallet
assert_in("Success", await wallet.sync())

address = await wallet.new_address()
num_txs_to_create = random.randint(1, 10)
for _ in range(num_txs_to_create):
output = await wallet.send_to_address(address, 1)
assert_in("The transaction was submitted successfully", output)

self.generate_block()
assert_in("Success", await wallet.sync())

limit = random.randint(1, 100)
txs = await wallet.list_transactions_by_address(address, limit)
assert_equal(len(txs), min(num_txs_to_create, limit))

# without an address
txs = await wallet.list_transactions_by_address()
assert_equal(len(txs), num_txs_to_create+1)


if __name__ == '__main__':
WalletListTransactions().main()

8 changes: 8 additions & 0 deletions wallet/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,14 @@ impl Account {
self.output_cache.pending_transactions()
}

pub fn mainchain_transactions(
&self,
destination: Option<Destination>,
limit: usize,
) -> Vec<WithId<&Transaction>> {
self.output_cache.mainchain_transactions(destination, limit)
}

pub fn abandon_transaction(
&mut self,
tx_id: Id<Transaction>,
Expand Down
56 changes: 55 additions & 1 deletion wallet/src/account/output_cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.

use std::{
cmp::Reverse,
collections::{btree_map::Entry, BTreeMap, BTreeSet},
ops::Add,
};
Expand Down Expand Up @@ -44,7 +45,7 @@ use wallet_types::{
AccountWalletTxId, BlockInfo, WalletTx,
};

use crate::{WalletError, WalletResult};
use crate::{get_tx_output_destination, WalletError, WalletResult};

pub type UtxoWithTxOutput<'a> = (UtxoOutPoint, (&'a TxOutput, Option<TokenId>));

Expand Down Expand Up @@ -1173,6 +1174,59 @@ impl OutputCache {
.collect()
}

pub fn mainchain_transactions(
&self,
destination: Option<Destination>,
limit: usize,
) -> Vec<WithId<&Transaction>> {
let mut txs: Vec<&WalletTx> = self.txs.values().collect();
txs.sort_by_key(|tx| Reverse(tx.state().block_height()));

txs.iter()
.filter_map(|tx| match tx {
WalletTx::Block(_) => None,
WalletTx::Tx(tx) => match tx.state() {
TxState::Confirmed(_, _, _) => {
let tx = tx.get_transaction_with_id();
if let Some(dest) = &destination {
(self.destination_in_tx_outputs(&tx, dest)
|| self.destination_in_tx_inputs(&tx, dest))
.then_some(tx)
} else {
Some(tx)
}
}
TxState::Inactive(_)
| TxState::Conflicted(_)
| TxState::InMempool(_)
| TxState::Abandoned => None,
},
})
.take(limit)
.collect()
}

/// Returns true if the destination is found in the transaction's inputs
fn destination_in_tx_inputs(&self, tx: &WithId<&Transaction>, dest: &Destination) -> bool {
tx.inputs().iter().any(|inp| match inp {
TxInput::Utxo(utxo) => self
.txs
.get(&utxo.source_id())
.and_then(|tx| tx.outputs().get(utxo.output_index() as usize))
.and_then(|txo| get_tx_output_destination(txo, &|pool_id| self.pools.get(pool_id)))
.map_or(false, |output_dest| &output_dest == dest),
TxInput::Account(_) | TxInput::AccountCommand(_, _) => false,
})
}

/// Returns true if the destination is found in the transaction's outputs
fn destination_in_tx_outputs(&self, tx: &WithId<&Transaction>, dest: &Destination) -> bool {
tx.outputs().iter().any(|txo| {
get_tx_output_destination(txo, &|pool_id| self.pools.get(pool_id))
.map_or(false, |output_dest| &output_dest == dest)
})
}

/// Mark a transaction and its descendants as abandoned
/// Returns a Vec of the transaction Ids that have been abandoned
pub fn abandon_transaction(
Expand Down
11 changes: 11 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,17 @@ impl<B: storage::Backend> Wallet<B> {
Ok(transactions)
}

pub fn mainchain_transactions(
&self,
account_index: U31,
destination: Option<Destination>,
limit: usize,
) -> WalletResult<Vec<WithId<&Transaction>>> {
let account = self.get_account(account_index)?;
let transactions = account.mainchain_transactions(destination, limit);
Ok(transactions)
}

pub fn abandon_transaction(
&mut self,
account_index: U31,
Expand Down
64 changes: 64 additions & 0 deletions wallet/src/wallet/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,70 @@ fn wallet_get_transaction(#[case] seed: Seed) {
assert_eq!(found_tx.get_transaction(), tx.transaction());
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
fn wallet_list_confirmed_transactions(#[case] seed: Seed) {
let mut rng = make_seedable_rng(seed);
let chain_config = Arc::new(create_regtest());

let mut wallet = create_wallet(chain_config.clone());
// Generate a new block which sends reward to the wallet
let block1_amount = Amount::from_atoms(rng.gen_range(100000..1000000));
let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0);
let dest = addr.decode_object(&chain_config).unwrap();

let coin_balance = get_coin_balance(&wallet);
assert_eq!(coin_balance, block1_amount);

// send some coins to the address
let tx = wallet
.create_transaction_to_addresses(
DEFAULT_ACCOUNT_INDEX,
[TxOutput::Transfer(OutputValue::Coin(block1_amount), dest.clone())],
vec![],
FeeRate::from_amount_per_kb(Amount::ZERO),
FeeRate::from_amount_per_kb(Amount::ZERO),
)
.unwrap();

let send_tx_id = tx.transaction().get_id();

// put the tx in a block and scan it as confirmed
let _ = create_block(
&chain_config,
&mut wallet,
vec![tx.clone()],
Amount::ZERO,
1,
);

let tx = wallet
.create_transaction_to_addresses(
DEFAULT_ACCOUNT_INDEX,
[gen_random_transfer(&mut rng, block1_amount)],
vec![],
FeeRate::from_amount_per_kb(Amount::ZERO),
FeeRate::from_amount_per_kb(Amount::ZERO),
)
.unwrap();
let spend_from_tx_id = tx.transaction().get_id();

let _ = create_block(
&chain_config,
&mut wallet,
vec![tx.clone()],
Amount::ZERO,
2,
);

let txs = wallet.mainchain_transactions(DEFAULT_ACCOUNT_INDEX, Some(dest), 100).unwrap();
// should have 2 txs the send to and the spent from
assert_eq!(txs.len(), 2);
assert!(txs.iter().any(|tx| tx.get_id() == send_tx_id));
assert!(txs.iter().any(|tx| tx.get_id() == spend_from_tx_id));
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
Expand Down
19 changes: 19 additions & 0 deletions wallet/wallet-cli-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,16 @@ pub enum WalletCommand {
#[clap(name = "transaction-list-pending")]
ListPendingTransactions,

/// List mainchain transactions with optional address filter
#[clap(name = "transaction-list-by-address")]
ListMainchainTransactions {
/// Address to filter by
address: Option<String>,
/// limit the number of printed transactions, default is 100
#[arg(long = "limit", default_value_t = 100)]
limit: usize,
},

/// Get a transaction from the wallet, if present
#[clap(name = "transaction-get")]
GetTransaction {
Expand Down Expand Up @@ -1477,6 +1487,15 @@ where
Ok(ConsoleCommand::Print(format!("{utxos:#?}")))
}

WalletCommand::ListMainchainTransactions { address, limit } => {
let selected_account = self.get_selected_acc()?;
let utxos = self
.wallet_rpc
.mainchain_transactions(selected_account, address, limit)
.await?;
Ok(ConsoleCommand::Print(format!("{utxos:#?}")))
}

WalletCommand::GetTransaction { transaction_id } => {
let selected_account = self.get_selected_acc()?;
let tx = self
Expand Down
10 changes: 10 additions & 0 deletions wallet/wallet-controller/src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> {
.map_err(ControllerError::WalletError)
}

pub fn mainchain_transactions(
&self,
destination: Option<Destination>,
limit: usize,
) -> Result<Vec<WithId<&'a Transaction>>, ControllerError<T>> {
self.wallet
.mainchain_transactions(self.account_index, destination, limit)
.map_err(ControllerError::WalletError)
}

pub fn get_transaction_list(
&self,
skip: usize,
Expand Down
Loading

0 comments on commit 26b6ae8

Please sign in to comment.