use super::{
common::ed_sk_from_hex,
helpers::{parse_coins_amount, pk_from_hex, pk_to_hex},
xorurl::XorUrlEncoder,
xorurl::{SafeContentType, SafeDataType},
Safe,
};
use crate::{Error, Result};
use rand::rngs::OsRng;
use sn_data_types::{Keypair, SecretKey};
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 = XorUrlEncoder::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_sk = match ed_sk_from_hex(&from) {
Ok(sk) => 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 from_keypair = Keypair::from(from_sk);
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 = XorUrlEncoder::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 = XorUrlEncoder::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) => {
let bytes = sk.to_bytes();
let secret_key = ed25519_dalek::SecretKey::from_bytes(&bytes).map_err(|err| {
Error::InvalidInput(format!("Error parsing SecretKey bytes: {}", err))
})?;
Keypair::from(secret_key)
}
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 keypair = match secret_key {
SecretKey::Ed25519(sk) => {
let bytes = sk.to_bytes();
let secret_key = ed25519_dalek::SecretKey::from_bytes(&bytes).map_err(|err| {
Error::InvalidInput(format!("Error parsing SecretKey bytes: {}", err))
})?;
Keypair::from(secret_key)
}
_ => {
return Err(Error::InvalidInput(
"Cannot form a keypair from a BlsKeyShare at this time.".to_string(),
))
}
};
let (xorurl_encoder, _) = self.parse_and_resolve_url(url).await?;
let public_key = keypair.public_key();
let derived_xorname = XorName::from(public_key);
if xorurl_encoder.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(pk_to_hex(&public_key))
}
}
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://") {
let (to_xorurl_encoder, _) = self.parse_and_resolve_url(to).await.map_err(|_| {
Error::InvalidInput(format!("Failed to parse the 'to' URL: {}", to))
})?;
let to_xorname = if to_xorurl_encoder.content_type() == SafeContentType::Wallet {
let (to_balance, _) = self
.wallet_get_default_balance(&to_xorurl_encoder.to_string())
.await?;
XorUrlEncoder::from_url(&to_balance.xorurl)?.xorname()
} else if to_xorurl_encoder.content_type() == SafeContentType::Raw
&& to_xorurl_encoder.data_type() == SafeDataType::SafeKey
{
to_xorurl_encoder.xorname()
} else {
return Err(Error::InvalidInput(format!(
"The destination URL doesn't target a SafeKey or Wallet, target is: {:?} ({})",
to_xorurl_encoder.content_type(),
to_xorurl_encoder.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(_)) => {
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),
}
}
}
#[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,
};
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_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, pk_to_hex(&keypair.public_key()));
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::NetDataError(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_nrsurl = random_nrs_name();
let _ = safe
.nrs_map_container_create(&to_nrsurl, &to_safekey_xorurl, false, true, false)
.await?;
let _ = safe
.keys_transfer("0.2", Some(&from_sk1_hex), &to_nrsurl)
.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 = pk_to_hex(&keypair2.public_key());
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(())
}
}