diff --git a/common/src/chain/transaction/input.rs b/common/src/chain/transaction/input.rs index 1fb1c4d847..2635ee03a6 100644 --- a/common/src/chain/transaction/input.rs +++ b/common/src/chain/transaction/input.rs @@ -15,7 +15,7 @@ use serialization::{Decode, Encode}; -use crate::chain::AccountNonce; +use crate::{chain::AccountNonce, text_summary::TextSummary}; use super::{AccountCommand, AccountOutPoint, AccountSpending, OutPointSourceId, UtxoOutPoint}; @@ -56,3 +56,32 @@ impl From for TxInput { TxInput::Utxo(outpoint) } } + +impl TextSummary for TxInput { + fn text_summary(&self, _chain_config: &crate::chain::ChainConfig) -> String { + match self { + TxInput::Utxo(utxo) => { + let source_id = utxo.source_id(); + let n = utxo.output_index(); + match source_id { + OutPointSourceId::Transaction(ref id) => { + let id_str = format!("{:?}", id.to_hash()) + .strip_prefix("0x") + .expect("0x exists for some hellish reason") + .to_string(); + format!("Transaction({id_str}, {n})") + } + OutPointSourceId::BlockReward(id) => { + let id_str = format!("{:?}", id.to_hash()) + .strip_prefix("0x") + .expect("0x exists for some hellish reason") + .to_string(); + format!("BlockReward({id_str}, {n})") + } + } + } + TxInput::Account(acc_out) => format!("{acc_out:?}"), + TxInput::AccountCommand(nonce, cmd) => format!("AccountCommand({nonce}, {cmd:?})"), + } + } +} diff --git a/common/src/chain/transaction/mod.rs b/common/src/chain/transaction/mod.rs index 59eb7a808c..600b96262f 100644 --- a/common/src/chain/transaction/mod.rs +++ b/common/src/chain/transaction/mod.rs @@ -19,6 +19,7 @@ use serialization::{DirectDecode, DirectEncode}; use typename::TypeName; use crate::primitives::{id::WithId, Id, Idable, H256}; +use crate::text_summary::TextSummary; pub mod input; pub use input::*; @@ -143,8 +144,10 @@ impl Transaction { } SignedTransaction::new(self, witnesses) } +} - pub fn printable(&self, chain_config: &ChainConfig) -> String { +impl TextSummary for Transaction { + fn text_summary(&self, chain_config: &ChainConfig) -> String { printout::transaction_summary(self, chain_config) } } diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index 090402f6ac..86adb3f720 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -15,15 +15,18 @@ use crate::{ address::{ - hexified::HexifiedAddress, pubkeyhash::PublicKeyHash, traits::Addressable, AddressError, + hexified::HexifiedAddress, pubkeyhash::PublicKeyHash, traits::Addressable, Address, + AddressError, }, chain::{ output_value::OutputValue, - tokens::{NftIssuance, TokenId, TokenIssuance}, + tokens::{IsTokenFreezable, NftIssuance, TokenId, TokenIssuance, TokenTotalSupply}, ChainConfig, DelegationId, PoolId, }, primitives::{Amount, Id}, + text_summary::TextSummary, }; +use crypto::vrf::VRFPublicKey; use script::Script; use serialization::{Decode, DecodeAll, Encode}; use variant_count::VariantCount; @@ -128,3 +131,161 @@ impl TxOutput { } } } + +impl TextSummary for TxOutput { + fn text_summary(&self, chain_config: &ChainConfig) -> String { + let fmt_ml = |v: &Amount| v.into_fixedpoint_str(chain_config.coin_decimals()); + let fmt_val = |val: &OutputValue| { + match val { + OutputValue::Coin(amount) => fmt_ml(amount), + OutputValue::TokenV0(token_data) => format!("{token_data:?}"), // Not important since it's deprecated + OutputValue::TokenV1(id, amount) => { + format!( + "TokenV1({}, {amount:?})", + Address::new(chain_config, id) + .expect("Cannot fail due to TokenId being fixed size") + ) + } + } + }; + let fmt_timelock = |tl: &OutputTimeLock| match tl { + OutputTimeLock::UntilHeight(h) => format!("OutputTimeLock::UntilHeight({h})"), + OutputTimeLock::UntilTime(t) => format!("OutputTimeLock::UntilTime({})", t.into_time()), + OutputTimeLock::ForBlockCount(n) => { + format!("OutputTimeLock::ForBlockCount({n} blocks)") + } + OutputTimeLock::ForSeconds(secs) => { + format!("OutputTimeLock::ForSeconds({secs} seconds)") + } + }; + let fmt_dest = + |d: &Destination| format!("{}", Address::new(chain_config, d).expect("addressable")); + let fmt_vrf = + |k: &VRFPublicKey| format!("{}", Address::new(chain_config, k).expect("addressable")); + let fmt_poolid = |id: &PoolId| { + Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") + }; + let fmt_tknid = |id: &TokenId| { + Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") + }; + let fmt_delid = |id: &DelegationId| { + Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") + }; + let fmt_stakepooldata = |p: &StakePoolData| { + let pledge = fmt_ml(&p.pledge()); + format!( + "Pledge({pledge}), Staker({}), VRFPubKey({}), DecommissionKey({}), MarginRatio({}), CostPerBlock({})", + fmt_dest(p.staker()), + fmt_vrf(p.vrf_public_key()), + fmt_dest(p.decommission_key()), + p.margin_ratio_per_thousand().into_percentage_str(), + fmt_ml(&p.cost_per_block()) + ) + }; + let fmt_tkn_supply = |s: &TokenTotalSupply, d: u8| match s { + TokenTotalSupply::Fixed(v) => format!("Fixed({})", v.into_fixedpoint_str(d)), + TokenTotalSupply::Lockable => "Lockable".to_string(), + TokenTotalSupply::Unlimited => "Unlimited".to_string(), + }; + let fmt_tkn_frzble = |f: &IsTokenFreezable| match f { + IsTokenFreezable::No => "Yes".to_string(), + IsTokenFreezable::Yes => "No".to_string(), + }; + let fmt_tkn_iss = |iss: &TokenIssuance| { + match iss { + TokenIssuance::V1(iss1) => format!( + "TokenIssuance(Ticker({}), Decimals({}), MetadataUri({}), TotalSupply({}), Authority({}), IsFreezable({}))", + String::from_utf8_lossy(&iss1.token_ticker), + iss1.number_of_decimals, + String::from_utf8_lossy(&iss1.metadata_uri), + fmt_tkn_supply(&iss1.total_supply, iss1.number_of_decimals), + fmt_dest(&iss1.authority), + fmt_tkn_frzble(&iss1.is_freezable) + ), + } + }; + let fmt_nft_iss = |iss: &NftIssuance| match iss { + NftIssuance::V0(iss1) => { + let md = &iss1.metadata; + let creator = match &md.creator { + Some(c) => hex::encode(c.public_key.encode()).to_string(), + None => "Unspecified".to_string(), + }; + format!( + "Create({}), Name({}), Description({}), Ticker({}), IconUri({}), AdditionalMetaData({}), MediaUri({}), MediaHash(0x{})", + creator, + String::from_utf8_lossy(&md.name), + String::from_utf8_lossy(&md.description), + String::from_utf8_lossy(&md.ticker), + String::from_utf8_lossy(md.icon_uri.as_ref().as_ref().unwrap_or(&vec![])), + String::from_utf8_lossy( + md.additional_metadata_uri.as_ref().as_ref().unwrap_or(&vec![]) + ), + String::from_utf8_lossy( + md.media_uri.as_ref().as_ref().unwrap_or(&vec![]) + ), + hex::encode(&md.media_hash), + + ) + } + }; + + match self { + TxOutput::Transfer(val, dest) => { + let val_str = fmt_val(val); + format!("Transfer({}, {val_str})", fmt_dest(dest)) + } + TxOutput::LockThenTransfer(val, dest, timelock) => { + let val_str = fmt_val(val); + format!( + "LockThenTransfer({}, {val_str}, {})", + fmt_dest(dest), + fmt_timelock(timelock) + ) + } + TxOutput::Burn(val) => format!("Burn({})", fmt_val(val)), + TxOutput::CreateStakePool(id, data) => { + format!( + "CreateStakePool(Id({}), {})", + fmt_poolid(id), + fmt_stakepooldata(data) + ) + } + TxOutput::ProduceBlockFromStake(dest, pool_id) => { + format!( + "ProduceBlockFromStake({}, {})", + fmt_dest(dest), + fmt_poolid(pool_id) + ) + } + TxOutput::CreateDelegationId(owner, pool_id) => { + format!( + "CreateDelegationId(Owner({}), StakingPool({}))", + fmt_dest(owner), + fmt_poolid(pool_id) + ) + } + TxOutput::DelegateStaking(amount, del_ig) => { + format!( + "DelegateStaking(Owner({}), StakingPool({}))", + fmt_ml(amount), + fmt_delid(del_ig) + ) + } + TxOutput::IssueFungibleToken(issuance) => { + format!("IssueFungibleToken({})", fmt_tkn_iss(issuance)) + } + TxOutput::IssueNft(token_id, iss, receiver) => { + format!( + "IssueNft(Id({}), NftIssuance({}), Receiver({}))", + fmt_tknid(token_id), + fmt_nft_iss(iss), + fmt_dest(receiver) + ) + } + TxOutput::DataDeposit(data) => { + format!("DataDeposit(0x{})", hex::encode(data)) + } + } + } +} diff --git a/common/src/chain/transaction/printout.rs b/common/src/chain/transaction/printout.rs index d2b151b69e..d90971428d 100644 --- a/common/src/chain/transaction/printout.rs +++ b/common/src/chain/transaction/printout.rs @@ -13,22 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crypto::vrf::VRFPublicKey; -use serialization::Encode; - use crate::{ - address::Address, - chain::{ - tokens::{IsTokenFreezable, NftIssuance, TokenId, TokenIssuance, TokenTotalSupply}, - ChainConfig, DelegationId, PoolId, - }, - primitives::{Amount, Idable, H256}, + chain::ChainConfig, + primitives::{Idable, H256}, + text_summary::TextSummary, }; -use super::{ - output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, Destination, - Transaction, TxOutput, -}; +use super::Transaction; use std::fmt::Write; fn id_to_hex_string(id: H256) -> String { @@ -45,102 +36,9 @@ pub fn transaction_summary(tx: &Transaction, chain_config: &ChainConfig) -> Stri id_to_hex_string(tx.get_id().to_hash()) ); - let fmt_ml = |v: &Amount| v.into_fixedpoint_str(chain_config.coin_decimals()); - let fmt_val = |val: &OutputValue| { - match val { - OutputValue::Coin(amount) => fmt_ml(amount), - OutputValue::TokenV0(token_data) => format!("{token_data:?}"), // Not important since it's deprecated - OutputValue::TokenV1(id, amount) => { - format!( - "TokenV1({}, {amount:?})", - Address::new(chain_config, id) - .expect("Cannot fail due to TokenId being fixed size") - ) - } - } - }; - let fmt_timelock = |tl: &OutputTimeLock| match tl { - OutputTimeLock::UntilHeight(h) => format!("OutputTimeLock::UntilHeight({h})"), - OutputTimeLock::UntilTime(t) => format!("OutputTimeLock::UntilTime({})", t.into_time()), - OutputTimeLock::ForBlockCount(n) => format!("OutputTimeLock::ForBlockCount({n} blocks)"), - OutputTimeLock::ForSeconds(secs) => { - format!("OutputTimeLock::ForSeconds({secs} seconds)") - } - }; - let fmt_dest = - |d: &Destination| format!("{}", Address::new(chain_config, d).expect("addressable")); - let fmt_vrf = - |k: &VRFPublicKey| format!("{}", Address::new(chain_config, k).expect("addressable")); - let fmt_poolid = |id: &PoolId| { - Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") - }; - let fmt_tknid = |id: &TokenId| { - Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") - }; - let fmt_delid = |id: &DelegationId| { - Address::new(chain_config, id).expect("Cannot fail because fixed size addressable") - }; - let fmt_stakepooldata = |p: &StakePoolData| { - let pledge = fmt_ml(&p.pledge()); - format!( - "Pledge({pledge}), Staker({}), VRFPubKey({}), DecommissionKey({}), MarginRatio({}), CostPerBlock({})", - fmt_dest(p.staker()), - fmt_vrf(p.vrf_public_key()), - fmt_dest(p.decommission_key()), - p.margin_ratio_per_thousand().into_percentage_str(), - fmt_ml(&p.cost_per_block()) - ) - }; - let fmt_tkn_supply = |s: &TokenTotalSupply, d: u8| match s { - TokenTotalSupply::Fixed(v) => format!("Fixed({})", v.into_fixedpoint_str(d)), - TokenTotalSupply::Lockable => "Lockable".to_string(), - TokenTotalSupply::Unlimited => "Unlimited".to_string(), - }; - let fmt_tkn_frzble = |f: &IsTokenFreezable| match f { - IsTokenFreezable::No => "Yes".to_string(), - IsTokenFreezable::Yes => "No".to_string(), - }; - let fmt_tkn_iss = |iss: &TokenIssuance| { - match iss { - TokenIssuance::V1(iss1) => format!( - "TokenIssuance(Ticker({}), Decimals({}), MetadataUri({}), TotalSupply({}), Authority({}), IsFreezable({}))", - String::from_utf8_lossy(&iss1.token_ticker), - iss1.number_of_decimals, - String::from_utf8_lossy(&iss1.metadata_uri), - fmt_tkn_supply(&iss1.total_supply, iss1.number_of_decimals), - fmt_dest(&iss1.authority), - fmt_tkn_frzble(&iss1.is_freezable) - ), - } - }; - let fmt_nft_iss = |iss: &NftIssuance| match iss { - NftIssuance::V0(iss1) => { - let md = &iss1.metadata; - let creator = match &md.creator { - Some(c) => hex::encode(c.public_key.encode()).to_string(), - None => "Unspecified".to_string(), - }; - format!( - "Create({}), Name({}), Description({}), Ticker({}), IconUri({}), AdditionalMetaData({}), MediaUri({}), MediaHash(0x{})", - creator, - String::from_utf8_lossy(&md.name), - String::from_utf8_lossy(&md.description), - String::from_utf8_lossy(&md.ticker), - String::from_utf8_lossy(md.icon_uri.as_ref().as_ref().unwrap_or(&vec![])), - String::from_utf8_lossy( - md.additional_metadata_uri.as_ref().as_ref().unwrap_or(&vec![]) - ), - String::from_utf8_lossy( - md.media_uri.as_ref().as_ref().unwrap_or(&vec![]) - ), - hex::encode(&md.media_hash), - - ) - } - }; - for input in tx.inputs() { - writeln!(&mut result, "- {input:?}").expect("Writing to a memory buffer should not fail"); + let s = input.text_summary(chain_config); + writeln!(&mut result, "- {s}").expect("Writing to a memory buffer should not fail"); } writeln!( @@ -150,63 +48,7 @@ pub fn transaction_summary(tx: &Transaction, chain_config: &ChainConfig) -> Stri .expect("Writing to a memory buffer should not fail"); for output in tx.outputs() { - let s = match output { - TxOutput::Transfer(val, dest) => { - let val_str = fmt_val(val); - format!("Transfer({}, {val_str})", fmt_dest(dest)) - } - TxOutput::LockThenTransfer(val, dest, timelock) => { - let val_str = fmt_val(val); - format!( - "LockThenTransfer({}, {val_str}, {})", - fmt_dest(dest), - fmt_timelock(timelock) - ) - } - TxOutput::Burn(val) => format!("Burn({})", fmt_val(val)), - TxOutput::CreateStakePool(id, data) => { - format!( - "CreateStakePool(Id({}), {})", - fmt_poolid(id), - fmt_stakepooldata(data) - ) - } - TxOutput::ProduceBlockFromStake(dest, pool_id) => { - format!( - "ProduceBlockFromStake({}, {})", - fmt_dest(dest), - fmt_poolid(pool_id) - ) - } - TxOutput::CreateDelegationId(owner, pool_id) => { - format!( - "CreateDelegationId(Owner({}), StakingPool({}))", - fmt_dest(owner), - fmt_poolid(pool_id) - ) - } - TxOutput::DelegateStaking(amount, del_ig) => { - format!( - "DelegateStaking(Owner({}), StakingPool({}))", - fmt_ml(amount), - fmt_delid(del_ig) - ) - } - TxOutput::IssueFungibleToken(issuance) => { - format!("IssueFungibleToken({})", fmt_tkn_iss(issuance)) - } - TxOutput::IssueNft(token_id, iss, receiver) => { - format!( - "IssueNft(Id({}), NftIssuance({}), Receiver({}))", - fmt_tknid(token_id), - fmt_nft_iss(iss), - fmt_dest(receiver) - ) - } - TxOutput::DataDeposit(data) => { - format!("DataDeposit(0x{})", hex::encode(data)) - } - }; + let s = output.text_summary(chain_config); writeln!(&mut result, "- {s}").expect("Writing to a memory buffer should not fail"); } writeln!(&mut result, "=== END OF OUTPUTS ===") @@ -231,8 +73,8 @@ mod tests { IsTokenFreezable, Metadata, NftIssuance, NftIssuanceV0, TokenCreator, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, - DelegationId, Destination, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, - UtxoOutPoint, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, + Destination, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, Id, H256}, time_getter::TimeGetter, @@ -368,6 +210,21 @@ mod tests { OutPointSourceId::Transaction(Id::new(H256::random())), 1, )), + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random())), + 1, + )), + TxInput::Account(AccountOutPoint::new( + AccountNonce::new(15), + AccountSpending::DelegationBalance( + Id::new(H256::random()), + Amount::from_atoms(100000), + ), + )), + TxInput::AccountCommand( + AccountNonce::new(25), + AccountCommand::MintTokens(Id::new(H256::random()), Amount::from_atoms(100000)), + ), ] .to_vec(), outputs.to_vec(), diff --git a/common/src/lib.rs b/common/src/lib.rs index 76590e656c..62bdcfa114 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -17,6 +17,7 @@ pub mod address; pub mod chain; pub mod primitives; pub mod size_estimation; +pub mod text_summary; pub mod time_getter; pub mod uint; diff --git a/common/src/text_summary.rs b/common/src/text_summary.rs new file mode 100644 index 0000000000..4bc4fe2f31 --- /dev/null +++ b/common/src/text_summary.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// 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. + +use crate::chain::ChainConfig; + +pub trait TextSummary { + fn text_summary(&self, chain_config: &ChainConfig) -> String; +} diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 578edfb3fa..b06e0972a2 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -32,6 +32,7 @@ use common::{ Block, ChainConfig, Destination, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id, H256}, + text_summary::TextSummary, }; use crypto::key::{hdkd::u31::U31, PublicKey}; use mempool::tx_options::TxOptionsOverrides; @@ -1050,7 +1051,7 @@ where let output_str = match result.into_signed_tx() { Ok(signed_tx) => { let summary = - signed_tx.transaction().printable(self.wallet_rpc.chain_config()); + signed_tx.transaction().text_summary(self.wallet_rpc.chain_config()); let result_hex: HexEncoded = signed_tx.into(); let qr_code_string = utils::qrcode::qrcode_from_str(result_hex.to_string())