| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- //! FFI Wallet bindings
- use std::str::FromStr;
- use std::sync::Arc;
- use bip39::Mnemonic;
- use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
- use crate::error::FfiError;
- use crate::token::Token;
- use crate::types::payment_request::PaymentRequest;
- use crate::types::*;
- /// FFI-compatible Wallet
- #[derive(uniffi::Object)]
- pub struct Wallet {
- inner: Arc<CdkWallet>,
- }
- impl Wallet {
- /// Create a Wallet from an existing CDK wallet (internal use only)
- pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
- Self { inner }
- }
- }
- #[uniffi::export(async_runtime = "tokio")]
- impl Wallet {
- /// Create a new Wallet from mnemonic using WalletDatabaseFfi trait
- #[uniffi::constructor]
- pub fn new(
- mint_url: String,
- unit: CurrencyUnit,
- mnemonic: String,
- db: Arc<dyn crate::database::WalletDatabase>,
- config: WalletConfig,
- ) -> Result<Self, FfiError> {
- // Parse mnemonic and generate seed without passphrase
- let m = Mnemonic::parse(&mnemonic)
- .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
- let seed = m.to_seed_normalized("");
- tracing::info!("creating ffi wallet");
- // Convert the FFI database trait to a CDK database implementation
- let localstore = crate::database::create_cdk_database_from_ffi(db);
- let wallet = CdkWalletBuilder::new()
- .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
- FfiError::internal(format!("Invalid URL: {}", e))
- })?)
- .unit(unit.into())
- .localstore(localstore)
- .seed(seed)
- .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
- .build()
- .map_err(FfiError::from)?;
- Ok(Self {
- inner: Arc::new(wallet),
- })
- }
- /// Get the mint URL
- pub fn mint_url(&self) -> MintUrl {
- self.inner.mint_url.clone().into()
- }
- /// Get the currency unit
- pub fn unit(&self) -> CurrencyUnit {
- self.inner.unit.clone().into()
- }
- /// Set metadata cache TTL (time-to-live) in seconds
- ///
- /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
- /// before requiring a refresh from the mint server.
- ///
- /// # Arguments
- ///
- /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used.
- ///
- /// # Example
- ///
- /// ```ignore
- /// // Cache expires after 5 minutes
- /// wallet.set_metadata_cache_ttl(Some(300));
- ///
- /// // Cache never expires (default)
- /// wallet.set_metadata_cache_ttl(None);
- /// ```
- pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
- let ttl = ttl_secs.map(std::time::Duration::from_secs);
- self.inner.set_metadata_cache_ttl(ttl);
- }
- /// Get total balance
- pub async fn total_balance(&self) -> Result<Amount, FfiError> {
- let balance = self.inner.total_balance().await?;
- Ok(balance.into())
- }
- /// Get total pending balance
- pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
- let balance = self.inner.total_pending_balance().await?;
- Ok(balance.into())
- }
- /// Get total reserved balance
- pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
- let balance = self.inner.total_reserved_balance().await?;
- Ok(balance.into())
- }
- /// Get mint info from mint
- pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
- let info = self.inner.fetch_mint_info().await?;
- Ok(info.map(Into::into))
- }
- /// Load mint info
- ///
- /// This will get mint info from cache if it is fresh
- pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
- let info = self.inner.load_mint_info().await?;
- Ok(info.into())
- }
- /// Receive tokens
- pub async fn receive(
- &self,
- token: std::sync::Arc<Token>,
- options: ReceiveOptions,
- ) -> Result<Amount, FfiError> {
- let amount = self
- .inner
- .receive(&token.to_string(), options.into())
- .await?;
- Ok(amount.into())
- }
- /// Restore wallet from seed
- pub async fn restore(&self) -> Result<Restored, FfiError> {
- let restored = self.inner.restore().await?;
- Ok(restored.into())
- }
- /// Verify token DLEQ proofs
- pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
- let cdk_token = token.inner.clone();
- self.inner.verify_token_dleq(&cdk_token).await?;
- Ok(())
- }
- /// Receive proofs directly
- pub async fn receive_proofs(
- &self,
- proofs: Proofs,
- options: ReceiveOptions,
- memo: Option<String>,
- ) -> Result<Amount, FfiError> {
- let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
- proofs.into_iter().map(|p| p.try_into()).collect();
- let cdk_proofs = cdk_proofs?;
- let amount = self
- .inner
- .receive_proofs(cdk_proofs, options.into(), memo)
- .await?;
- Ok(amount.into())
- }
- /// Prepare a send operation
- pub async fn prepare_send(
- &self,
- amount: Amount,
- options: SendOptions,
- ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
- let prepared = self
- .inner
- .prepare_send(amount.into(), options.into())
- .await?;
- Ok(std::sync::Arc::new(prepared.into()))
- }
- /// Get a mint quote
- pub async fn mint_quote(
- &self,
- amount: Amount,
- description: Option<String>,
- ) -> Result<MintQuote, FfiError> {
- let quote = self.inner.mint_quote(amount.into(), description).await?;
- Ok(quote.into())
- }
- /// Mint tokens
- pub async fn mint(
- &self,
- quote_id: String,
- amount_split_target: SplitTarget,
- spending_conditions: Option<SpendingConditions>,
- ) -> Result<Proofs, FfiError> {
- // Convert spending conditions if provided
- let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
- let proofs = self
- .inner
- .mint("e_id, amount_split_target.into(), conditions)
- .await?;
- Ok(proofs.into_iter().map(|p| p.into()).collect())
- }
- /// Get a melt quote
- pub async fn melt_quote(
- &self,
- request: String,
- options: Option<MeltOptions>,
- ) -> Result<MeltQuote, FfiError> {
- let cdk_options = options.map(Into::into);
- let quote = self.inner.melt_quote(request, cdk_options).await?;
- Ok(quote.into())
- }
- /// Melt tokens
- pub async fn melt(&self, quote_id: String) -> Result<Melted, FfiError> {
- let melted = self.inner.melt("e_id).await?;
- Ok(melted.into())
- }
- /// Melt specific proofs
- ///
- /// This method allows melting proofs that may not be in the wallet's database,
- /// similar to how `receive_proofs` handles external proofs. The proofs will be
- /// added to the database and used for the melt operation.
- ///
- /// # Arguments
- ///
- /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
- /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
- ///
- /// # Returns
- ///
- /// A `Melted` result containing the payment details and any change proofs
- pub async fn melt_proofs(&self, quote_id: String, proofs: Proofs) -> Result<Melted, FfiError> {
- let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
- proofs.into_iter().map(|p| p.try_into()).collect();
- let cdk_proofs = cdk_proofs?;
- let melted = self.inner.melt_proofs("e_id, cdk_proofs).await?;
- Ok(melted.into())
- }
- /// Get a quote for a bolt12 mint
- pub async fn mint_bolt12_quote(
- &self,
- amount: Option<Amount>,
- description: Option<String>,
- ) -> Result<MintQuote, FfiError> {
- let quote = self
- .inner
- .mint_bolt12_quote(amount.map(Into::into), description)
- .await?;
- Ok(quote.into())
- }
- /// Get a mint quote using a unified interface for any payment method
- ///
- /// This method supports bolt11, bolt12, and custom payment methods.
- /// For custom methods, you can pass extra JSON data that will be forwarded
- /// to the payment processor.
- ///
- /// # Arguments
- /// * `amount` - Optional amount to mint (required for bolt11)
- /// * `method` - Payment method to use (bolt11, bolt12, or custom)
- /// * `description` - Optional description for the quote
- /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
- pub async fn mint_quote_unified(
- &self,
- amount: Option<Amount>,
- method: PaymentMethod,
- description: Option<String>,
- extra: Option<String>,
- ) -> Result<MintQuote, FfiError> {
- let quote = self
- .inner
- .mint_quote_unified(amount.map(Into::into), method.into(), description, extra)
- .await?;
- Ok(quote.into())
- }
- /// Mint tokens using bolt12
- pub async fn mint_bolt12(
- &self,
- quote_id: String,
- amount: Option<Amount>,
- amount_split_target: SplitTarget,
- spending_conditions: Option<SpendingConditions>,
- ) -> Result<Proofs, FfiError> {
- let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
- let proofs = self
- .inner
- .mint_bolt12(
- "e_id,
- amount.map(Into::into),
- amount_split_target.into(),
- conditions,
- )
- .await?;
- Ok(proofs.into_iter().map(|p| p.into()).collect())
- }
- pub async fn mint_unified(
- &self,
- quote_id: String,
- amount: Option<Amount>,
- amount_split_target: SplitTarget,
- spending_conditions: Option<SpendingConditions>,
- ) -> Result<Proofs, FfiError> {
- let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
- let proofs = self
- .inner
- .mint_unified(
- "e_id,
- amount.map(Into::into),
- amount_split_target.into(),
- conditions,
- )
- .await?;
- Ok(proofs.into_iter().map(|p| p.into()).collect())
- }
- /// Get a quote for a bolt12 melt
- pub async fn melt_bolt12_quote(
- &self,
- request: String,
- options: Option<MeltOptions>,
- ) -> Result<MeltQuote, FfiError> {
- let cdk_options = options.map(Into::into);
- let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?;
- Ok(quote.into())
- }
- /// Get a melt quote using a unified interface for any payment method
- ///
- /// This method supports bolt11, bolt12, and custom payment methods.
- /// For custom methods, you can pass extra JSON data that will be forwarded
- /// to the payment processor.
- ///
- /// # Arguments
- /// * `method` - Payment method to use (bolt11, bolt12, or custom)
- /// * `request` - Payment request string (invoice, offer, or custom format)
- /// * `options` - Optional melt options (MPP, amountless, etc.)
- /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
- pub async fn melt_quote_unified(
- &self,
- method: PaymentMethod,
- request: String,
- options: Option<MeltOptions>,
- extra: Option<String>,
- ) -> Result<MeltQuote, FfiError> {
- // Parse the extra JSON string into a serde_json::Value
- let extra_value = extra
- .map(|s| serde_json::from_str(&s))
- .transpose()
- .map_err(|e| FfiError::internal(format!("Invalid extra JSON: {}", e)))?;
- let cdk_options = options.map(Into::into);
- let quote = self
- .inner
- .melt_quote_unified(method.into(), request, cdk_options, extra_value)
- .await?;
- Ok(quote.into())
- }
- /// Swap proofs
- pub async fn swap(
- &self,
- amount: Option<Amount>,
- amount_split_target: SplitTarget,
- input_proofs: Proofs,
- spending_conditions: Option<SpendingConditions>,
- include_fees: bool,
- ) -> Result<Option<Proofs>, FfiError> {
- let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
- input_proofs.into_iter().map(|p| p.try_into()).collect();
- let cdk_proofs = cdk_proofs?;
- // Convert spending conditions if provided
- let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
- let result = self
- .inner
- .swap(
- amount.map(Into::into),
- amount_split_target.into(),
- cdk_proofs,
- conditions,
- include_fees,
- )
- .await?;
- Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
- }
- /// Get proofs by states
- pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
- let mut all_proofs = Vec::new();
- for state in states {
- let proofs = match state {
- ProofState::Unspent => self.inner.get_unspent_proofs().await?,
- ProofState::Pending => self.inner.get_pending_proofs().await?,
- ProofState::Reserved => self.inner.get_reserved_proofs().await?,
- ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
- ProofState::Spent => {
- // CDK doesn't have a method to get spent proofs directly
- // They are removed from the database when spent
- continue;
- }
- };
- for proof in proofs {
- all_proofs.push(proof.into());
- }
- }
- Ok(all_proofs)
- }
- /// Check if proofs are spent
- pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
- let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
- proofs.into_iter().map(|p| p.try_into()).collect();
- let cdk_proofs = cdk_proofs?;
- let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
- // Convert ProofState to bool (spent = true, unspent = false)
- let spent_bools = proof_states
- .into_iter()
- .map(|proof_state| {
- matches!(
- proof_state.state,
- cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
- )
- })
- .collect();
- Ok(spent_bools)
- }
- /// List transactions
- pub async fn list_transactions(
- &self,
- direction: Option<TransactionDirection>,
- ) -> Result<Vec<Transaction>, FfiError> {
- let cdk_direction = direction.map(Into::into);
- let transactions = self.inner.list_transactions(cdk_direction).await?;
- Ok(transactions.into_iter().map(Into::into).collect())
- }
- /// Get transaction by ID
- pub async fn get_transaction(
- &self,
- id: TransactionId,
- ) -> Result<Option<Transaction>, FfiError> {
- let cdk_id = id.try_into()?;
- let transaction = self.inner.get_transaction(cdk_id).await?;
- Ok(transaction.map(Into::into))
- }
- /// Get proofs for a transaction by transaction ID
- ///
- /// This retrieves all proofs associated with a transaction by looking up
- /// the transaction's Y values and fetching the corresponding proofs.
- pub async fn get_proofs_for_transaction(
- &self,
- id: TransactionId,
- ) -> Result<Vec<Proof>, FfiError> {
- let cdk_id = id.try_into()?;
- let proofs = self.inner.get_proofs_for_transaction(cdk_id).await?;
- Ok(proofs.into_iter().map(Into::into).collect())
- }
- /// Revert a transaction
- pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
- let cdk_id = id.try_into()?;
- self.inner.revert_transaction(cdk_id).await?;
- Ok(())
- }
- /// Subscribe to wallet events
- pub async fn subscribe(
- &self,
- params: SubscribeParams,
- ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
- let cdk_params: cdk::nuts::nut17::Params<Arc<String>> = params.clone().into();
- let sub_id = cdk_params.id.to_string();
- let active_sub = self.inner.subscribe(cdk_params).await?;
- Ok(std::sync::Arc::new(ActiveSubscription::new(
- active_sub, sub_id,
- )))
- }
- /// Refresh keysets from the mint
- pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
- let keysets = self.inner.refresh_keysets().await?;
- Ok(keysets.into_iter().map(Into::into).collect())
- }
- /// Get the active keyset for the wallet's unit
- pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
- let keyset = self.inner.get_active_keyset().await?;
- Ok(keyset.into())
- }
- /// Get fees for a specific keyset ID
- pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
- let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
- Ok(self
- .inner
- .get_keyset_fees_and_amounts_by_id(id)
- .await?
- .fee())
- }
- /// Reclaim unspent proofs (mark them as unspent in the database)
- pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), FfiError> {
- let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
- proofs.iter().map(|p| p.clone().try_into()).collect();
- let cdk_proofs = cdk_proofs?;
- self.inner.reclaim_unspent(cdk_proofs).await?;
- Ok(())
- }
- /// Check all pending proofs and return the total amount reclaimed
- pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
- let amount = self.inner.check_all_pending_proofs().await?;
- Ok(amount.into())
- }
- /// Calculate fee for a given number of proofs with the specified keyset
- pub async fn calculate_fee(
- &self,
- proof_count: u32,
- keyset_id: String,
- ) -> Result<Amount, FfiError> {
- let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
- let fee = self
- .inner
- .get_keyset_count_fee(&id, proof_count as u64)
- .await?;
- Ok(fee.into())
- }
- /// Pay a NUT-18 payment request
- ///
- /// This method prepares and sends a payment for the given payment request.
- /// It will use the Nostr or HTTP transport specified in the request.
- ///
- /// # Arguments
- ///
- /// * `payment_request` - The NUT-18 payment request to pay
- /// * `custom_amount` - Optional amount to pay (required if request has no amount)
- pub async fn pay_request(
- &self,
- payment_request: std::sync::Arc<PaymentRequest>,
- custom_amount: Option<Amount>,
- ) -> Result<(), FfiError> {
- self.inner
- .pay_request(
- payment_request.inner().clone(),
- custom_amount.map(Into::into),
- )
- .await?;
- Ok(())
- }
- }
- /// BIP353 methods for Wallet
- #[cfg(not(target_arch = "wasm32"))]
- #[uniffi::export(async_runtime = "tokio")]
- impl Wallet {
- /// Get a quote for a BIP353 melt
- ///
- /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
- /// and then creates a melt quote for that offer.
- pub async fn melt_bip353_quote(
- &self,
- bip353_address: String,
- amount_msat: Amount,
- ) -> Result<MeltQuote, FfiError> {
- let cdk_amount: cdk::Amount = amount_msat.into();
- let quote = self
- .inner
- .melt_bip353_quote(&bip353_address, cdk_amount)
- .await?;
- Ok(quote.into())
- }
- /// Get a quote for a Lightning address melt
- ///
- /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
- /// and then creates a melt quote for that invoice.
- pub async fn melt_lightning_address_quote(
- &self,
- lightning_address: String,
- amount_msat: Amount,
- ) -> Result<MeltQuote, FfiError> {
- let cdk_amount: cdk::Amount = amount_msat.into();
- let quote = self
- .inner
- .melt_lightning_address_quote(&lightning_address, cdk_amount)
- .await?;
- Ok(quote.into())
- }
- /// Get a quote for a human-readable address melt
- ///
- /// This method accepts a human-readable address that could be either a BIP353 address
- /// or a Lightning address. It intelligently determines which to try based on mint support:
- ///
- /// 1. If the mint supports Bolt12, it tries BIP353 first
- /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
- /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
- /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
- pub async fn melt_human_readable(
- &self,
- address: String,
- amount_msat: Amount,
- ) -> Result<MeltQuote, FfiError> {
- let cdk_amount: cdk::Amount = amount_msat.into();
- let quote = self
- .inner
- .melt_human_readable_quote(&address, cdk_amount)
- .await?;
- Ok(quote.into())
- }
- }
- /// Auth methods for Wallet
- #[uniffi::export(async_runtime = "tokio")]
- impl Wallet {
- /// Set Clear Auth Token (CAT) for authentication
- pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
- self.inner.set_cat(cat).await?;
- Ok(())
- }
- /// Set refresh token for authentication
- pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
- self.inner.set_refresh_token(refresh_token).await?;
- Ok(())
- }
- /// Refresh access token using the stored refresh token
- pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
- self.inner.refresh_access_token().await?;
- Ok(())
- }
- /// Mint blind auth tokens
- pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
- let proofs = self.inner.mint_blind_auth(amount.into()).await?;
- Ok(proofs.into_iter().map(|p| p.into()).collect())
- }
- /// Get unspent auth proofs
- pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
- let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
- Ok(auth_proofs.into_iter().map(Into::into).collect())
- }
- }
- /// Configuration for creating wallets
- #[derive(Debug, Clone, uniffi::Record)]
- pub struct WalletConfig {
- pub target_proof_count: Option<u32>,
- }
- /// Generates a new random mnemonic phrase
- #[uniffi::export]
- pub fn generate_mnemonic() -> Result<String, FfiError> {
- let mnemonic = Mnemonic::generate(12)
- .map_err(|e| FfiError::internal(format!("Failed to generate mnemonic: {}", e)))?;
- Ok(mnemonic.to_string())
- }
- /// Converts a mnemonic phrase to its entropy bytes
- #[uniffi::export]
- pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
- let m = Mnemonic::parse(&mnemonic)
- .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
- Ok(m.to_entropy())
- }
|