pub use sn_dbc::{self as dbc, Dbc, DbcTransaction, Token};
pub use sn_interface::dbcs::DbcReason;
use super::{helpers::parse_tokens_amount, register::EntryHash};
use crate::{
safeurl::{ContentType, SafeUrl, XorUrl},
Error, Result, Safe,
};
use sn_client::Client;
use sn_dbc::{
rng, Error as DbcError, Hash, Owner, OwnerOnce, PublicKey, RevealedAmount, SpentProof,
SpentProofShare, TransactionBuilder,
};
use sn_interface::types::fees::{FeeCiphers, RequiredFee};
use bytes::Bytes;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use tracing::{debug, warn};
use xor_name::XorName;
pub const WALLET_TYPE_TAG: u64 = 1_000;
pub type WalletSpendableDbcs = BTreeMap<String, (Dbc, EntryHash)>;
type ReissueCiphers = BTreeMap<PublicKey, BTreeMap<XorName, (RequiredFee, OwnerOnce)>>;
struct ReissueComponents {
input_dbcs_to_spend: Vec<Dbc>,
input_dbcs_entries_hash: BTreeSet<EntryHash>,
outputs_owners: Vec<(Token, OwnerOnce)>,
change_amount: Token,
#[cfg(not(feature = "data-network"))]
all_fee_cipher_params: ReissueCiphers,
}
const NUM_OF_DBC_REISSUE_ATTEMPTS: u8 = 5;
struct SpentProofKeyVerifier<'a> {
client: &'a Client,
}
impl sn_dbc::SpentProofKeyVerifier for SpentProofKeyVerifier<'_> {
type Error = crate::Error;
fn verify_known_key(&self, key: &PublicKey) -> Result<()> {
if !futures::executor::block_on(self.client.is_known_section_key(key)) {
Err(Error::DbcVerificationFailed(format!(
"SpentProof key is an unknown section key: {}",
key.to_hex()
)))
} else {
Ok(())
}
}
}
impl Safe {
pub async fn wallet_create(&self) -> Result<XorUrl> {
let xorurl = self.multimap_create(None, WALLET_TYPE_TAG).await?;
let mut safeurl = SafeUrl::from_url(&xorurl)?;
safeurl.set_content_type(ContentType::Wallet)?;
Ok(safeurl.to_string())
}
pub async fn wallet_deposit(
&self,
wallet_url: &str,
spendable_name: Option<&str>,
dbc: &Dbc,
secret_key: Option<bls::SecretKey>,
) -> Result<(String, Token)> {
let dbc_to_deposit = if dbc.is_bearer() {
if secret_key.is_some() {
return Err(Error::DbcDepositError(
"A secret key should not be supplied when depositing a bearer DBC".to_string(),
));
}
dbc.clone()
} else if let Some(sk) = secret_key {
let mut owned_dbc = dbc.clone();
owned_dbc.to_bearer(&sk).map_err(|err| {
if let DbcError::DbcBearerConversionFailed(_) = err {
Error::DbcDepositInvalidSecretKey
} else {
Error::DbcDepositError(err.to_string())
}
})?;
owned_dbc
} else {
return Err(Error::DbcDepositError(
"A secret key must be provided to deposit an owned DBC".to_string(),
));
};
let proof_key_verifier = SpentProofKeyVerifier {
client: self.get_safe_client()?,
};
dbc_to_deposit.verify(
&dbc_to_deposit.owner_base().secret_key()?,
&proof_key_verifier,
)?;
let spendable_name = match spendable_name {
Some(name) => name.to_string(),
None => format!("dbc-{}", &hex::encode(dbc_to_deposit.hash())[0..8]),
};
let amount = dbc_to_deposit
.revealed_amount_bearer()
.map(|amount| amount.value())?;
let safeurl = self.parse_and_resolve_url(wallet_url).await?;
self.insert_dbc_into_wallet(&safeurl, &dbc_to_deposit, spendable_name.clone())
.await?;
debug!(
"A spendable DBC deposited (amount: {}) into wallet at {}, with name: {}",
amount, safeurl, spendable_name
);
Ok((spendable_name, Token::from_nano(amount)))
}
pub async fn is_dbc_spent(&self, public_key: PublicKey) -> Result<bool> {
let client = self.get_safe_client()?;
let spent_proof_shares = client.spent_proof_shares(public_key).await?;
let spent_transactions: BTreeSet<Hash> = spent_proof_shares
.iter()
.map(|share| share.content.transaction_hash)
.collect();
let proof_key_verifier = SpentProofKeyVerifier { client };
let is_spent = spent_transactions.into_iter().any(|tx_hash| {
let shares_for_current_tx = spent_proof_shares
.iter()
.cloned()
.filter(|share| share.content.transaction_hash == tx_hash)
.collect();
verify_spent_proof_shares_for_tx(
public_key,
tx_hash,
&shares_for_current_tx,
&proof_key_verifier,
)
.is_ok()
});
Ok(is_spent)
}
pub async fn wallet_get(&self, wallet_url: &str) -> Result<WalletSpendableDbcs> {
let safeurl = self.parse_and_resolve_url(wallet_url).await?;
debug!("Wallet URL was parsed and resolved to: {}", safeurl);
self.fetch_wallet(&safeurl).await
}
pub(crate) async fn fetch_wallet(&self, safeurl: &SafeUrl) -> Result<WalletSpendableDbcs> {
let entries = match self.fetch_multimap(safeurl).await {
Ok(entries) => entries,
Err(Error::AccessDenied(_)) => {
return Err(Error::AccessDenied(format!(
"Couldn't read wallet found at \"{safeurl}\"",
)))
}
Err(Error::ContentNotFound(_)) => {
return Err(Error::ContentNotFound(format!(
"No wallet found at {safeurl}",
)))
}
Err(err) => {
return Err(Error::ContentError(format!(
"Failed to read balances from wallet: {err}",
)))
}
};
let mut balances = WalletSpendableDbcs::default();
for (entry_hash, (key, value)) in &entries {
let xorurl_str = std::str::from_utf8(value)?;
let dbc_xorurl = SafeUrl::from_xorurl(xorurl_str)?;
let dbc_bytes = self.fetch_data(&dbc_xorurl, None).await?;
let dbc: Dbc = match rmp_serde::from_slice(&dbc_bytes) {
Ok(dbc) => dbc,
Err(err) => {
warn!("Ignoring entry found in wallet since it cannot be deserialised as a valid DBC: {:?}", err);
continue;
}
};
let spendable_name = std::str::from_utf8(key)?.to_string();
balances.insert(spendable_name, (dbc, *entry_hash));
}
Ok(balances)
}
pub async fn wallet_balance(&self, wallet_url: &str) -> Result<Token> {
debug!("Finding total wallet balance for: {}", wallet_url);
let balances = self.wallet_get(wallet_url).await?;
let key_hashes: Vec<_> = balances
.iter()
.map(|(key, (_, hash))| (key, hash))
.collect();
debug!("Spendable balances to check: {key_hashes:?}");
let mut total_balance = Token::from_nano(0);
for (name, (dbc, _)) in &balances {
debug!("Checking spendable balance named: {}", name);
let balance = match dbc.revealed_amount_bearer() {
Ok(amount) => Token::from_nano(amount.value()),
Err(err) => {
warn!("Ignoring amount from DBC found in wallet due to dbc not being bearer: {:?}", err);
continue;
}
};
debug!("Amount in spendable balance '{}': {}", name, balance);
match total_balance.checked_add(balance) {
None => {
return Err(Error::ContentError(format!(
"Failed to calculate total balance due to overflow when adding {balance} to {total_balance}",
)))
}
Some(new_total_balance) => total_balance = new_total_balance,
}
}
Ok(total_balance)
}
pub async fn wallet_reissue(
&self,
wallet_url: &str,
amount: &str,
owner_public_key: Option<bls::PublicKey>,
reason: DbcReason,
) -> Result<Dbc> {
debug!(
"Reissuing DBC from wallet at {} for an amount of {} tokens",
wallet_url, amount
);
let dbcs = self
.wallet_reissue_many(
wallet_url,
[(amount.to_string(), owner_public_key)]
.into_iter()
.collect(),
reason,
)
.await?;
dbcs.into_iter()
.next()
.ok_or_else(|| Error::DbcReissueError(
"Unexpectedly failed to generate output DBC. No balance were removed from the wallet.".to_string(),
))
}
pub async fn wallet_reissue_many(
&self,
wallet_url: &str,
outputs: Vec<(String, Option<bls::PublicKey>)>,
reason: DbcReason,
) -> Result<Vec<Dbc>> {
let mut total_output_amount = Token::zero();
let mut outputs_owners = Vec::<(Token, OwnerOnce)>::new();
let mut rng = rng::thread_rng();
for (amount, base_owner_pk) in outputs {
let output_amount = parse_tokens_amount(&amount)?;
if output_amount.as_nano() == 0 {
return Err(Error::InvalidAmount(
"Output amount to reissue needs to be larger than zero (0).".to_string(),
));
}
total_output_amount =
total_output_amount
.checked_add(output_amount)
.ok_or_else(|| {
Error::DbcReissueError(
"Overflow occurred while calculating the total amount for the output DBC"
.to_string(),
)
})?;
let output_owner = if let Some(pk) = base_owner_pk {
let owner = Owner::from(pk);
OwnerOnce::from_owner_base(owner, &mut rng)
} else {
let owner = Owner::from_random_secret_key(&mut rng);
OwnerOnce::from_owner_base(owner, &mut rng)
};
outputs_owners.push((output_amount, output_owner));
}
let safeurl = self.parse_and_resolve_url(wallet_url).await?;
let spendable_dbcs = self.fetch_wallet(&safeurl).await?;
let components = self
.get_reissue_components(spendable_dbcs, outputs_owners)
.await?;
let ReissueComponents {
input_dbcs_to_spend,
input_dbcs_entries_hash,
outputs_owners,
change_amount,
#[cfg(not(feature = "data-network"))]
all_fee_cipher_params,
} = components;
let (output_dbcs, change_dbc) = self
.reissue_dbcs(
input_dbcs_to_spend,
outputs_owners,
change_amount,
reason,
#[cfg(not(feature = "data-network"))]
all_fee_cipher_params,
)
.await?;
if output_dbcs.is_empty() {
return Err(Error::DbcReissueError(
"Unexpectedly failed to generate output DBC. No balance were removed from the wallet.".to_string(),
));
}
if let Some(change_dbc) = change_dbc {
self.insert_dbc_into_wallet(
&safeurl,
&change_dbc,
format!("change-dbc-{}", &hex::encode(change_dbc.hash())[0..8]),
)
.await?;
}
self.multimap_remove(&safeurl.to_string(), input_dbcs_entries_hash)
.await?;
Ok(output_dbcs.into_iter().map(|(dbc, _, _)| dbc).collect())
}
pub(super) async fn send_tokens(
&self,
dbcs: Vec<Dbc>,
recipients: Vec<(Token, OwnerOnce)>,
) -> Result<(Vec<(Dbc, OwnerOnce, RevealedAmount)>, Option<Dbc>)> {
let client = self.get_safe_client()?;
Ok(sn_client::api::send_tokens(client, dbcs, recipients).await?)
}
async fn get_reissue_components(
&self,
spendable_dbcs: WalletSpendableDbcs,
outputs_owners: Vec<(Token, OwnerOnce)>,
) -> Result<ReissueComponents> {
let dbcs = spendable_dbcs
.iter()
.map(|(_, (dbc, _))| dbc)
.cloned()
.collect();
let client = self.get_safe_client()?;
let (input_dbcs_to_spend, outputs_owners, change_amount, all_fee_cipher_params) =
sn_client::api::select_dbc_inputs(client, dbcs, outputs_owners).await?;
let input_dbc_keys = input_dbcs_to_spend.iter().map(|d| d.public_key()).collect();
let input_dbcs_entries_hash = Self::entry_hashes(input_dbc_keys, spendable_dbcs);
Ok(ReissueComponents {
input_dbcs_to_spend,
input_dbcs_entries_hash,
outputs_owners,
change_amount,
all_fee_cipher_params,
})
}
fn entry_hashes(
input_dbc_keys: BTreeSet<PublicKey>,
spendable: WalletSpendableDbcs,
) -> BTreeSet<EntryHash> {
spendable
.into_iter()
.filter_map(|(_, (dbc, entry_hash))| {
input_dbc_keys
.contains(&dbc.public_key())
.then_some(entry_hash)
})
.collect()
}
async fn insert_dbc_into_wallet(
&self,
safeurl: &SafeUrl,
dbc: &Dbc,
spendable_name: String,
) -> Result<()> {
if !dbc.is_bearer() {
return Err(Error::InvalidInput("Only bearer DBC's are supported at this point by the wallet. Please deposit a bearer DBC's.".to_string()));
}
let dbc_bytes = Bytes::from(rmp_serde::to_vec_named(dbc).map_err(|err| {
Error::Serialisation(format!(
"Failed to serialise DBC to insert it into the wallet: {err:?}",
))
})?);
let dbc_xorurl = self.store_bytes(dbc_bytes, None).await?;
let entry = (spendable_name.into_bytes(), dbc_xorurl.into_bytes());
let _entry_hash = self
.multimap_insert(&safeurl.to_string(), entry, BTreeSet::default())
.await?;
Ok(())
}
async fn reissue_dbcs(
&self,
input_dbcs: Vec<Dbc>,
outputs: Vec<(Token, OwnerOnce)>,
change_amount: Token,
reason: DbcReason,
#[cfg(not(feature = "data-network"))] all_fee_cipher_params: BTreeMap<
PublicKey,
BTreeMap<XorName, (RequiredFee, OwnerOnce)>,
>,
) -> Result<(Vec<(Dbc, OwnerOnce, RevealedAmount)>, Option<Dbc>)> {
let mut tx_builder = TransactionBuilder::default()
.add_inputs_dbc_bearer(input_dbcs.iter())?
.add_outputs_by_amount(outputs.into_iter().map(|(token, owner)| (token, owner)));
let client = self.get_safe_client()?;
let change_owneronce =
OwnerOnce::from_owner_base(client.dbc_owner().clone(), &mut rng::thread_rng());
if change_amount.as_nano() > 0 {
tx_builder = tx_builder.add_output_by_amount(change_amount, change_owneronce.clone());
}
let proof_key_verifier = SpentProofKeyVerifier { client };
let inputs_spent_proofs: BTreeSet<SpentProof> = input_dbcs
.iter()
.flat_map(|dbc| dbc.inputs_spent_proofs.clone())
.collect();
let inputs_spent_transactions: BTreeSet<DbcTransaction> = input_dbcs
.iter()
.flat_map(|dbc| dbc.inputs_spent_transactions.clone())
.collect();
let mut dbc_builder = tx_builder.build(rng::thread_rng())?;
#[cfg(not(feature = "data-network"))]
let outputs = dbc_builder
.revealed_outputs
.iter()
.map(|output| (output.public_key, output.revealed_amount))
.collect();
for (public_key, tx) in dbc_builder.inputs() {
#[cfg(not(feature = "data-network"))]
let input_fee_ciphers = {
let fee_cipher_params = all_fee_cipher_params
.get(&public_key)
.ok_or(Error::DbcReissueError("Missing fee!".to_string()))?;
fee_ciphers(&outputs, fee_cipher_params)?
};
let tx_hash = Hash::from(tx.hash());
let mut attempts = 0;
loop {
attempts += 1;
client
.spend_dbc(
public_key,
tx.clone(),
reason,
inputs_spent_proofs.clone(),
inputs_spent_transactions.clone(),
#[cfg(not(feature = "data-network"))]
input_fee_ciphers.clone(),
)
.await?;
let spent_proof_shares = client.spent_proof_shares(public_key).await?;
let shares_for_current_tx: HashSet<SpentProofShare> = spent_proof_shares
.into_iter()
.filter(|proof_share| proof_share.content.transaction_hash == tx_hash)
.collect();
match verify_spent_proof_shares_for_tx(
public_key,
tx_hash,
&shares_for_current_tx,
&proof_key_verifier,
) {
Ok(()) => {
dbc_builder = dbc_builder
.add_spent_proof_shares(shares_for_current_tx.into_iter())
.add_spent_transaction(tx);
break;
}
Err(err) if attempts == NUM_OF_DBC_REISSUE_ATTEMPTS => {
return Err(Error::DbcReissueError(format!(
"Failed to spend input, {} proof shares obtained from spentbook: {}",
shares_for_current_tx.len(),
err
)));
}
Err(_) => {}
}
}
}
let mut output_dbcs = dbc_builder.build(&proof_key_verifier)?;
let mut change_dbc = None;
output_dbcs.retain(|(dbc, owneronce, _)| {
if owneronce == &change_owneronce && change_amount.as_nano() > 0 {
change_dbc = Some(dbc.clone());
false
} else {
true
}
});
Ok((output_dbcs, change_dbc))
}
}
#[cfg(not(feature = "data-network"))]
fn fee_ciphers(
outputs: &BTreeMap<PublicKey, RevealedAmount>,
fee_cipher_params: &BTreeMap<XorName, (RequiredFee, OwnerOnce)>,
) -> Result<BTreeMap<XorName, FeeCiphers>> {
let mut input_fee_ciphers = BTreeMap::new();
for (elder_name, (required_fee, owner_once)) in fee_cipher_params {
let derivation_index_cipher = required_fee
.content
.elder_reward_key
.encrypt(owner_once.derivation_index);
let output_owner_pk = owner_once.as_owner().public_key();
let revealed_amount = outputs
.get(&output_owner_pk)
.ok_or(Error::DbcReissueError("Missing output!".to_string()))?;
let amount_cipher = revealed_amount.encrypt(&output_owner_pk);
input_fee_ciphers.insert(
*elder_name,
FeeCiphers::new(amount_cipher, derivation_index_cipher),
);
}
Ok(input_fee_ciphers)
}
fn verify_spent_proof_shares_for_tx(
public_key: PublicKey,
tx_hash: Hash,
proof_shares: &HashSet<sn_dbc::SpentProofShare>,
proof_key_verifier: &SpentProofKeyVerifier,
) -> Result<()> {
SpentProof::try_from_proof_shares(public_key, tx_hash, proof_shares)
.and_then(|spent_proof| spent_proof.verify(tx_hash, proof_key_verifier))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::test_helpers::{
get_next_bearer_dbc, new_read_only_safe_instance, new_safe_instance,
new_safe_instance_with_dbc, new_safe_instance_with_dbc_owner, GENESIS_DBC,
};
use sn_client::{api::TransferError, Error as ClientError, ErrorMsg};
use sn_dbc::{Error as DbcError, Owner};
use sn_interface::network_knowledge::DEFAULT_ELDER_COUNT;
use anyhow::{anyhow, Result};
use xor_name::XorName;
#[tokio::test]
async fn test_wallet_create() -> Result<()> {
let safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
assert!(wallet_xorurl.starts_with("safe://"));
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, Token::zero());
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_with_bearer_dbc() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (_, amount) = safe
.wallet_deposit(&wallet_xorurl, None, &dbc, None)
.await?;
assert_eq!(amount, dbc_balance);
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert_eq!(wallet_balances.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_with_name() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (name, amount) = safe
.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, None)
.await?;
assert_eq!(name, "my-dbc");
assert_eq!(amount, dbc_balance);
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert!(wallet_balances.contains_key("my-dbc"));
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_with_no_name() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (name, amount) = safe
.wallet_deposit(&wallet_xorurl, None, &dbc, None)
.await?;
assert_eq!(amount, dbc_balance);
assert_eq!(name, format!("dbc-{}", &hex::encode(dbc.hash())[0..8]));
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert!(wallet_balances.contains_key(&name));
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_with_owned_dbc() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let sk = bls::SecretKey::random();
safe.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, None)
.await?;
let owned_dbc = safe
.wallet_reissue(
&wallet_xorurl,
"2.35",
Some(sk.public_key()),
DbcReason::none(),
)
.await?;
safe.wallet_deposit(
&wallet_xorurl,
Some("owned-dbc"),
&owned_dbc,
Some(sk.clone()),
)
.await?;
let owner = Owner::from(sk);
let balances = safe.wallet_get(&wallet_xorurl).await?;
let (owned_dbc, _) = balances
.get("owned-dbc")
.ok_or_else(|| anyhow!("Couldn't read DBC from wallet"))?;
assert_eq!(*owned_dbc.owner_base(), owner);
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_with_owned_dbc_without_providing_secret_key() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let pk = bls::SecretKey::random().public_key();
safe.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, None)
.await?;
let owned_dbc = safe
.wallet_reissue(&wallet_xorurl, "2.35", Some(pk), DbcReason::none())
.await?;
let result = safe
.wallet_deposit(&wallet_xorurl, Some("owned-dbc"), &owned_dbc, None)
.await;
match result {
Ok(_) => Err(anyhow!(
"This test case should result in an error".to_string()
)),
Err(Error::DbcDepositError(e)) => {
assert_eq!(e, "A secret key must be provided to deposit an owned DBC");
Ok(())
}
Err(_) => Err(anyhow!("This test should use a DbcDepositError".to_string())),
}
}
#[tokio::test]
async fn test_wallet_deposit_with_owned_dbc_with_invalid_secret_key() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let sk = bls::SecretKey::random();
let sk2 = bls::SecretKey::random();
let pk = sk.public_key();
safe.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, None)
.await?;
let owned_dbc = safe
.wallet_reissue(&wallet_xorurl, "2.35", Some(pk), DbcReason::none())
.await?;
let result = safe
.wallet_deposit(&wallet_xorurl, Some("owned-dbc"), &owned_dbc, Some(sk2))
.await;
match result {
Ok(_) => Err(anyhow!(
"This test case should result in an error".to_string()
)),
Err(Error::DbcDepositInvalidSecretKey) => Ok(()),
Err(_) => Err(anyhow!(
"This test should use a DbcDepositInvalidSecretKey error".to_string()
)),
}
}
#[tokio::test]
async fn test_wallet_deposit_with_bearer_dbc_and_secret_key() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let sk = bls::SecretKey::random();
let result = safe
.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, Some(sk))
.await;
match result {
Ok(_) => Err(anyhow!(
"This test case should result in an error".to_string()
)),
Err(Error::DbcDepositError(e)) => {
assert_eq!(
e,
"A secret key should not be supplied when depositing a bearer DBC"
);
Ok(())
}
Err(_) => Err(anyhow!("This test should use a DbcDepositError".to_string())),
}
}
#[tokio::test]
async fn test_wallet_reissue_with_deposited_owned_dbc() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let wallet2_xorurl = safe.wallet_create().await?;
let sk = bls::SecretKey::random();
safe.wallet_deposit(&wallet_xorurl, Some("my-dbc"), &dbc, None)
.await?;
let owned_dbc = safe
.wallet_reissue(
&wallet_xorurl,
"2.35",
Some(sk.public_key()),
DbcReason::none(),
)
.await?;
safe.wallet_deposit(
&wallet2_xorurl,
Some("owned-dbc"),
&owned_dbc,
Some(sk.clone()),
)
.await?;
let result = safe
.wallet_reissue(&wallet2_xorurl, "2", None, DbcReason::none())
.await;
match result {
Ok(_) => {
Ok(())
}
Err(e) => Err(anyhow!(e)),
}
}
#[tokio::test]
async fn test_wallet_balance() -> Result<()> {
let (safe, dbc1, dbc1_balance) = new_safe_instance_with_dbc().await?;
let (dbc2, dbc2_balance) = get_next_bearer_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc1, None)
.await?;
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, dbc1_balance);
safe.wallet_deposit(&wallet_xorurl, Some("my-second-dbc"), &dbc2, None)
.await?;
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(
current_balance.as_nano(),
dbc1_balance.as_nano() + dbc2_balance.as_nano()
);
Ok(())
}
#[tokio::test]
async fn test_wallet_balance_overflow() -> Result<()> {
let safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
for i in 0..5 {
safe.wallet_deposit(
&wallet_xorurl,
Some(&format!("my-dbc-#{i}")),
&GENESIS_DBC,
None,
)
.await?;
}
let genesis_balance = 4_525_524_120_000_000_000;
match safe.wallet_balance(&wallet_xorurl).await {
Err(Error::ContentError(msg)) => {
assert_eq!(
msg,
format!(
"Failed to calculate total balance due to overflow when adding {} to {}",
Token::from_nano(genesis_balance),
Token::from_nano(genesis_balance * 4)
)
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(balance) => Err(anyhow!("Wallet balance obtained unexpectedly: {}", balance)),
}
}
#[tokio::test]
async fn test_wallet_get() -> Result<()> {
let (safe, dbc1, dbc1_balance) = new_safe_instance_with_dbc().await?;
let (dbc2, dbc2_balance) = get_next_bearer_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc1, None)
.await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-second-dbc"), &dbc2, None)
.await?;
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
let (dbc1_read, _) = wallet_balances
.get("my-first-dbc")
.ok_or_else(|| anyhow!("Couldn't read first DBC from fetched wallet"))?;
assert_eq!(dbc1_read.owner_base(), dbc1.owner_base());
let balance1 = dbc1_read
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from first DBC fetched: {:?}", err))?;
assert_eq!(balance1.value(), dbc1_balance.as_nano());
let (dbc2_read, _) = wallet_balances
.get("my-second-dbc")
.ok_or_else(|| anyhow!("Couldn't read second DBC from fetched wallet"))?;
assert_eq!(dbc2_read.owner_base(), dbc2.owner_base());
let balance2 = dbc2_read
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from second DBC fetched: {:?}", err))?;
assert_eq!(balance2.value(), dbc2_balance.as_nano());
Ok(())
}
#[ignore]
#[tokio::test]
async fn test_wallet_get_not_owned_wallet() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc, None)
.await?;
let read_only_safe = new_read_only_safe_instance().await?;
match read_only_safe.wallet_get(&wallet_xorurl).await {
Err(Error::AccessDenied(msg)) => {
assert_eq!(
msg,
format!("Couldn't read wallet found at \"{wallet_xorurl}\"")
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Wallet get succeeded unexpectedly".to_string())),
}
}
#[tokio::test]
async fn test_wallet_get_non_compatible_content() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc, None)
.await?;
let corrupted_dbc_xorurl = safe.store_bytes(Bytes::from_static(b"bla"), None).await?;
let entry = (b"corrupted-dbc".to_vec(), corrupted_dbc_xorurl.into_bytes());
safe.multimap_insert(&wallet_xorurl, entry, BTreeSet::default())
.await?;
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, dbc_balance);
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_with_multiple_input_dbcs() -> Result<()> {
let (safe, dbc1, dbc1_balance) = new_safe_instance_with_dbc().await?;
let (dbc2, dbc2_balance) = get_next_bearer_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc1, None)
.await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-2"), &dbc2, None)
.await?;
let fee_1 = get_spend_fee(&safe, &dbc1).await?;
let fee_2 = get_spend_fee(&safe, &dbc2).await?;
let total_fee = fee_1.as_nano() + fee_2.as_nano();
let expected_change = total_fee;
let change_plus_fees = 2 * total_fee;
let amount_to_reissue =
Token::from_nano(dbc1_balance.as_nano() + dbc2_balance.as_nano() - change_plus_fees);
let output_dbc = safe
.wallet_reissue(
&wallet_xorurl,
&amount_to_reissue.to_string(),
None,
DbcReason::none(),
)
.await?;
let output_balance = output_dbc
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from output DBC: {:?}", err))?;
assert_eq!(output_balance.value(), amount_to_reissue.as_nano());
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, Token::from_nano(expected_change));
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert_eq!(wallet_balances.len(), 1);
let (_, (change_dbc_read, _)) = wallet_balances
.iter()
.next()
.ok_or_else(|| anyhow!("Couldn't read change DBC from fetched wallet"))?;
let change = change_dbc_read
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from change DBC fetched: {:?}", err))?;
assert_eq!(change.value(), expected_change);
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_with_single_input_dbc() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc, None)
.await?;
let fee = get_spend_fee(&safe, &dbc).await?;
let output_dbc = safe
.wallet_reissue(&wallet_xorurl, "1", None, DbcReason::none())
.await?;
let output_balance = output_dbc
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from output DBC: {:?}", err))?;
assert_eq!(output_balance.value(), 1_000_000_000);
let change_amount = Token::from_nano(dbc_balance.as_nano() - 1_000_000_000 - fee.as_nano()); let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, change_amount);
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert_eq!(wallet_balances.len(), 1);
let (_, (change_dbc_read, _)) = wallet_balances
.iter()
.next()
.ok_or_else(|| anyhow!("Couldn't read change DBC from fetched wallet"))?;
let change = change_dbc_read
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from change DBC fetched: {:?}", err))?;
assert_eq!(change.value(), change_amount.as_nano());
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_with_persistent_dbc_owner() -> Result<()> {
let (safe, dbc_owner) = new_safe_instance_with_dbc_owner(
"3917ad935714cf1e71b9b5e2831684811e83acc6c10f030031fe886292152e83",
)
.await?;
let wallet_xorurl = safe.wallet_create().await?;
let (_safe, dbc, _) = new_safe_instance_with_dbc().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc, None)
.await?;
let _ = safe
.wallet_reissue(&wallet_xorurl, "1", None, DbcReason::none())
.await?;
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
let (_, (change_dbc_read, _)) = wallet_balances
.iter()
.next()
.ok_or_else(|| anyhow!("Couldn't read change DBC from fetched wallet"))?;
assert_eq!(*change_dbc_read.owner_base(), dbc_owner);
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_with_owned_dbc() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc, None)
.await?;
let pk = bls::SecretKey::random().public_key();
let owner = Owner::from(pk);
let output_dbc = safe
.wallet_reissue(&wallet_xorurl, "1", Some(pk), DbcReason::none())
.await?;
assert_eq!(owner, *output_dbc.owner_base());
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_with_reason() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc, None)
.await?;
let pk = bls::SecretKey::random().public_key();
let just_any_xorname = XorName::from_content(&[1, 2, 3, 4]);
let any_reason = DbcReason::from(just_any_xorname);
let output_dbc = safe
.wallet_reissue(&wallet_xorurl, "1", Some(pk), any_reason)
.await?;
assert_eq!(Some(any_reason.into()), output_dbc.reason());
Ok(())
}
#[tokio::test]
async fn test_wallet_not_enough_balance() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc"), &dbc, None)
.await?;
match safe
.wallet_reissue(
&wallet_xorurl,
&Token::from_nano(dbc_balance.as_nano() + 1).to_string(),
None,
DbcReason::none(),
)
.await
{
Err(Error::ClientError(ClientError::TransferError(
TransferError::NotEnoughBalance(msg),
))) => {
assert_eq!(msg, dbc_balance.to_string());
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Wallet reissue succeeded unexpectedly".to_string())),
}
}
#[tokio::test]
async fn test_wallet_reissue_invalid_amount() -> Result<()> {
let safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
match safe
.wallet_reissue(&wallet_xorurl, "0", None, DbcReason::none())
.await
{
Err(Error::InvalidAmount(msg)) => {
assert_eq!(
msg,
"Output amount to reissue needs to be larger than zero (0)."
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Wallet reissue succeeded unexpectedly".to_string())),
}
}
#[tokio::test]
async fn test_wallet_reissue_with_non_compatible_content() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc, None)
.await?;
let corrupted_dbc_xorurl = safe.store_bytes(Bytes::from_static(b"bla"), None).await?;
let entry = (b"corrupted-dbc".to_vec(), corrupted_dbc_xorurl.into_bytes());
safe.multimap_insert(&wallet_xorurl, entry, BTreeSet::default())
.await?;
let fee = get_spend_fee(&safe, &dbc).await?;
let _ = safe
.wallet_reissue(&wallet_xorurl, "0.4", None, DbcReason::none())
.await?;
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(
current_balance,
Token::from_nano(dbc_balance.as_nano() - 400_000_000 - fee.as_nano())
);
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_all_balance() -> Result<()> {
let (safe, dbc, dbc_balance) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("my-first-dbc"), &dbc, None)
.await?;
let fee = get_spend_fee(&safe, &dbc).await?;
let _ = safe
.wallet_reissue(
&wallet_xorurl,
&Token::from_nano(dbc_balance.as_nano() - fee.as_nano()).to_string(), None,
DbcReason::none(),
)
.await?;
tokio::time::sleep(tokio::time::Duration::from_millis(10000)).await;
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, Token::zero());
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert!(wallet_balances.is_empty());
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_reissued_dbc() -> Result<()> {
let (safe, dbc, _) = new_safe_instance_with_dbc().await?;
let wallet1_xorurl = safe.wallet_create().await?;
let wallet2_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet1_xorurl, Some("deposited-dbc"), &dbc, None)
.await?;
let output_dbc = safe
.wallet_reissue(&wallet1_xorurl, "0.25", None, DbcReason::none())
.await?;
safe.wallet_deposit(&wallet2_xorurl, Some("reissued-dbc"), &output_dbc, None)
.await?;
let balance = safe.wallet_balance(&wallet2_xorurl).await?;
assert_eq!(balance, Token::from_nano(250_000_000));
Ok(())
}
#[tokio::test]
async fn test_wallet_deposit_dbc_verification_fails() -> Result<()> {
let (safe, mut dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let random_pk = bls::SecretKey::random().public_key();
dbc.inputs_spent_proofs = dbc
.inputs_spent_proofs
.into_iter()
.map(|mut proof| {
proof.spentbook_pub_key = random_pk;
proof
})
.collect();
match safe
.wallet_deposit(&wallet_xorurl, Some("deposited-dbc"), &dbc, None)
.await
{
Err(Error::DbcError(DbcError::InvalidSpentProofSignature(_public_key))) => Ok(()),
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Wallet deposit succeeded unexpectedly".to_string())),
}
}
#[tokio::test]
async fn test_wallet_reissue_dbc_verification_fails() -> Result<()> {
let (safe, mut dbc, _) = new_safe_instance_with_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
let random_pk = bls::SecretKey::random().public_key();
dbc.inputs_spent_proofs = dbc
.inputs_spent_proofs
.into_iter()
.map(|mut proof| {
proof.spentbook_pub_key = random_pk;
proof
})
.collect();
safe.insert_dbc_into_wallet(
&SafeUrl::from_url(&wallet_xorurl)?,
&dbc,
"corrupted_dbc".to_string(),
)
.await?;
match safe
.wallet_reissue(&wallet_xorurl, "0.1", None, DbcReason::none())
.await
{
Err(Error::ClientError(ClientError::CmdError {
source: ErrorMsg::InvalidOperation(msg),
..
})) => {
assert_eq!(
msg,
format!(
"Failed to perform operation: SpentbookError(\"Spent proof \
signature {random_pk:?} is invalid\")",
)
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Wallet deposit succeeded unexpectedly".to_string())),
}
}
#[tokio::test]
async fn test_wallet_is_dbc_spent() -> Result<()> {
let safe = new_safe_instance().await?;
let is_genesis_spent = safe.is_dbc_spent(GENESIS_DBC.public_key()).await?;
assert!(is_genesis_spent);
Ok(())
}
#[tokio::test]
async fn test_wallet_dbc_is_unspent() -> Result<()> {
let (safe, unspent_dbc, _) = new_safe_instance_with_dbc().await?;
let is_unspent_dbc_spent = safe.is_dbc_spent(unspent_dbc.public_key()).await?;
assert!(!is_unspent_dbc_spent);
Ok(())
}
#[tokio::test]
async fn test_wallet_reissue_multiple_output_dbcs() -> Result<()> {
let (safe, dbc1, dbc1_balance) = new_safe_instance_with_dbc().await?;
let (dbc2, dbc2_balance) = get_next_bearer_dbc().await?;
let wallet_xorurl = safe.wallet_create().await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-1"), &dbc1, None)
.await?;
safe.wallet_deposit(&wallet_xorurl, Some("deposited-dbc-2"), &dbc2, None)
.await?;
let fee_1 = get_spend_fee(&safe, &dbc1).await?;
let fee_2 = get_spend_fee(&safe, &dbc2).await?;
let total_fee = fee_1.as_nano() + fee_2.as_nano();
let expected_change = 1000;
let change_plus_fees = 1000 + total_fee;
let amount_to_reissue =
Token::from_nano(dbc1_balance.as_nano() + dbc2_balance.as_nano() - change_plus_fees);
let output_amounts = vec![
dbc1_balance.as_nano() - total_fee,
dbc2_balance.as_nano() - (expected_change + 400),
150,
100,
60,
90,
];
assert_eq!(
amount_to_reissue.as_nano(),
output_amounts.iter().sum::<u64>()
);
let outputs_owners = output_amounts
.iter()
.map(|amount| (Token::from_nano(*amount).to_string(), None))
.collect();
let output_dbcs = safe
.wallet_reissue_many(&wallet_xorurl, outputs_owners, DbcReason::none())
.await?;
#[cfg(feature = "data-network")]
let expected_fee_outputs = 0; #[cfg(not(feature = "data-network"))]
let expected_fee_outputs = 2 * DEFAULT_ELDER_COUNT;
assert_eq!(
output_dbcs.len(),
output_amounts.len() + expected_fee_outputs
);
let mut num_fee_outputs = 0;
for dbc in output_dbcs {
if let Ok(balance) = dbc.revealed_amount_bearer() {
assert!(output_amounts.contains(&balance.value()));
} else {
num_fee_outputs += 1;
}
}
assert_eq!(num_fee_outputs, expected_fee_outputs);
let current_balance = safe.wallet_balance(&wallet_xorurl).await?;
assert_eq!(current_balance, Token::from_nano(expected_change));
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert_eq!(wallet_balances.len(), 1);
let (_, (change_dbc_read, _)) = wallet_balances
.iter()
.next()
.ok_or_else(|| anyhow!("Couldn't read change DBC from fetched wallet"))?;
let change = change_dbc_read
.revealed_amount_bearer()
.map_err(|err| anyhow!("Couldn't read balance from change DBC fetched: {:?}", err))?;
assert_eq!(change.value(), expected_change);
Ok(())
}
async fn get_spend_fee(safe: &Safe, dbc: &Dbc) -> Result<Token> {
let secret_key = dbc.as_revealed_input_bearer()?.secret_key;
let dbc_id = secret_key.public_key(); let client = safe.get_safe_client()?;
let elder_fees = client.get_section_fees(dbc_id).await?;
let mut decrypted_fees = vec![];
for (elder, fee) in elder_fees {
match fee.content.decrypt_amount(&secret_key) {
Ok(amount) => decrypted_fees.push(amount),
Err(_) => Err(crate::Error::DbcReissueError(format!(
"Could not decrypt fee amount sent from {elder}."
)))?,
}
}
let fee_per_input = decrypted_fees.into_iter()
.fold(Some(Token::zero()), |total, fee| {
total.and_then(|t| t.checked_add(fee))
})
.ok_or_else(|| crate::Error::DbcReissueError(
"Overflow occurred while summing the individual Elder's fees in order to calculate the total amount for the output DBCs."
.to_string(),
))?;
Ok(fee_per_input)
}
}