Skip to content

Commit

Permalink
Merge pull request #1527 from mintlayer/feature/wallet-list-transactions
Browse files Browse the repository at this point in the history
add list transactions command to the wallet
  • Loading branch information
OBorce committed Feb 6, 2024
2 parents 0c4f5f7 + a8ea2ae commit fdd9b51
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 4 deletions.
5 changes: 5 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,10 @@ 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")
return output.split('\n')[3:][::2]

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()

10 changes: 9 additions & 1 deletion wallet/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ use wallet_types::{
};

pub use self::output_cache::{
DelegationData, FungibleTokenInfo, PoolData, UnconfirmedTokenInfo, UtxoWithTxOutput,
DelegationData, FungibleTokenInfo, PoolData, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput,
};
use self::output_cache::{OutputCache, TokenIssuanceData};
use self::transaction_list::{get_transaction_list, TransactionList};
Expand Down Expand Up @@ -1804,6 +1804,14 @@ impl Account {
self.output_cache.pending_transactions()
}

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

pub fn abandon_transaction(
&mut self,
tx_id: Id<Transaction>,
Expand Down
75 changes: 73 additions & 2 deletions wallet/src/account/output_cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
// limitations under the License.

use std::{
cmp::Reverse,
collections::{btree_map::Entry, BTreeMap, BTreeSet},
ops::Add,
};

use common::{
chain::{
block::timestamp::BlockTimestamp,
output_value::OutputValue,
stakelock::StakePoolData,
tokens::{
Expand All @@ -30,7 +32,7 @@ use common::{
AccountCommand, AccountNonce, AccountSpending, DelegationId, Destination, GenBlock,
OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint,
},
primitives::{id::WithId, per_thousand::PerThousand, Amount, BlockHeight, Id},
primitives::{id::WithId, per_thousand::PerThousand, Amount, BlockHeight, Id, Idable},
};
use crypto::vrf::VRFPublicKey;
use itertools::Itertools;
Expand All @@ -44,10 +46,26 @@ 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>));

pub struct TxInfo {
pub id: Id<Transaction>,
pub height: BlockHeight,
pub timestamp: BlockTimestamp,
}

impl TxInfo {
fn new(id: Id<Transaction>, height: BlockHeight, timestamp: BlockTimestamp) -> Self {
Self {
id,
height,
timestamp,
}
}
}

pub struct DelegationData {
pub pool_id: PoolId,
pub destination: Destination,
Expand Down Expand Up @@ -1173,6 +1191,59 @@ impl OutputCache {
.collect()
}

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

txs.iter()
.filter_map(|tx| match tx {
WalletTx::Block(_) => None,
WalletTx::Tx(tx) => match tx.state() {
TxState::Confirmed(block_height, timestamp, _) => {
let tx_with_id = tx.get_transaction_with_id();
if let Some(dest) = &destination {
(self.destination_in_tx_outputs(&tx_with_id, dest)
|| self.destination_in_tx_inputs(&tx_with_id, dest))
.then_some(TxInfo::new(tx_with_id.get_id(), *block_height, *timestamp))
} else {
Some(TxInfo::new(tx_with_id.get_id(), *block_height, *timestamp))
}
}
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
12 changes: 12 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::account::transaction_list::TransactionList;
use crate::account::TxInfo;
use crate::account::{
currency_grouper::Currency, CurrentFeeRate, DelegationData, PartiallySignedTransaction,
PoolData, TransactionToSign, UnconfirmedTokenInfo, UtxoSelectorError,
Expand Down Expand Up @@ -875,6 +876,17 @@ impl<B: storage::Backend> Wallet<B> {
Ok(transactions)
}

pub fn mainchain_transactions(
&self,
account_index: U31,
destination: Option<Destination>,
limit: usize,
) -> WalletResult<Vec<TxInfo>> {
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_mainchain_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(|info| info.id == send_tx_id));
assert!(txs.iter().any(|info| info.id == spend_from_tx_id));
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
Expand Down
Loading

0 comments on commit fdd9b51

Please sign in to comment.