use super::{
common::sk_from_hex,
helpers::{parse_coins_amount, pk_from_hex, pk_to_hex, xorname_from_pk, KeyPair},
xorurl::{SafeContentType, SafeDataType},
Safe, SafeApp,
};
use crate::{
xorurl::{XorUrl, XorUrlEncoder},
Error, Result,
};
use rand_core::RngCore;
use safe_nd::{Coins, XorName};
use serde::{Deserialize, Serialize};
use threshold_crypto::{PublicKey, SecretKey};
#[derive(Clone, Serialize, Deserialize)]
pub struct BlsKeyPair {
pub pk: String,
pub sk: String,
}
impl Safe {
pub fn keypair(&self) -> Result<BlsKeyPair> {
let key_pair = KeyPair::random();
let (pk, sk) = key_pair.to_hex_key_pair()?;
Ok(BlsKeyPair { pk, sk })
}
async fn create_coin_balance(
&mut self,
from_sk: Option<SecretKey>,
to_pk: PublicKey,
amount: Coins,
) -> Result<XorName> {
match self.safe_app.create_balance(from_sk, to_pk, amount).await {
Err(Error::InvalidAmount(_)) => Err(Error::InvalidAmount(format!(
"The amount '{}' specified for the transfer is invalid",
amount
))),
Err(Error::NotEnoughBalance(_)) => Err(Error::NotEnoughBalance(
"Not enough balance at 'source' for the operation".to_string(),
)),
Err(other_error) => Err(Error::Unexpected(format!(
"Unexpected error when attempting to create Key: {}",
other_error
))),
Ok(xorname) => Ok(xorname),
}
}
pub async fn keys_create(
&mut self,
from: Option<&str>,
preload_amount: Option<&str>,
pk: Option<&str>,
) -> Result<(XorUrl, Option<BlsKeyPair>)> {
let from_sk = match from {
Some(sk) => match sk_from_hex(&sk) {
Ok(sk) => Some(sk),
Err(_) => return Err(Error::InvalidInput("The source of funds needs to be a secret key. The secret key provided is invalid".to_string())),
},
None => None,
};
let amount = parse_coins_amount(&preload_amount.unwrap_or_else(|| "0.0"))?;
let (xorname, key_pair) = match pk {
Some(pk_str) => {
let pk = pk_from_hex(&pk_str)?;
let xorname = self.create_coin_balance(from_sk, pk, amount).await?;
(xorname, None)
}
None => {
let key_pair = KeyPair::random();
let (pk, sk) = key_pair.to_hex_key_pair()?;
let xorname = self
.create_coin_balance(from_sk, key_pair.pk, amount)
.await?;
(xorname, Some(BlsKeyPair { pk, sk }))
}
};
let xorurl = XorUrlEncoder::encode_safekey(xorname, self.xorurl_base)?;
Ok((xorurl, key_pair))
}
pub async fn keys_create_preload_test_coins(
&mut self,
preload_amount: &str,
) -> Result<(XorUrl, Option<BlsKeyPair>)> {
let amount = parse_coins_amount(preload_amount)?;
let key_pair = KeyPair::random();
let xorname = self
.safe_app
.allocate_test_coins(key_pair.sk.clone(), amount)
.await?;
let (pk, sk) = key_pair.to_hex_key_pair()?;
let key_pair = Some(BlsKeyPair { pk, sk });
let xorurl = XorUrlEncoder::encode_safekey(xorname, self.xorurl_base)?;
Ok((xorurl, key_pair))
}
pub async fn keys_balance_from_sk(&self, sk: &str) -> Result<String> {
let secret_key = sk_from_hex(sk)?;
let coins = self
.safe_app
.get_balance_from_sk(secret_key)
.await
.map_err(|_| {
Error::ContentNotFound("No SafeKey found at specified location".to_string())
})?;
Ok(coins.to_string())
}
pub async fn keys_balance_from_url(&self, url: &str, sk: &str) -> Result<String> {
self.validate_sk_for_url(sk, url).await?;
self.keys_balance_from_sk(sk).await
}
pub async fn validate_sk_for_url(&self, sk: &str, url: &str) -> Result<String> {
let secret_key: SecretKey = sk_from_hex(sk)
.map_err(|_| Error::InvalidInput("Invalid secret key provided".to_string()))?;
let (xorurl_encoder, _) = self.parse_and_resolve_url(url).await?;
let public_key = secret_key.public_key();
let derived_xorname = xorname_from_pk(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: Option<&str>,
to_url: &str,
tx_id: Option<u64>,
) -> Result<u64> {
let amount_coins = parse_coins_amount(amount)?;
let (to_xorurl_encoder, _) = self.parse_and_resolve_url(to_url).await?;
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()
)));
};
let tx_id = tx_id.unwrap_or_else(|| rand::thread_rng().next_u64());
let from = match &from_sk {
Some(sk) => Some(sk_from_hex(sk)?),
None => None,
};
match self
.safe_app
.safecoin_transfer_to_xorname(from, to_xorname, tx_id, amount_coins)
.await
{
Err(Error::InvalidAmount(_)) => Err(Error::InvalidAmount(format!(
"The amount '{}' specified for the transfer is invalid",
amount
))),
Err(Error::NotEnoughBalance(_)) => {
let msg = if from_sk.is_some() {
"Not enough balance for the transfer at provided source SafeKey".to_string()
} else {
"Not enough balance for the transfer at Account's default SafeKey".to_string()
};
Err(Error::NotEnoughBalance(msg))
}
Err(other_error) => Err(Error::Unexpected(format!(
"Unexpected error when attempting to transfer: {}",
other_error
))),
Ok(tx) => Ok(tx.id),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::app::test_helpers::{new_safe_instance, random_nrs_name, unwrap_key_pair};
#[tokio::test]
async fn test_keys_create_preload_test_coins() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (xorurl, key_pair) = safe.keys_create_preload_test_coins("12.23").await?;
assert!(xorurl.starts_with("safe://"));
assert!(key_pair.is_some());
Ok(())
}
#[tokio::test]
async fn test_keys_create() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, from_key_pair) = safe.keys_create_preload_test_coins("23.23").await?;
let (xorurl, key_pair) = safe
.keys_create(Some(&unwrap_key_pair(from_key_pair)?.sk), None, None)
.await?;
assert!(xorurl.starts_with("safe://"));
assert!(key_pair.is_some());
Ok(())
}
#[tokio::test]
async fn test_keys_create_preload() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, from_key_pair) = safe.keys_create_preload_test_coins("543.2312").await?;
let preload_amount = "1.800000000";
let (xorurl, key_pair) = safe
.keys_create(
Some(&unwrap_key_pair(from_key_pair)?.sk),
Some(preload_amount),
None,
)
.await?;
assert!(xorurl.starts_with("safe://"));
match key_pair {
None => Err(Error::Unexpected(
"Key pair was not generated as it was expected".to_string(),
)),
Some(kp) => {
let balance = safe.keys_balance_from_sk(&kp.sk).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 {
Err(err) => assert_eq!(
err,
Error::InvalidAmount(
"Invalid safecoins amount '.45' (Can\'t parse coin units)".to_string()
)
),
Ok(_) => {
return Err(Error::Unexpected(
"Key with test-coins was created unexpectedly".to_string(),
))
}
};
let (_, kp) = safe.keys_create_preload_test_coins("12").await?;
let mut key_pair = unwrap_key_pair(kp.clone())?;
match safe
.keys_create(Some(&key_pair.sk), Some(".003"), None)
.await
{
Err(err) => assert_eq!(
err,
Error::InvalidAmount(
"Invalid safecoins amount '.003' (Can\'t parse coin units)".to_string()
)
),
Ok(_) => {
return Err(Error::Unexpected(
"Key was created unexpectedly".to_string(),
))
}
};
key_pair.sk.replace_range(..6, "ababab");
match safe
.keys_create(Some(&key_pair.sk), Some(".003"), None)
.await
{
Err(err) => assert_eq!(
err,
Error::InvalidAmount(
"Invalid safecoins amount '.003' (Can\'t parse coin units)".to_string()
)
),
Ok(_) => {
return Err(Error::Unexpected(
"Key was created unexpectedly".to_string(),
))
}
};
match safe
.keys_create(Some(&unwrap_key_pair(kp)?.sk), Some("12.000000001"), None)
.await
{
Err(err) => {
assert_eq!(
err,
Error::NotEnoughBalance(
"Not enough balance at 'source' for the operation".to_string()
)
);
Ok(())
}
Ok(_) => Err(Error::Unexpected(
"Key was created unexpectedly".to_string(),
)),
}
}
#[tokio::test]
async fn test_keys_create_pk() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_, from_key_pair) = safe.keys_create_preload_test_coins("1.1").await?;
let pk = pk_to_hex(&SecretKey::random().public_key());
let (xorurl, key_pair) = safe
.keys_create(Some(&unwrap_key_pair(from_key_pair)?.sk), None, Some(&pk))
.await?;
assert!(xorurl.starts_with("safe://"));
assert!(key_pair.is_none());
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 (_, key_pair) = safe.keys_create_preload_test_coins(preload_amount).await?;
let current_balance = safe
.keys_balance_from_sk(&unwrap_key_pair(key_pair)?.sk)
.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, key_pair) = safe.keys_create_preload_test_coins(preload_amount).await?;
let current_balance = safe
.keys_balance_from_url(&xorurl, &unwrap_key_pair(key_pair)?.sk)
.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 (_xorurl, key_pair) = 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, &unwrap_key_pair(key_pair)?.sk)
.await;
match current_balance {
Err(Error::ContentNotFound(msg)) => {
assert!(msg.contains(&format!("Content not found at {}", invalid_xorurl)));
Ok(())
}
Err(err) => Err(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
))),
Ok(balance) => Err(Error::Unexpected(format!(
"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, kp) = safe.keys_create_preload_test_coins(amount).await?;
let key_pair = unwrap_key_pair(kp)?;
let current_balance = safe.keys_balance_from_url(&xorurl, &key_pair.sk).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, &key_pair.sk)
.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(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
))),
Ok(balance) => Err(Error::Unexpected(format!(
"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, kp) = safe.keys_create_preload_test_coins("0").await?;
let mut sk = unwrap_key_pair(kp)?.sk;
sk.replace_range(..6, "ababab");
let current_balance = safe.keys_balance_from_sk(&sk).await;
match current_balance {
Err(Error::ContentNotFound(msg)) => {
assert!(msg.contains("No SafeKey found at specified location"));
Ok(())
}
Err(err) => Err(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
))),
Ok(balance) => Err(Error::Unexpected(format!(
"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_kp) = safe.keys_create_preload_test_coins(preload_amount).await?;
let from_key_pair = unwrap_key_pair(from_kp)?;
let amount = "1740.000000000";
let (_, to_key_pair) = safe
.keys_create(Some(&from_key_pair.sk), Some(amount), None)
.await?;
let from_current_balance = safe.keys_balance_from_sk(&from_key_pair.sk).await?;
assert_eq!(
"3.233999999",
from_current_balance
);
let to_current_balance = safe
.keys_balance_from_sk(&unwrap_key_pair(to_key_pair)?.sk)
.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_kp) = safe.keys_create_preload_test_coins(preload_amount).await?;
let from_key_pair = unwrap_key_pair(from_kp)?;
let amount = "35.300000000";
let (to_xorname, to_key_pair) = safe
.keys_create(Some(&from_key_pair.sk), Some(amount), None)
.await?;
let from_current_balance = safe
.keys_balance_from_url(&from_xorname, &from_key_pair.sk)
.await?;
assert_eq!(
"400.039999999",
from_current_balance
);
let to_current_balance = safe
.keys_balance_from_url(&to_xorname, &unwrap_key_pair(to_key_pair)?.sk)
.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, kp) = safe.keys_create_preload_test_coins("23.22").await?;
let key_pair = unwrap_key_pair(kp)?;
let pk = safe.validate_sk_for_url(&key_pair.sk, &xorurl).await?;
assert_eq!(pk, key_pair.pk);
Ok(())
}
#[tokio::test]
async fn test_keys_transfer_from_zero_balance() -> Result<()> {
let mut safe = new_safe_instance().await?;
let (_from_safekey_xorurl, key_pair1) = safe.keys_create_preload_test_coins("0.0").await?;
let (to_safekey_xorurl, _key_pair2) = safe.keys_create_preload_test_coins("0.5").await?;
match safe
.keys_transfer(
"0",
Some(&unwrap_key_pair(key_pair1)?.sk),
&to_safekey_xorurl,
None,
)
.await
{
Err(Error::InvalidAmount(msg)) => {
assert_eq!(
msg,
"The amount '0' specified for the transfer is invalid".to_string()
);
Ok(())
}
Err(err) => Err(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
))),
Ok(_) => Err(Error::Unexpected(
"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, kp1) = safe.keys_create_preload_test_coins("0.5").await?;
let (safekey2_xorurl, kp2) = safe.keys_create_preload_test_coins("100.5").await?;
let key_pair1 = unwrap_key_pair(kp1)?;
match safe
.keys_transfer("100.6", Some(&key_pair1.sk), &safekey2_xorurl, None)
.await
{
Err(Error::NotEnoughBalance(msg)) => assert_eq!(
msg,
"Not enough balance for the transfer at provided source SafeKey".to_string()
),
Err(err) => {
return Err(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
)))
}
Ok(_) => {
return Err(Error::Unexpected(
"Transfer succeeded unexpectedly".to_string(),
))
}
};
match safe
.keys_transfer(".06", Some(&key_pair1.sk), &safekey2_xorurl, None)
.await
{
Err(Error::InvalidAmount(msg)) => assert_eq!(
msg,
"Invalid safecoins amount '.06' (Can\'t parse coin units)"
),
Err(err) => {
return Err(Error::Unexpected(format!(
"Error returned is not the expected: {:?}",
err
)))
}
Ok(_) => {
return Err(Error::Unexpected(
"Transfer succeeded unexpectedly".to_string(),
))
}
};
let key_pair2 = unwrap_key_pair(kp2)?;
match safe.keys_transfer(
"0.0000000009",
Some(&key_pair2.sk),
&safekey2_xorurl,
None
).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) => return Err(Error::Unexpected(format!("Error returned is not the expected: {:?}", err))),
Ok(_) => return Err(Error::Unexpected("Transfer succeeded unexpectedly".to_string())),
};
match safe
.keys_transfer("100.4", Some(&key_pair2.sk), &safekey1_xorurl, None)
.await
{
Err(msg) => Err(Error::Unexpected(format!(
"Transfer was expected to succeed: {}",
msg
))),
Ok(_) => {
let from_current_balance = safe.keys_balance_from_sk(&key_pair2.sk).await?;
assert_eq!("0.100000000", from_current_balance);
let to_current_balance = safe.keys_balance_from_sk(&key_pair1.sk).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 (_, kp1) = safe.keys_create_preload_test_coins("10.0").await?;
let key_pair1 = unwrap_key_pair(kp1)?;
safe.wallet_insert(
&to_wallet_xorurl,
Some("my-first-balance"),
true, &key_pair1.sk,
)
.await?;
let (_safekey_xorurl, kp2) = safe.keys_create_preload_test_coins("4621.45").await?;
let key_pair2 = unwrap_key_pair(kp2)?;
match safe
.keys_transfer(
"523.87",
Some(&key_pair2.sk),
&to_wallet_xorurl.clone(),
None,
)
.await
{
Err(msg) => Err(Error::Unexpected(format!(
"Transfer was expected to succeed: {}",
msg
))),
Ok(_) => {
let from_current_balance = safe.keys_balance_from_sk(&key_pair2.sk).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 (_from_safekey_xorurl, kp1) = safe.keys_create_preload_test_coins("0.2").await?;
let (to_safekey_xorurl, kp2) = safe.keys_create_preload_test_coins("0.1").await?;
let key_pair1 = unwrap_key_pair(kp1)?;
let key_pair2 = unwrap_key_pair(kp2)?;
let to_nrsurl = random_nrs_name();
let _ = safe
.nrs_map_container_create(&to_nrsurl, &to_safekey_xorurl, false, true, false)
.await?;
match safe
.keys_transfer("0.2", Some(&key_pair1.sk), &to_nrsurl, None)
.await
{
Err(msg) => Err(Error::Unexpected(format!(
"Transfer was expected to succeed: {}",
msg
))),
Ok(_) => {
let from_current_balance = safe.keys_balance_from_sk(&key_pair1.sk).await?;
assert_eq!("0.000000000" , from_current_balance);
let to_current_balance = safe.keys_balance_from_sk(&key_pair2.sk).await?;
assert_eq!("0.300000000" , to_current_balance);
Ok(())
}
}
}
}