Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add list transactions command to the wallet #1527

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading