Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions fedimint-client-module/src/module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,19 @@ pub trait ClientModule: Debug + MaybeSend + MaybeSync + 'static {
input: &<Self::Common as ModuleCommon>::Input,
) -> Option<Amounts>;

/// Returns the amount of an input previously generated by the module.
///
/// None is only returned if the client can't determine the amount of an
/// input, which normally should only be the case if a future version of
/// Fedimint introduces a new input variant, which isn't a problem since
/// clients only deal with transactions they generated. When the feature was
/// introduced old LNv2 transactions would also return `None` though.
///
/// # Panics
/// May panic if the input was not generated by the client itself since some
/// modules may rely on cached metadata.
async fn input_amount(&self, input: &<Self::Common as ModuleCommon>::Input) -> Option<Amounts>;

/// Returns the fee the processing of this output requires.
///
/// If the semantics of a given output aren't known this function returns
Expand All @@ -825,6 +838,22 @@ pub trait ClientModule: Debug + MaybeSend + MaybeSync + 'static {
output: &<Self::Common as ModuleCommon>::Output,
) -> Option<Amounts>;

/// Returns the amount of the output.
///
/// None is only returned if a future version of Fedimint introduces a
/// new output variant. For clients this should only be the case when
/// processing transactions created by other users, so the result of
/// this function can be `unwrap`ped whenever dealing with inputs
/// generated by ourselves.
///
/// # Panics
/// May panic if the output was not generated by the client itself since
/// some modules may rely on cached metadata.
async fn output_amount(
&self,
output: &<Self::Common as ModuleCommon>::Output,
) -> Option<Amounts>;

fn supports_backup(&self) -> bool {
false
}
Expand Down Expand Up @@ -988,8 +1017,12 @@ pub trait IClientModule: Debug {

fn input_fee(&self, amount: &Amounts, input: &DynInput) -> Option<Amounts>;

async fn input_amount(&self, input: &DynInput) -> Option<Amounts>;

fn output_fee(&self, amount: &Amounts, output: &DynOutput) -> Option<Amounts>;

async fn output_amount(&self, output: &DynOutput) -> Option<Amounts>;

fn supports_backup(&self) -> bool;

async fn backup(&self, module_instance_id: ModuleInstanceId)
Expand Down Expand Up @@ -1070,6 +1103,17 @@ where
)
}

async fn input_amount(&self, input: &DynInput) -> Option<Amounts> {
<T as ClientModule>::input_amount(
self,
input
.as_any()
.downcast_ref()
.expect("Dispatched to correct module"),
)
.await
}

fn output_fee(&self, amount: &Amounts, output: &DynOutput) -> Option<Amounts> {
<T as ClientModule>::output_fee(
self,
Expand All @@ -1081,6 +1125,17 @@ where
)
}

async fn output_amount(&self, output: &DynOutput) -> Option<Amounts> {
<T as ClientModule>::output_amount(
self,
output
.as_any()
.downcast_ref()
.expect("Dispatched to correct module"),
)
.await
}

fn supports_backup(&self) -> bool {
<T as ClientModule>::supports_backup(self)
}
Expand Down
130 changes: 130 additions & 0 deletions fedimint-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,126 @@ impl Client {
active_state_exists || inactive_state_exists
}

/// Calculates the federation fees paid in the course of the operation.
///
/// Federation fees are fees paid to the federation (e.g. for ecash) and do
/// not include fees paid to service providers like the Lightning gateway.
/// These still have to be separately reported by the module.
///
/// Fees are calculated by subtracting the total output amount from the
/// total input amount, any difference is either due to direct fees or
/// overpayment to avoid generating uneconomic outputs.
///
/// Returns `None` if any input/output amount cannot be determined (e.g.,
/// for old LNv2 payments where the contract metadata wasn't stored).
///
/// # Panics
/// Panics if the operation does not exist.
pub async fn get_operation_fees(&self, operation_id: OperationId) -> Option<OperationFees> {
assert!(
self.operation_exists(operation_id).await,
"Operation does not exist"
);

let (active_states, inactive_states) =
self.executor().get_operation_states(operation_id).await;
let is_final = active_states.is_empty();

let states = active_states
.into_iter()
.map(|(state, _)| state)
.chain(inactive_states.into_iter().map(|(state, _)| state));

let transaction_states = states
.filter_map(|state| {
if state.module_instance_id() != TRANSACTION_SUBMISSION_MODULE_INSTANCE {
return None;
}

let tx_state = state
.as_any()
.downcast_ref::<TxSubmissionStatesSM>()
.cloned()
.expect("Invalid dyn state");

Some(tx_state)
})
.collect::<Vec<_>>();

let accepted_transactions = transaction_states
.iter()
.filter_map(|tx_state| match &tx_state.state {
TxSubmissionStates::Accepted(transaction_id) => Some(*transaction_id),
_ => None,
})
.collect::<HashSet<_>>();

let operation_transactions = transaction_states
.into_iter()
.filter_map(|tx_state| match tx_state.state {
TxSubmissionStates::Created(transaction)
if accepted_transactions.contains(&transaction.tx_hash()) =>
{
Some(transaction)
}
_ => None,
})
.collect::<Vec<_>>();

let mut total_fees = Amounts::ZERO;
for transaction in &operation_transactions {
let mut inputs_amount = Amounts::ZERO;
for input in &transaction.inputs {
let module = self.get_module(input.module_instance_id());
let Some(amount) = module.input_amount(input).await else {
warn!(
target: LOG_CLIENT,
module_instance_id = input.module_instance_id(),
module_kind = ?input.module_kind(),
tx_id = %transaction.tx_hash(),
"Could not determine input amount, fee calculation unavailable"
);
return None;
};

inputs_amount = inputs_amount
.checked_add(&amount)
.expect("Our own transactions never overflow");
}

let mut outputs_amount = Amounts::ZERO;
for output in &transaction.outputs {
let module = self.get_module(output.module_instance_id());
let Some(amount) = module.output_amount(output).await else {
warn!(
target: LOG_CLIENT,
module_instance_id = output.module_instance_id(),
module_kind = ?output.module_kind(),
tx_id = %transaction.tx_hash(),
"Could not determine output amount, fee calculation unavailable"
);
return None;
};

outputs_amount = outputs_amount
.checked_add(&amount)
.expect("Our own transactions never overflow");
}

let transaction_fees = inputs_amount
.checked_sub(&outputs_amount)
.expect("Own transaction doesn't overflow");
total_fees = total_fees
.checked_add(&transaction_fees)
.expect("Won' overflow for any real-world case")
}

Some(OperationFees {
amount: total_fees,
is_final,
})
}

pub async fn has_active_states(&self, operation_id: OperationId) -> bool {
self.db
.begin_transaction_nc()
Expand Down Expand Up @@ -2194,3 +2314,13 @@ pub fn client_decoders<'a>(
}
ModuleDecoderRegistry::from(modules)
}

/// Fees paid for transactions within an operation.
#[derive(Debug, Clone)]
pub struct OperationFees {
/// The amount of fees paid in transactions belonging to this operation.
pub amount: Amounts,
/// True if the fee amount will not change anymore. False for ongoing
/// operations.
pub is_final: bool,
}
12 changes: 12 additions & 0 deletions fedimint-core/src/module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ impl Amounts {
Some(self)
}

pub fn checked_sub(&self, other: &Self) -> Option<Self> {
let mut result = self.clone();

for (unit, amount) in &other.0 {
let prev = result.0.entry(*unit).or_default();

*prev = prev.checked_sub(*amount)?;
}

Some(result)
}

pub fn remove(&mut self, unit: &AmountUnit) -> Option<Amount> {
self.0.remove(unit)
}
Expand Down
13 changes: 13 additions & 0 deletions modules/fedimint-dummy-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ impl ClientModule for DummyClientModule {
Some(Amounts::new_bitcoin(self.cfg.tx_fee))
}

async fn input_amount(&self, input: &<Self::Common as ModuleCommon>::Input) -> Option<Amounts> {
let amount_btc = input.maybe_v0_ref()?.amount;
Some(Amounts::new_bitcoin(amount_btc))
}

fn output_fee(
&self,
_amount: &Amounts,
Expand All @@ -104,6 +109,14 @@ impl ClientModule for DummyClientModule {
Some(Amounts::new_bitcoin(self.cfg.tx_fee))
}

async fn output_amount(
&self,
output: &<Self::Common as ModuleCommon>::Output,
) -> Option<Amounts> {
let amount_btc = output.maybe_v0_ref()?.amount;
Some(Amounts::new_bitcoin(amount_btc))
}

fn supports_being_primary(&self) -> PrimaryModuleSupport {
PrimaryModuleSupport::Any {
priority: PrimaryModulePriority::LOW,
Expand Down
14 changes: 14 additions & 0 deletions modules/fedimint-empty-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ impl ClientModule for EmptyClientModule {
unreachable!()
}

async fn input_amount(
&self,
_input: &<Self::Common as ModuleCommon>::Input,
) -> Option<Amounts> {
unreachable!()
}

fn output_fee(
&self,
_amount: &Amounts,
Expand All @@ -76,6 +83,13 @@ impl ClientModule for EmptyClientModule {
unreachable!()
}

async fn output_amount(
&self,
_output: &<Self::Common as ModuleCommon>::Output,
) -> Option<Amounts> {
unreachable!()
}

async fn get_balance(&self, _dbtx: &mut DatabaseTransaction<'_>, _unit: AmountUnit) -> Amount {
Amount::ZERO
}
Expand Down
18 changes: 17 additions & 1 deletion modules/fedimint-gw-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use fedimint_core::config::FederationId;
use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
use fedimint_core::db::{AutocommitError, DatabaseTransaction};
use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_core::module::{Amounts, ApiVersion, ModuleInit, MultiApiVersion};
use fedimint_core::module::{Amounts, ApiVersion, ModuleCommon, ModuleInit, MultiApiVersion};
use fedimint_core::util::{FmtCompact, SafeUrl, Spanned};
use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, secp256k1};
use fedimint_derive_secret::ChildId;
Expand Down Expand Up @@ -209,6 +209,7 @@ pub struct GatewayClientModule {
connector_registry: ConnectorRegistry,
}

#[async_trait::async_trait]
impl ClientModule for GatewayClientModule {
type Init = LightningClientInit;
type Common = LightningModuleTypes;
Expand Down Expand Up @@ -236,6 +237,10 @@ impl ClientModule for GatewayClientModule {
Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
}

async fn input_amount(&self, input: &<Self::Common as ModuleCommon>::Input) -> Option<Amounts> {
Some(Amounts::new_bitcoin(input.maybe_v0_ref()?.amount))
}

fn output_fee(
&self,
_amount: &Amounts,
Expand All @@ -250,6 +255,17 @@ impl ClientModule for GatewayClientModule {
}
}
}

async fn output_amount(
&self,
output: &<Self::Common as ModuleCommon>::Output,
) -> Option<Amounts> {
let amount_btc = match output.maybe_v0_ref()? {
LightningOutputV0::Contract(contract_output) => contract_output.amount,
LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => Amount::ZERO,
};
Some(Amounts::new_bitcoin(amount_btc))
}
}

impl GatewayClientModule {
Expand Down
1 change: 1 addition & 0 deletions modules/fedimint-gwv2-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ futures = { workspace = true }
lightning-invoice = { workspace = true }
serde = { workspace = true }
serde_millis = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tpe = { workspace = true }
tracing = { workspace = true, features = ["log"] }
Expand Down
29 changes: 29 additions & 0 deletions modules/fedimint-gwv2-client/src/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_core::{OutPoint, impl_db_record};
use fedimint_lnv2_common::contracts::LightningContract;
use strum::EnumIter;

#[repr(u8)]
#[derive(Clone, EnumIter, Debug)]
pub enum DbKeyPrefix {
OutpointContract = 0x43,
#[allow(dead_code)]
/// Prefixes between 0xb0..=0xcf shall all be considered allocated for
/// historical and future external use
ExternalReservedStart = 0xb0,
#[allow(dead_code)]
/// Prefixes between 0xd0..=0xff shall all be considered allocated for
/// historical and future internal use
CoreInternalReservedStart = 0xd0,
#[allow(dead_code)]
CoreInternalReservedEnd = 0xff,
}

#[derive(Debug, Encodable, Decodable)]
pub struct OutpointContractKey(pub OutPoint);

impl_db_record!(
key = OutpointContractKey,
value = LightningContract,
db_prefix = DbKeyPrefix::OutpointContract,
);
Loading