use super::{
common::ed_sk_from_hex,
helpers::{parse_coins_amount, pk_from_hex},
};
use crate::{
api::app::safeurl::{SafeContentType, SafeDataType, SafeUrl, XorUrl},
Error, Result, Safe,
};
use hex::encode;
use log::debug;
use serde::{Deserialize, Serialize};
use sn_data_types::{Keypair, MapValue, Token};
use std::collections::BTreeMap;
use xor_name::XorName;
const WALLET_TYPE_TAG: u64 = 1_000;
const WALLET_DEFAULT_BYTES: &[u8] = b"_default";
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct WalletSpendableBalance {
pub xorurl: XorUrl,
pub sk: String,
}
pub type WalletSpendableBalances = BTreeMap<String, (bool, WalletSpendableBalance)>;
impl Safe {
pub async fn wallet_create(&mut self) -> Result<XorUrl> {
let xorname = self
.safe_client
.store_map(None, WALLET_TYPE_TAG, None, None)
.await?;
SafeUrl::encode_mutable_data(
xorname,
WALLET_TYPE_TAG,
SafeContentType::Wallet,
self.xorurl_base,
)
}
pub async fn wallet_insert(
&mut self,
url: &str,
name: Option<&str>,
default: bool,
sk: &str,
) -> Result<String> {
let acutal_sk = ed_sk_from_hex(sk)?;
let keypair = Keypair::from(acutal_sk);
let xorname = XorName::from(keypair.public_key());
let xorurl = SafeUrl::encode_safekey(xorname, self.xorurl_base)?;
let value = WalletSpendableBalance {
xorurl: xorurl.clone(),
sk: sk.to_string(),
};
let serialised_value = serde_json::to_string(&value).map_err(|err| {
Error::Serialisation(format!(
"Failed to serialise data to insert in Wallet container: {:?}",
err
))
})?;
let md_key = name.unwrap_or(&xorurl);
let (safeurl, _) = self.parse_and_resolve_url(url).await?;
self.safe_client
.map_insert(
safeurl.xorname(),
WALLET_TYPE_TAG,
&md_key.to_string().into_bytes(),
&serialised_value.into_bytes(),
)
.await
.map_err(|err| match err {
Error::EntryExists(_) => Error::EntryExists(format!(
"A spendable balance already exists in the Wallet with the same name: '{}'",
md_key
)),
other => other,
})?;
debug!(
"Wallet at {} had a spendable balance added with name: {}.",
&url, md_key
);
if default {
let default_key = md_key.to_string().into_bytes();
match self
.safe_client
.map_insert(
safeurl.xorname(),
WALLET_TYPE_TAG,
WALLET_DEFAULT_BYTES,
&default_key,
)
.await
{
Err(Error::EntryExists(_)) => {
let (_, version) = self.wallet_get_default_balance(url).await?;
self.safe_client
.update_map(
safeurl.xorname(),
WALLET_TYPE_TAG,
WALLET_DEFAULT_BYTES,
&default_key,
version + 1,
)
.await
}
other => other,
}?;
debug!("Default wallet set.");
}
Ok(md_key.to_string())
}
pub async fn wallet_balance(&mut self, url: &str) -> Result<String> {
debug!("Finding total wallet balance for: {:?}", url);
let mut total_balance = Token::from_nano(0);
let (safeurl, nrs_safeurl) = self.parse_and_resolve_url(url).await?;
debug!("Wallet URL was parsed and resolved to: {:?}", safeurl);
let url_path = if let Some(nrs_url) = nrs_safeurl {
nrs_url.path().to_string()
} else {
safeurl.path().to_string()
};
let balances = if url_path.is_empty() {
debug!("We'll check the total balance of the Wallet");
gen_wallet_spendable_balances_list(self, safeurl.xorname(), safeurl.type_tag(), url)
.await?
} else {
let balance_name = &url_path[1..];
debug!(
"We'll check only the balance for spendable balance named: '{}'",
balance_name
);
let (spendable_balance, _) = wallet_get_spendable_balance(
self,
safeurl.xorname(),
safeurl.type_tag(),
balance_name.as_bytes(),
)
.await
.map_err(|_| {
Error::InvalidInput(format!(
"No spendable balance named '{}' found in Wallet: '{}'",
balance_name, url
))
})?;
let mut balances = WalletSpendableBalances::default();
balances.insert(balance_name.to_string(), (false, spendable_balance));
balances
};
debug!("Spendable balances to check: {:?}", balances);
for (name, (_, balance)) in balances.iter() {
debug!("Checking wallet of name: {:?}", name);
let secret_key = ed_sk_from_hex(&balance.sk)?;
let id = Keypair::from(secret_key);
let current_balance = self
.safe_client
.read_balance_from_keypair(id)
.await
.map_err(|_| {
Error::ContentNotFound("One of the SafeKey's was not found".to_string())
})?;
debug!("{}: balance is {}", name, current_balance);
match total_balance.checked_add(current_balance) {
None => {
return Err(Error::ContentError(format!(
"Failed to calculate total balance due to overflow: {}",
total_balance
)))
}
Some(new_balance_coins) => total_balance = new_balance_coins,
};
}
Ok(total_balance.to_string())
}
pub async fn wallet_get_default_balance(
&mut self,
url: &str,
) -> Result<(WalletSpendableBalance, u64)> {
let (safeurl, _) = self.parse_and_resolve_url(url).await?;
let default = self
.safe_client
.map_get_value(safeurl.xorname(), safeurl.type_tag(), WALLET_DEFAULT_BYTES)
.await
.map_err(|err| match err {
Error::AccessDenied(_) => Error::AccessDenied(format!(
"Couldn't read source Wallet for the transfer at \"{}\"",
url
)),
Error::ContentNotFound(_) => {
Error::ContentError(format!("No Wallet found at \"{}\"", url))
}
_other => {
Error::ContentError(format!("No default balance found at Wallet \"{}\"", url))
}
})?;
let default_data = match default {
MapValue::Seq(value) => value.data,
MapValue::Unseq(_) => {
return Err(Error::ContentError(
"Wallet could not be parsed as wallet map is unsequenced.".to_string(),
))
}
};
wallet_get_spendable_balance(self, safeurl.xorname(), safeurl.type_tag(), &default_data)
.await
}
pub async fn wallet_transfer(
&mut self,
amount: &str,
from_wallet_url: &str,
to: &str,
) -> Result<u64> {
let amount_coins = parse_coins_amount(amount)?;
let (from_safeurl, from_nrs_safeurl) = self
.parse_and_resolve_url(&from_wallet_url)
.await
.map_err(|_| {
Error::InvalidInput(format!(
"Failed to parse the 'from' URL: {}",
from_wallet_url
))
})?;
if from_safeurl.content_type() != SafeContentType::Wallet {
return Err(Error::InvalidInput(format!(
"The 'from_url' URL doesn't target a Wallet, it is: {:?} ({})",
from_safeurl.content_type(),
from_safeurl.data_type()
)));
}
let from_spendable_balance =
resolve_wallet_url(self, from_wallet_url, from_safeurl, from_nrs_safeurl).await?;
let from_sk = ed_sk_from_hex(&from_spendable_balance.sk)?;
let from = Some(Keypair::from(from_sk));
let result = if to.starts_with("safe://") {
let (to_safeurl, to_nrs_safeurl) =
self.parse_and_resolve_url(to).await.map_err(|_| {
Error::InvalidInput(format!("Failed to parse the 'to' URL: {}", to))
})?;
let to_xorname = if to_safeurl.content_type() == SafeContentType::Wallet {
let to_wallet_balance =
resolve_wallet_url(self, to, to_safeurl, to_nrs_safeurl).await?;
SafeUrl::from_url(&to_wallet_balance.xorurl)?.xorname()
} else if to_safeurl.content_type() == SafeContentType::Raw
&& to_safeurl.data_type() == SafeDataType::SafeKey
{
to_safeurl.xorname()
} else {
return Err(Error::InvalidInput(format!(
"The destination URL doesn't target a SafeKey or Wallet, target is: {:?} ({})",
to_safeurl.content_type(),
to_safeurl.data_type()
)));
};
self.safe_client
.safecoin_transfer_to_xorname(from, to_xorname, amount_coins)
.await
} else {
let to_pk = pk_from_hex(to)?;
self.safe_client
.safecoin_transfer_to_pk(from, to_pk, amount_coins)
.await
};
match result {
Err(Error::NotEnoughBalance(_)) => Err(Error::NotEnoughBalance(format!(
"Not enough balance for the transfer at Wallet \"{}\"",
from_wallet_url
))),
Err(other_error) => Err(other_error),
Ok(id) => Ok(id),
}
}
pub async fn wallet_get(&mut self, url: &str) -> Result<WalletSpendableBalances> {
let (safeurl, _) = self.parse_and_resolve_url(url).await?;
self.fetch_wallet(&safeurl).await
}
pub(crate) async fn fetch_wallet(
&mut self,
safeurl: &SafeUrl,
) -> Result<WalletSpendableBalances> {
gen_wallet_spendable_balances_list(
self,
safeurl.xorname(),
safeurl.type_tag(),
&safeurl.to_string(),
)
.await
}
}
async fn gen_wallet_spendable_balances_list(
safe: &mut Safe,
xorname: XorName,
type_tag: u64,
url: &str,
) -> Result<WalletSpendableBalances> {
let entries = match safe.safe_client.list_map_entries(xorname, type_tag).await {
Ok(entries) => entries,
Err(Error::AccessDenied(_)) => {
return Err(Error::AccessDenied(format!(
"Couldn't read Wallet at \"{}\"",
url
)))
}
Err(Error::ContentNotFound(_)) => {
return Err(Error::ContentNotFound(format!(
"No Wallet found at {}",
url
)))
}
Err(err) => {
return Err(Error::ContentError(format!(
"Failed to read balances from Wallet: {}",
err
)))
}
};
let mut balances = WalletSpendableBalances::default();
let mut default_balance = "".to_string();
for (key, value) in entries.iter() {
let value_str = String::from_utf8_lossy(&value.data).to_string();
if key.as_slice() == WALLET_DEFAULT_BYTES {
default_balance = value_str;
} else {
let spendable_balance: WalletSpendableBalance = serde_json::from_str(&value_str)
.map_err(|_| {
Error::ContentError(
"Couldn't deserialise data stored in the Wallet".to_string(),
)
})?;
let thename = String::from_utf8_lossy(key).to_string();
balances.insert(thename, (false, spendable_balance));
}
}
if !default_balance.is_empty() {
let mut default = balances.get_mut(&default_balance).ok_or_else(|| {
Error::ContentError(format!(
"Failed to get default spendable balance from Wallet at \"{}\"",
url
))
})?;
default.0 = true;
}
Ok(balances)
}
async fn wallet_get_spendable_balance(
safe: &mut Safe,
xorname: XorName,
type_tag: u64,
balance_name: &[u8],
) -> Result<(WalletSpendableBalance, u64)> {
let the_balance: (WalletSpendableBalance, u64) = {
let default_balance_vec = match safe
.safe_client
.map_get_value(xorname, type_tag, balance_name)
.await
.map_err(|_| {
Error::ContentError(format!(
"Default balance set but not found at Wallet \"{}\"",
encode(&xorname)
))
})? {
MapValue::Seq(data) => data,
MapValue::Unseq(_) => {
return Err(Error::ContentError(
"Wallet could not be parsed as wallet map is unsequenced.".to_string(),
))
}
};
let default_balance = String::from_utf8_lossy(&default_balance_vec.data).to_string();
let spendable_balance: WalletSpendableBalance = serde_json::from_str(&default_balance)
.map_err(|_| {
Error::ContentError("Couldn't deserialise data stored in the Wallet".to_string())
})?;
(spendable_balance, default_balance_vec.version)
};
Ok(the_balance)
}
async fn resolve_wallet_url(
safe: &mut Safe,
wallet_url: &str,
safeurl: SafeUrl,
nrs_safeurl: Option<SafeUrl>,
) -> Result<WalletSpendableBalance> {
let url_path = if let Some(nrs_url) = nrs_safeurl {
nrs_url.path().to_string()
} else {
safeurl.path().to_string()
};
let (wallet_balance, _) = if url_path.is_empty() {
safe.wallet_get_default_balance(&wallet_url).await?
} else {
wallet_get_spendable_balance(
safe,
safeurl.xorname(),
safeurl.type_tag(),
url_path[1..].as_bytes(),
)
.await
.map_err(|_| {
Error::InvalidInput(format!(
"No spendable balance named '{}' found in Wallet: '{}'",
url_path[1..].to_string(),
wallet_url
))
})?
};
Ok(wallet_balance)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
api::{
app::test_helpers::{new_read_only_safe_instance, new_safe_instance, random_nrs_name},
common::sk_to_hex,
},
retry_loop, retry_loop_for_pattern,
};
use anyhow::{anyhow, bail, Result};
#[tokio::test]
async fn test_wallet_create() -> Result<()> {
let mut safe = new_safe_instance().await?;
let xorurl = safe.wallet_create().await?;
assert!(xorurl.starts_with("safe://"));
let _ = retry_loop!(safe.fetch(&xorurl, None));
let current_balance = safe.wallet_balance(&xorurl).await?;
assert_eq!("0.000000000", current_balance);
Ok(())
}
#[tokio::test]
async fn test_wallet_insert_and_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("12.23").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (_, keypair2) = safe.keys_create_preload_test_coins("1.53").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
let _ = retry_loop!(safe.fetch(&wallet_xorurl, None));
safe.wallet_insert(&wallet_xorurl, Some("my-first-balance"), true, &sk1_hex)
.await?;
let _ = retry_loop_for_pattern!(safe.wallet_balance(&wallet_xorurl), Ok(balance) if balance == "12.230000000")?;
safe.wallet_insert(&wallet_xorurl, Some("my-second-balance"), false, &sk2_hex)
.await?;
let _ = retry_loop_for_pattern!(safe.wallet_balance(&wallet_xorurl), Ok(balance) if balance == "13.760000000" )?;
Ok(())
}
#[tokio::test]
async fn test_wallet_insert_and_get() -> Result<()> {
let mut safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (key1_xorurl, keypair1) = safe.keys_create_preload_test_coins("12.23").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (key2_xorurl, keypair2) = safe.keys_create_preload_test_coins("1.53").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
let _ = retry_loop!(safe.fetch(&wallet_xorurl, None));
safe.wallet_insert(&wallet_xorurl, Some("my-first-balance"), true, &sk1_hex)
.await?;
safe.wallet_insert(&wallet_xorurl, Some("my-second-balance"), false, &sk2_hex)
.await?;
let wallet_balances = retry_loop_for_pattern!(safe.wallet_get(&wallet_xorurl), Ok(balances) if balances.len() == 2)?;
assert_eq!(wallet_balances["my-first-balance"].0, true);
assert_eq!(wallet_balances["my-first-balance"].1.xorurl, key1_xorurl);
assert_eq!(
wallet_balances["my-first-balance"].1.sk,
sk_to_hex(keypair1.secret_key()?)
);
assert_eq!(wallet_balances["my-second-balance"].0, false);
assert_eq!(wallet_balances["my-second-balance"].1.xorurl, key2_xorurl);
assert_eq!(
wallet_balances["my-second-balance"].1.sk,
sk_to_hex(keypair2.secret_key()?)
);
Ok(())
}
#[tokio::test]
#[ignore]
async fn test_wallet_insert_and_set_default() -> Result<()> {
let mut safe = new_safe_instance().await?;
let wallet_xorurl = safe.wallet_create().await?;
let (key1_xorurl, keypair1) = safe.keys_create_preload_test_coins("65.82").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (key2_xorurl, keypair2) = safe.keys_create_preload_test_coins("11.44").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
let _ = retry_loop!(safe.fetch(&key1_xorurl, None));
let _ = retry_loop!(safe.fetch(&key2_xorurl, None));
let _ = retry_loop!(safe.fetch(&wallet_xorurl, None));
safe.wallet_insert(&wallet_xorurl, Some("my-first-balance"), true, &sk1_hex)
.await?;
let _ = retry_loop_for_pattern!(safe.wallet_get(&wallet_xorurl), Ok(balances) if balances.len() == 1)?;
safe.wallet_insert(&wallet_xorurl, Some("my-second-balance"), true, &sk2_hex)
.await?;
let _ = retry_loop_for_pattern!(safe.wallet_get(&wallet_xorurl), Ok(balances) if balances.len() == 2)?;
let wallet_balances = safe.wallet_get(&wallet_xorurl).await?;
assert_eq!(wallet_balances["my-first-balance"].0, false);
assert_eq!(wallet_balances["my-first-balance"].1.xorurl, key1_xorurl);
assert_eq!(
wallet_balances["my-first-balance"].1.sk,
sk_to_hex(keypair1.secret_key()?)
);
assert_eq!(wallet_balances["my-second-balance"].0, true);
assert_eq!(wallet_balances["my-second-balance"].1.xorurl, key2_xorurl);
assert_eq!(
wallet_balances["my-second-balance"].1.sk,
sk_to_hex(keypair2.secret_key()?)
);
Ok(())
}
#[tokio::test]
async fn test_wallet_transfer_no_default() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let to_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair) = safe.keys_create_preload_test_coins("43523").await?;
let sk_hex = sk_to_hex(keypair.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
let _ = retry_loop!(safe.fetch(&to_wallet_xorurl, None));
safe.wallet_insert(
&to_wallet_xorurl,
Some("my-first-balance"),
true,
&sk_hex,
)
.await?;
match safe
.wallet_transfer("10", &from_wallet_xorurl, &to_wallet_xorurl)
.await
{
Err(Error::ContentError(msg)) => assert_eq!(
msg,
format!(
"No default balance found at Wallet \"{}\"",
from_wallet_xorurl
)
),
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly".to_string(),),
};
match safe
.wallet_transfer("10", &to_wallet_xorurl, &from_wallet_xorurl)
.await
{
Err(Error::ContentError(msg)) => {
assert_eq!(
msg,
format!(
"No default balance found at Wallet \"{}\"",
from_wallet_xorurl
)
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Transfer succeeded unexpectedly".to_string(),)),
}
}
#[tokio::test]
async fn test_wallet_transfer_from_zero_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("0.0").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("my-first-balance"),
true,
&sk1_hex,
)
.await?;
let (to_key_xorurl, _) = safe.keys_create_preload_test_coins("0.5").await?;
match safe
.wallet_transfer("0", &from_wallet_xorurl, &to_key_xorurl)
.await
{
Err(Error::InvalidAmount(msg)) => {
assert!(msg.contains("Cannot send zero-value transfers"))
}
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly"),
};
let to_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("0.5").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
safe.wallet_insert(
&to_wallet_xorurl,
Some("also-my-balance"),
true,
&sk2_hex,
)
.await?;
match safe
.wallet_transfer("0", &from_wallet_xorurl, &to_wallet_xorurl)
.await
{
Err(Error::InvalidAmount(msg)) => {
assert!(msg.contains("Cannot send zero-value transfers"));
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Transfer succeeded unexpectedly".to_string(),)),
}
}
#[tokio::test]
async fn test_wallet_transfer_diff_amounts() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("100.5").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("my-first-balance"),
true,
&sk1_hex,
)
.await?;
let to_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("0.5").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
safe.wallet_insert(
&to_wallet_xorurl,
Some("also-my-balance"),
true,
&sk2_hex,
)
.await?;
match safe
.wallet_transfer("100.6", &from_wallet_xorurl, &to_wallet_xorurl)
.await
{
Err(Error::NotEnoughBalance(msg)) => assert_eq!(
msg,
format!(
"Not enough balance for the transfer at Wallet \"{}\"",
from_wallet_xorurl
)
),
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly".to_string()),
};
match safe
.wallet_transfer(".06", &from_wallet_xorurl, &to_wallet_xorurl)
.await
{
Err(Error::InvalidAmount(msg)) => assert_eq!(
msg,
"Invalid safecoins amount '.06' (Can\'t parse token units)"
),
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly".to_string()),
};
match safe
.wallet_transfer("100.4", &from_wallet_xorurl, &to_wallet_xorurl)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!("0.100000000", from_current_balance);
let to_current_balance = safe.wallet_balance(&to_wallet_xorurl).await?;
assert_eq!("100.900000000", to_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_to_safekey() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("4621.45").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("my-first-balance"),
true,
&sk2_hex,
)
.await?;
let (key_xorurl, keypair3) = safe.keys_create_preload_test_coins("10.0").await?;
match safe
.wallet_transfer("523.87", &from_wallet_xorurl, &key_xorurl)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!(
"4097.580000000",
from_current_balance
);
let key_current_balance = safe.keys_balance_from_sk(keypair3.secret_key()?).await?;
assert_eq!("533.870000000", key_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_to_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("1122.98").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("my-first-balance"),
true,
&sk1_hex,
)
.await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("3232").await?;
let to_pk_hex = encode(keypair2.public_key().to_bytes());
match safe
.wallet_transfer("557.92", &from_wallet_xorurl, &to_pk_hex)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!(
"565.060000000",
from_current_balance
);
let key_current_balance = safe.keys_balance_from_sk(keypair2.secret_key()?).await?;
assert_eq!(
"3789.920000000",
key_current_balance
);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_from_safekey() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (safekey_xorurl1, _) = safe.keys_create_preload_test_coins("7").await?;
let (safekey_xorurl2, _) = safe.keys_create_preload_test_coins("0").await?;
match safe
.wallet_transfer("1", &safekey_xorurl1, &safekey_xorurl2)
.await
{
Ok(_) => Err(anyhow!(
"Transfer from SafeKey was expected to fail".to_string(),
)),
Err(Error::InvalidInput(msg)) => {
assert_eq!(
msg,
"The 'from_url' URL doesn't target a Wallet, it is: Raw (SafeKey)"
);
Ok(())
}
Err(err) => Err(anyhow!("Error is not the expected one: {:?}", err)),
}
}
#[tokio::test]
async fn test_wallet_transfer_with_nrs_urls() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("0.2").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("my-first-balance"),
true,
&sk1_hex,
)
.await?;
let from_nrs_name = random_nrs_name();
let (xorurl, _, _) = safe
.nrs_map_container_create(&from_nrs_name, &from_wallet_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let (key_xorurl, keypair3) = safe.keys_create_preload_test_coins("0.1").await?;
let to_nrs_name = random_nrs_name();
let (xorurl, _, _) = safe
.nrs_map_container_create(&to_nrs_name, &key_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let from_nrs_url = format!("safe://{}", from_nrs_name);
let to_nrs_url = format!("safe://{}", to_nrs_name);
match safe
.wallet_transfer("0.2", &from_nrs_url, &to_nrs_url)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_nrs_url).await?;
assert_eq!("0.000000000" , from_current_balance);
let key_current_balance = safe.keys_balance_from_sk(keypair3.secret_key()?).await?;
assert_eq!("0.300000000" , key_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_from_specific_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_key_xorurl1, keypair1) = safe.keys_create_preload_test_coins("100.5").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("from-first-balance"),
true,
&sk1_hex,
)
.await?;
let (_key_xorurl2, keypair2) = safe.keys_create_preload_test_coins("200.5").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
safe.wallet_insert(
&from_wallet_xorurl,
Some("from-second-balance"),
false,
&sk2_hex,
)
.await?;
let to_wallet_xorurl = safe.wallet_create().await?;
let (_key_xorurl3, keypair3) = safe.keys_create_preload_test_coins("10.5").await?;
let sk3_hex = sk_to_hex(keypair3.secret_key()?);
let _ = retry_loop!(safe.fetch(&to_wallet_xorurl, None));
safe.wallet_insert(
&to_wallet_xorurl,
Some("to-first-balance"),
true,
&sk3_hex,
)
.await?;
let mut from_wallet_spendable_balance = SafeUrl::from_url(&from_wallet_xorurl)?;
from_wallet_spendable_balance.set_path("from-second-balance");
let from_spendable_balance = from_wallet_spendable_balance.to_string();
match safe
.wallet_transfer("200.6", &from_spendable_balance, &to_wallet_xorurl)
.await
{
Err(Error::NotEnoughBalance(msg)) => assert_eq!(
msg,
format!(
"Not enough balance for the transfer at Wallet \"{}\"",
from_spendable_balance
)
),
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly".to_string()),
};
match safe
.wallet_transfer("100.3", &from_spendable_balance, &to_wallet_xorurl)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_first_current_balance = safe
.wallet_balance(&format!("{}/from-first-balance", from_wallet_xorurl))
.await?;
assert_eq!("100.500000000", from_first_current_balance);
let from_second_current_balance =
safe.wallet_balance(&from_spendable_balance).await?;
assert_eq!(
"100.200000000",
from_second_current_balance
);
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!("200.700000000" , from_current_balance);
let to_current_balance = safe.wallet_balance(&to_wallet_xorurl).await?;
assert_eq!("110.800000000" , to_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_to_specific_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = safe.wallet_create().await?;
let (_key_xorurl1, keypair1) = safe.keys_create_preload_test_coins("100.7").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("from-first-balance"),
true,
&sk1_hex,
)
.await?;
let to_wallet_xorurl = safe.wallet_create().await?;
let (_key_xorurl2, keypair2) = safe.keys_create_preload_test_coins("10.2").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
let _ = retry_loop!(safe.fetch(&to_wallet_xorurl, None));
safe.wallet_insert(
&to_wallet_xorurl,
Some("to-first-balance"),
true,
&sk2_hex,
)
.await?;
let (_key_xorurl3, keypair3) = safe.keys_create_preload_test_coins("20.2").await?;
let sk3_hex = sk_to_hex(keypair3.secret_key()?);
safe.wallet_insert(
&to_wallet_xorurl,
Some("to-second-balance"),
false,
&sk3_hex,
)
.await?;
let mut to_wallet_spendable_balance = SafeUrl::from_url(&to_wallet_xorurl)?;
to_wallet_spendable_balance.set_path("to-second-balance");
let to_spendable_balance = to_wallet_spendable_balance.to_string();
match safe
.wallet_transfer("100.5", &from_wallet_xorurl, &to_spendable_balance)
.await
{
Err(msg) => bail!("Transfer was expected to succeed: {}", msg),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!("0.200000000" , from_current_balance);
let to_first_current_balance = safe
.wallet_balance(&format!("{}/to-first-balance", to_wallet_xorurl))
.await?;
assert_eq!("10.200000000", to_first_current_balance);
let to_second_current_balance = safe.wallet_balance(&to_spendable_balance).await?;
assert_eq!(
"120.700000000",
to_second_current_balance
);
let to_current_balance = safe.wallet_balance(&to_wallet_xorurl).await?;
assert_eq!("130.900000000", to_current_balance);
}
};
let to_wallet_nrsurl = random_nrs_name();
let (xorurl, _, _) = safe
.nrs_map_container_create(&to_wallet_nrsurl, &to_wallet_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let to_first_current_balance = safe
.wallet_balance(&format!("{}/to-first-balance", to_wallet_nrsurl))
.await?;
assert_eq!("10.200000000", to_first_current_balance);
let to_second_current_balance = safe
.wallet_balance(&format!("{}/to-second-balance", to_wallet_nrsurl))
.await?;
assert_eq!("120.700000000", to_second_current_balance);
Ok(())
}
#[tokio::test]
async fn test_wallet_transfer_specific_balances_with_nrs_urls() -> Result<()> {
let mut safe = new_safe_instance().await?;
let from_wallet_xorurl = {
let from_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("10.1").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
let _ = retry_loop!(safe.fetch(&from_wallet_xorurl, None));
safe.wallet_insert(
&from_wallet_xorurl,
Some("from-first-balance"),
true,
&sk1_hex,
)
.await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("20.2").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
safe.wallet_insert(
&from_wallet_xorurl,
Some("from-second-balance"),
false,
&sk2_hex,
)
.await?;
from_wallet_xorurl
};
let to_wallet_xorurl = {
let to_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair3) = safe.keys_create_preload_test_coins("30.3").await?;
let sk3_hex = sk_to_hex(keypair3.secret_key()?);
let _ = retry_loop!(safe.fetch(&to_wallet_xorurl, None));
safe.wallet_insert(
&to_wallet_xorurl,
Some("to-first-balance"),
true,
&sk3_hex,
)
.await?;
let (_, keypair4) = safe.keys_create_preload_test_coins("40.4").await?;
let sk4_hex = sk_to_hex(keypair4.secret_key()?);
safe.wallet_insert(
&to_wallet_xorurl,
Some("to-second-balance"),
false,
&sk4_hex,
)
.await?;
to_wallet_xorurl
};
let from_nrs_name = random_nrs_name();
let (xorurl, _, _) = safe
.nrs_map_container_create(&from_nrs_name, &from_wallet_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let to_nrs_name = random_nrs_name();
let (xorurl, _, _) = safe
.nrs_map_container_create(&to_nrs_name, &to_wallet_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let from_nrs_url = format!("safe://{}", from_nrs_name);
let to_nrs_url = format!("safe://{}", to_nrs_name);
let from_spendable_balance = format!("{}/from-second-balance", from_nrs_url);
let to_spendable_balance = format!("{}/to-second-balance", to_nrs_url);
match safe
.wallet_transfer("5.8", &from_spendable_balance, &to_spendable_balance)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance = safe.wallet_balance(&from_wallet_xorurl).await?;
assert_eq!(
"24.500000000",
from_current_balance
);
let from_first_current_balance = safe
.wallet_balance(&format!("{}/from-first-balance", from_wallet_xorurl))
.await?;
assert_eq!("10.100000000", from_first_current_balance);
let from_second_current_balance =
safe.wallet_balance(&from_spendable_balance).await?;
assert_eq!(
"14.400000000",
from_second_current_balance
);
let to_current_balance = safe.wallet_balance(&to_wallet_xorurl).await?;
assert_eq!(
"76.500000000",
to_current_balance
);
let to_first_current_balance = safe
.wallet_balance(&format!("{}/to-first-balance", to_wallet_xorurl))
.await?;
assert_eq!("30.300000000", to_first_current_balance);
let to_second_current_balance = safe.wallet_balance(&to_spendable_balance).await?;
assert_eq!(
"46.200000000",
to_second_current_balance
);
Ok(())
}
}
}
#[tokio::test]
async fn test_wallet_transfer_from_not_owned_wallet() -> Result<()> {
let mut safe = new_safe_instance().await?;
let source_wallet_xorurl = safe.wallet_create().await?;
let (key_xorurl, keypair) = safe.keys_create_preload_test_coins("100.5").await?;
let sk_hex = sk_to_hex(keypair.secret_key()?);
let _ = retry_loop!(safe.fetch(&key_xorurl, None));
let _ = retry_loop!(safe.fetch(&source_wallet_xorurl, None));
safe.wallet_insert(
&source_wallet_xorurl,
Some("my-first-balance"),
true,
&sk_hex,
)
.await?;
let _ = retry_loop_for_pattern!(safe.wallet_get(&source_wallet_xorurl), Ok(balances) if balances.len() == 1)?;
let mut read_only_safe = new_read_only_safe_instance().await?;
match read_only_safe
.wallet_transfer("0.2", &source_wallet_xorurl, &key_xorurl)
.await
{
Err(Error::AccessDenied(msg)) => {
assert_eq!(
msg,
format!(
"Couldn't read source Wallet for the transfer at \"{}\"",
source_wallet_xorurl
)
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(_) => Err(anyhow!("Transfer succeeded unexpectedly".to_string(),)),
}
}
}