use super::{
common::ed_sk_from_hex,
helpers::{parse_coins_amount, pk_from_hex},
safeurl::{SafeContentType, SafeDataType, SafeUrl},
Safe,
};
use crate::{Error, Result};
use hex::encode;
use rand::rngs::OsRng;
use sn_data_types::{Keypair, SecretKey, Token};
use xor_name::XorName;
impl Safe {
pub fn generate_random_ed_keypair(&self) -> Keypair {
let mut rng = OsRng;
Keypair::new_ed25519(&mut rng)
}
pub async fn keys_create_and_preload(
&mut self,
preload_amount: &str,
) -> Result<(String, Keypair)> {
let amount = parse_coins_amount(preload_amount)?;
let new_keypair = self.generate_random_ed_keypair();
let _ = self
.safe_client
.safecoin_transfer_to_pk(None, new_keypair.public_key(), amount)
.await?;
let xorname = XorName::from(new_keypair.public_key());
let xorurl = SafeUrl::encode_safekey(xorname, self.xorurl_base)?;
Ok((xorurl, new_keypair))
}
pub async fn keys_create_and_preload_from_sk_string(
&mut self,
from: &str,
preload_amount: &str,
) -> Result<(String, Keypair)> {
let from_keypair = match ed_sk_from_hex(&from) {
Ok(sk) => Keypair::from(sk),
Err(_) => return Err(Error::InvalidInput(
"The source of funds needs to be an Ed25519 secret key. The secret key provided is invalid"
.to_string(),
)),
};
let amount = parse_coins_amount(&preload_amount)?;
let new_keypair = self.generate_random_ed_keypair();
let _ = self
.safe_client
.safecoin_transfer_to_pk(Some(from_keypair), new_keypair.public_key(), amount)
.await?;
let xorname = XorName::from(new_keypair.public_key());
let xorurl = SafeUrl::encode_safekey(xorname, self.xorurl_base)?;
Ok((xorurl, new_keypair))
}
#[cfg(feature = "simulated-payouts")]
pub async fn keys_create_preload_test_coins(
&mut self,
preload_amount: &str,
) -> Result<(String, Keypair)> {
let amount = parse_coins_amount(preload_amount)?;
let keypair = self.generate_random_ed_keypair();
self.safe_client
.trigger_simulated_farming_payout(amount, Some(keypair.clone()))
.await?;
let xorname = XorName::from(keypair.public_key());
let xorurl = SafeUrl::encode_safekey(xorname, self.xorurl_base)?;
Ok((xorurl, keypair))
}
pub async fn keys_balance_from_sk(&self, secret_key: SecretKey) -> Result<String> {
let keypair = match secret_key {
SecretKey::Ed25519(sk) => Keypair::from(sk),
SecretKey::BlsShare(_) => {
return Err(Error::InvalidInput(
"Cannot convert from BlsShare key to a Keypair".to_string(),
))
}
};
let balance = self.safe_client.read_balance_from_keypair(keypair).await?;
Ok(balance.to_string())
}
pub async fn keys_balance_from_url(
&mut self,
url: &str,
secret_key: SecretKey,
) -> Result<String> {
self.validate_sk_for_url(&secret_key, url).await?;
self.keys_balance_from_sk(secret_key).await
}
pub async fn validate_sk_for_url(
&mut self,
secret_key: &SecretKey,
url: &str,
) -> Result<String> {
let derived_xorname = match secret_key {
SecretKey::Ed25519(sk) => {
let pk: ed25519_dalek::PublicKey = sk.into();
XorName(pk.to_bytes())
}
_ => {
return Err(Error::InvalidInput(
"Cannot form a keypair from a BlsKeyShare at this time.".to_string(),
))
}
};
let (safeurl, _) = self.parse_and_resolve_url(url).await?;
if safeurl.xorname() != derived_xorname {
Err(Error::InvalidInput(
"The URL doesn't correspond to the public key derived from the provided secret key"
.to_string(),
))
} else {
Ok(encode(&derived_xorname))
}
}
pub async fn keys_transfer(
&mut self,
amount: &str,
from_sk_str: Option<&str>,
to: &str,
) -> Result<u64> {
let amount_coins = parse_coins_amount(amount)?;
let from = match &from_sk_str {
Some(sk) => Some(Keypair::from(ed_sk_from_hex(sk)?)),
None => None,
};
let result = if to.starts_with("safe://") {
self.transfer_to_url(from, to, amount_coins).await
} else {
match pk_from_hex(to) {
Ok(to_pk) => {
self.safe_client
.safecoin_transfer_to_pk(from, to_pk, amount_coins)
.await
}
Err(_) => {
self.transfer_to_url(from, to, amount_coins).await
}
}
};
match result {
Err(Error::NotEnoughBalance(_)) => {
let msg = if from_sk_str.is_some() {
"Not enough balance for the transfer at provided source SafeKey".to_string()
} else {
"Not enough balance for the transfer".to_string()
};
Err(Error::NotEnoughBalance(msg))
}
Err(other_error) => Err(other_error),
Ok(id) => Ok(id),
}
}
async fn transfer_to_url(
&mut self,
from: Option<Keypair>,
to: &str,
amount_coins: Token,
) -> Result<u64> {
let (to_safe_url, _) = self
.parse_and_resolve_url(to)
.await
.map_err(|_| Error::InvalidInput(format!("Failed to parse the 'to' URL: {}", to)))?;
let to_xorname = if to_safe_url.content_type() == SafeContentType::Wallet {
let (to_balance, _) = self
.wallet_get_default_balance(&to_safe_url.to_string())
.await?;
SafeUrl::from_url(&to_balance.xorurl)?.xorname()
} else if to_safe_url.content_type() == SafeContentType::Raw
&& to_safe_url.data_type() == SafeDataType::SafeKey
{
to_safe_url.xorname()
} else {
return Err(Error::InvalidInput(format!(
"The destination URL doesn't target a SafeKey or Wallet, target is: {:?} ({})",
to_safe_url.content_type(),
to_safe_url.data_type()
)));
};
self.safe_client
.safecoin_transfer_to_xorname(from, to_xorname, amount_coins)
.await
}
}
#[cfg(all(test, feature = "simulated-payouts"))]
mod tests {
use super::*;
use crate::{
api::{
app::test_helpers::{new_safe_instance, random_nrs_name},
common::sk_to_hex,
},
retry_loop,
};
use anyhow::{anyhow, bail, Result};
#[tokio::test]
async fn test_keys_create_preload_test_coins() -> Result<()> {
let mut safe = new_safe_instance().await?;
let _ = safe.keys_create_preload_test_coins("12.23").await?;
Ok(())
}
#[tokio::test]
async fn test_keys_create_and_preload_from_sk_string() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, from_keypair) = safe.keys_create_preload_test_coins("543.2312").await?;
let from_sk_hex = sk_to_hex(from_keypair.secret_key()?);
let preload_amount = "1.800000000";
let (_, keypair) = safe
.keys_create_and_preload_from_sk_string(&from_sk_hex, preload_amount)
.await?;
let balance = safe.keys_balance_from_sk(keypair.secret_key()?).await?;
assert_eq!(balance, preload_amount);
Ok(())
}
#[tokio::test]
async fn test_keys_create_preload_invalid_amounts() -> Result<()> {
let mut safe = new_safe_instance().await?;
match safe.keys_create_preload_test_coins(".45").await {
Ok(_) => {
bail!("Key with test-coins was created unexpectedly".to_string(),)
}
Err(Error::InvalidAmount(msg)) => assert_eq!(
msg,
"Invalid safecoins amount '.45' (Can\'t parse token units)".to_string()
),
other => bail!("Error returned is not the expected one: {:?}", other),
};
let (_, keypair) = safe.keys_create_preload_test_coins("12").await?;
let mut sk_hex = sk_to_hex(keypair.secret_key()?);
match safe
.keys_create_and_preload_from_sk_string(&sk_hex, ".003")
.await
{
Ok(_) => {
bail!("Key was created unexpectedly".to_string(),)
}
Err(Error::InvalidAmount(msg)) => assert_eq!(
msg,
"Invalid safecoins amount '.003' (Can\'t parse token units)".to_string()
),
other => bail!("Error returned is not the expected one: {:?}", other),
};
sk_hex.replace_range(..6, "ababab");
match safe
.keys_create_and_preload_from_sk_string(&sk_hex, ".003")
.await
{
Ok(_) => {
bail!("Key was created unexpectedly".to_string(),)
}
Err(Error::InvalidAmount(msg)) => assert_eq!(
msg,
"Invalid safecoins amount '.003' (Can\'t parse token units)".to_string()
),
other => bail!("Error returned is not the expected one: {:?}", other),
};
let amount = "12.000000001";
match safe
.keys_create_and_preload_from_sk_string(&sk_hex, amount)
.await
{
Ok(_) => Err(anyhow!("Key was created unexpectedly".to_string(),)),
Err(Error::NotEnoughBalance(msg)) => {
assert_eq!(
msg,
format!(
"Not enough balance at 'source' for the operation: {}",
amount
)
);
Ok(())
}
other => Err(anyhow!(
"Error returned is not the expected one: {:?}",
other
)),
}
}
#[tokio::test]
async fn test_keys_create_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, from_keypair) = safe.keys_create_preload_test_coins("1.1").await?;
let from_sk_hex = sk_to_hex(from_keypair.secret_key()?);
let _ = safe
.keys_create_and_preload_from_sk_string(&from_sk_hex, "0.1")
.await?;
Ok(())
}
#[tokio::test]
async fn test_keys_test_coins_balance_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let preload_amount = "1.154200000";
let (_, keypair) = safe.keys_create_preload_test_coins(preload_amount).await?;
let current_balance = safe.keys_balance_from_sk(keypair.secret_key()?).await?;
assert_eq!(preload_amount, current_balance);
Ok(())
}
#[tokio::test]
async fn test_keys_test_coins_balance_xorurl() -> Result<()> {
let mut safe = new_safe_instance().await?;
let preload_amount = "0.243000000";
let (xorurl, keypair) = safe.keys_create_preload_test_coins(preload_amount).await?;
let current_balance = safe
.keys_balance_from_url(&xorurl, keypair.secret_key()?)
.await?;
assert_eq!(preload_amount, current_balance);
Ok(())
}
#[tokio::test]
async fn test_keys_test_coins_balance_wrong_url() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, keypair) = safe.keys_create_preload_test_coins("0").await?;
let invalid_xorurl = "safe://this-is-not-a-valid-xor-url";
let current_balance = safe
.keys_balance_from_url(&invalid_xorurl, keypair.secret_key()?)
.await;
match current_balance {
Err(Error::ContentNotFound(msg)) => {
assert!(msg.contains(&format!("Content not found at {}", invalid_xorurl)));
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(balance) => Err(anyhow!("Unexpected balance obtained: {:?}", balance)),
}
}
#[tokio::test]
async fn test_keys_test_coins_balance_wrong_location() -> Result<()> {
let mut safe = new_safe_instance().await?;
let amount = "35312.000000000";
let (xorurl, keypair) = safe.keys_create_preload_test_coins(amount).await?;
let current_balance = safe
.keys_balance_from_url(&xorurl, keypair.secret_key()?)
.await?;
assert_eq!(amount, current_balance);
let (other_kp_xorurl, _) = safe.keys_create_preload_test_coins("0").await?;
let current_balance = safe
.keys_balance_from_url(&other_kp_xorurl, keypair.secret_key()?)
.await;
match current_balance {
Err(Error::InvalidInput(msg)) => {
assert!(msg.contains(
"The URL doesn't correspond to the public key derived from the provided secret key"
));
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(balance) => Err(anyhow!("Unexpected balance obtained: {:?}", balance)),
}
}
#[tokio::test]
async fn test_keys_test_coins_balance_wrong_sk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (xorurl, _) = safe.keys_create_preload_test_coins("0").await?;
let mut rng = OsRng;
let sk = Keypair::new_ed25519(&mut rng).secret_key()?;
let current_balance = safe.keys_balance_from_url(&xorurl, sk).await;
match current_balance {
Err(Error::InvalidInput(msg)) => {
assert_eq!(msg, "The URL doesn't correspond to the public key derived from the provided secret key");
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected: {:?}", err)),
Ok(balance) => Err(anyhow!("Unexpected balance obtained: {:?}", balance)),
}
}
#[tokio::test]
async fn test_keys_balance_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let preload_amount = "1743.234";
let (_, from_keypair) = safe.keys_create_preload_test_coins(preload_amount).await?;
let from_sk_hex = sk_to_hex(from_keypair.secret_key()?);
let amount = "1740.000000000";
let (_, to_keypair) = safe
.keys_create_and_preload_from_sk_string(&from_sk_hex, amount)
.await?;
let from_current_balance = safe
.keys_balance_from_sk(from_keypair.secret_key()?)
.await?;
assert_eq!(
"3.234000000",
from_current_balance
);
let to_current_balance = safe.keys_balance_from_sk(to_keypair.secret_key()?).await?;
assert_eq!(amount, to_current_balance);
Ok(())
}
#[tokio::test]
async fn test_keys_balance_xorname() -> Result<()> {
let mut safe = new_safe_instance().await?;
let preload_amount = "435.34";
let (from_xorname, from_keypair) =
safe.keys_create_preload_test_coins(preload_amount).await?;
let from_sk_hex = sk_to_hex(from_keypair.secret_key()?);
let amount = "35.300000000";
let (to_xorname, to_keypair) = safe
.keys_create_and_preload_from_sk_string(&from_sk_hex, amount)
.await?;
let from_current_balance = safe
.keys_balance_from_url(&from_xorname, from_keypair.secret_key()?)
.await?;
assert_eq!(
"400.040000000",
from_current_balance
);
let to_current_balance = safe
.keys_balance_from_url(&to_xorname, to_keypair.secret_key()?)
.await?;
assert_eq!(amount, to_current_balance);
Ok(())
}
#[tokio::test]
async fn test_keys_validate_sk_for_url() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (xorurl, keypair) = safe.keys_create_preload_test_coins("23.22").await?;
let pk = safe
.validate_sk_for_url(&keypair.secret_key()?, &xorurl)
.await?;
assert_eq!(pk, encode(keypair.public_key().to_bytes()));
Ok(())
}
#[tokio::test]
async fn test_keys_transfer_from_zero_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("0.0").await?;
let from_sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (to_safekey_xorurl, _keypair2) = safe.keys_create_preload_test_coins("0.5").await?;
match safe
.keys_transfer("0", Some(&from_sk1_hex), &to_safekey_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_keys_transfer_diff_amounts() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (safekey1_xorurl, keypair1) = safe.keys_create_preload_test_coins("0.5").await?;
let from_sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (safekey2_xorurl, keypair2) = safe.keys_create_preload_test_coins("100.5").await?;
let from_sk2_hex = sk_to_hex(keypair2.secret_key()?);
match safe
.keys_transfer("100.6", Some(&from_sk1_hex), &safekey2_xorurl)
.await
{
Err(Error::NotEnoughBalance(msg)) => assert_eq!(
msg,
"Not enough balance for the transfer at provided source SafeKey".to_string()
),
Err(err) => {
bail!("Error returned is not the expected: {:?}", err)
}
Ok(_) => {
bail!("Transfer succeeded unexpectedly".to_string(),)
}
};
match safe
.keys_transfer(".06", Some(&from_sk2_hex), &safekey2_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.keys_transfer(
"0.0000000009",
Some(&from_sk2_hex),
&safekey2_xorurl,
).await {
Err(Error::InvalidAmount(msg)) => assert_eq!(msg, "Invalid safecoins amount '0.0000000009', the minimum possible amount is one nano coin (0.000000001)"),
Err(err) => bail!("Error returned is not the expected: {:?}", err),
Ok(_) => bail!("Transfer succeeded unexpectedly".to_string()),
};
match safe
.keys_transfer("100.4", Some(&from_sk2_hex), &safekey1_xorurl)
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance =
safe.keys_balance_from_sk(keypair2.secret_key()?).await?;
assert_eq!("0.100000000", from_current_balance);
let to_current_balance = safe.keys_balance_from_sk(keypair1.secret_key()?).await?;
assert_eq!("100.900000000", to_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_keys_transfer_to_wallet() -> Result<()> {
let mut safe = new_safe_instance().await?;
let to_wallet_xorurl = safe.wallet_create().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("10.0").await?;
let sk1_hex = sk_to_hex(keypair1.secret_key()?);
safe.wallet_insert(
&to_wallet_xorurl,
Some("my-first-balance"),
true,
&sk1_hex,
)
.await?;
let (_, keypair2) = safe.keys_create_preload_test_coins("4621.45").await?;
let sk2_hex = sk_to_hex(keypair2.secret_key()?);
match safe
.keys_transfer("523.87", Some(&sk2_hex), &to_wallet_xorurl.clone())
.await
{
Err(msg) => Err(anyhow!("Transfer was expected to succeed: {}", msg)),
Ok(_) => {
let from_current_balance =
safe.keys_balance_from_sk(keypair2.secret_key()?).await?;
assert_eq!(
"4097.580000000",
from_current_balance
);
let wallet_current_balance = safe.wallet_balance(&to_wallet_xorurl).await?;
assert_eq!("533.870000000", wallet_current_balance);
Ok(())
}
}
}
#[tokio::test]
async fn test_keys_transfer_to_nrs_urls() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("0.2").await?;
let from_sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (to_safekey_xorurl, keypair2) = 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, &to_safekey_xorurl, false, true, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let _ = safe
.keys_transfer(
"0.2",
Some(&from_sk1_hex),
&format!("safe://{}", to_nrs_name),
)
.await?;
let from_current_balance = safe.keys_balance_from_sk(keypair1.secret_key()?).await?;
assert_eq!("0.000000000" , from_current_balance);
let to_current_balance = safe.keys_balance_from_sk(keypair2.secret_key()?).await?;
assert_eq!("0.300000000" , to_current_balance);
Ok(())
}
#[tokio::test]
async fn test_keys_transfer_to_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, keypair1) = safe.keys_create_preload_test_coins("0.136").await?;
let from_sk1_hex = sk_to_hex(keypair1.secret_key()?);
let (_, keypair2) = safe.keys_create_preload_test_coins("0.73").await?;
let to_pk2_hex = encode(keypair2.public_key().to_bytes());
let _ = safe
.keys_transfer("0.111", Some(&from_sk1_hex), &to_pk2_hex)
.await?;
let from_current_balance = safe.keys_balance_from_sk(keypair1.secret_key()?).await?;
assert_eq!("0.025000000" , from_current_balance);
let to_current_balance = safe.keys_balance_from_sk(keypair2.secret_key()?).await?;
assert_eq!("0.841000000" , to_current_balance);
Ok(())
}
}