sdk_client/src/api.rs
2024-09-02 14:45:35 +02:00

792 lines
25 KiB
Rust

use std::any::Any;
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::ops::Index;
use std::str::FromStr;
use std::string::FromUtf8Error;
use std::sync::{Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
use sdk_common::log::{debug, warn};
use rand::{thread_rng, Fill, Rng, RngCore};
use anyhow::Error as AnyhowError;
use sdk_common::crypto::{
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose,
};
use sdk_common::process::Process;
use sdk_common::signature;
use sdk_common::sp_client::bitcoin::blockdata::fee_rate;
use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize};
use sdk_common::sp_client::bitcoin::hashes::HashEngine;
use sdk_common::sp_client::bitcoin::hashes::{sha256, Hash};
use sdk_common::sp_client::bitcoin::hex::{
parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError,
};
use sdk_common::sp_client::bitcoin::key::{Parity, Secp256k1};
use sdk_common::sp_client::bitcoin::network::ParseNetworkError;
use sdk_common::sp_client::bitcoin::p2p::message::NetworkMessage;
use sdk_common::sp_client::bitcoin::psbt::raw;
use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, Scalar, SecretKey};
use sdk_common::sp_client::bitcoin::transaction::ParseOutPointError;
use sdk_common::sp_client::bitcoin::{
Amount, Network, OutPoint, Psbt, Transaction, Txid, XOnlyPublicKey,
};
use sdk_common::sp_client::constants::{
DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE,
};
use sdk_common::sp_client::silentpayments::utils as sp_utils;
use sdk_common::sp_client::silentpayments::{
utils::{Network as SpNetwork, SilentPaymentAddress},
Error as SpError,
};
use sdk_common::uuid::Uuid;
use serde_json::{Error as SerdeJsonError, Map, Value};
use serde::{Deserialize, Serialize};
use tsify::{JsValueSerdeExt, Tsify};
use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi};
use wasm_bindgen::prelude::*;
use sdk_common::network::{
self, AnkFlag, CachedMessage, CachedMessageStatus, Envelope, FaucetMessage, NewTxMessage};
use sdk_common::pcd::{AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule};
use sdk_common::prd::{AnkPrdHash, Prd, PrdType};
use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address};
use sdk_common::sp_client::spclient::{
derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient,
};
use sdk_common::sp_client::spclient::{SpWallet, SpendKey};
use sdk_common::device::Device;
use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE};
use crate::{lock_messages, lock_processes, ProcessState, ProcessStatus, RelevantProcess, CACHEDMESSAGES, CACHEDPROCESSES};
use crate::wallet::{generate_sp_wallet, lock_freezed_utxos};
pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
const IS_TESTNET: bool = true;
const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000);
#[derive(Debug, PartialEq, Eq)]
pub struct ApiError {
pub message: String,
}
impl From<AnyhowError> for ApiError {
fn from(value: AnyhowError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<SpError> for ApiError {
fn from(value: SpError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<SerdeJsonError> for ApiError {
fn from(value: SerdeJsonError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<HexToBytesError> for ApiError {
fn from(value: HexToBytesError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<HexToArrayError> for ApiError {
fn from(value: HexToArrayError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sdk_common::sp_client::bitcoin::psbt::PsbtParseError> for ApiError {
fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sdk_common::sp_client::bitcoin::psbt::ExtractTxError> for ApiError {
fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sdk_common::sp_client::bitcoin::secp256k1::Error> for ApiError {
fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sdk_common::sp_client::bitcoin::consensus::encode::Error> for ApiError {
fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<FromUtf8Error> for ApiError {
fn from(value: FromUtf8Error) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<ParseNetworkError> for ApiError {
fn from(value: ParseNetworkError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<ParseOutPointError> for ApiError {
fn from(value: ParseOutPointError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl Into<JsValue> for ApiError {
fn into(self) -> JsValue {
JsValue::from_str(&self.message)
}
}
#[wasm_bindgen]
pub fn setup() {
wasm_logger::init(wasm_logger::Config::default());
}
#[wasm_bindgen]
pub fn get_address() -> ApiResult<String> {
let local_device = lock_local_device()?;
Ok(local_device
.get_wallet()
.get_client()
.get_receiving_address())
}
#[wasm_bindgen]
pub fn restore_device(device_str: String) -> ApiResult<()> {
let device: Device = serde_json::from_str(&device_str)?;
let mut local_device = lock_local_device()?;
*local_device = device;
Ok(())
}
#[wasm_bindgen]
pub fn create_device_from_sp_wallet(sp_wallet: String) -> ApiResult<String> {
let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?;
let our_address = set_new_device(sp_wallet)?;
Ok(our_address)
}
#[wasm_bindgen]
pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult<String> {
let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?;
let our_address = set_new_device(sp_wallet)?;
Ok(our_address)
}
#[wasm_bindgen]
pub fn pair_device(uuid: String, mut sp_addresses: Vec<String>) -> ApiResult<()> {
let mut local_device = lock_local_device()?;
if local_device.is_linked() {
return Err(ApiError {
message: "Already paired".to_owned(),
});
}
sp_addresses.push(local_device.get_wallet().get_client().get_receiving_address());
local_device.pair(
Uuid::parse_str(&uuid).unwrap(),
Member::new(sp_addresses.into_iter().map(|a| TryInto::<SilentPaymentAddress>::try_into(a).unwrap()).collect())?
);
Ok(())
}
#[derive(Debug, Tsify, Serialize, Deserialize)]
#[tsify(from_wasm_abi, into_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct outputs_list(OutputList);
impl outputs_list {
fn as_inner(&self) -> &OutputList {
&self.0
}
}
#[wasm_bindgen]
pub fn logout() -> ApiResult<()> {
unimplemented!();
}
#[wasm_bindgen]
pub fn dump_wallet() -> ApiResult<String> {
let device = lock_local_device()?;
Ok(serde_json::to_string(device.get_wallet()).unwrap())
}
#[wasm_bindgen]
pub fn reset_message_cache() -> ApiResult<()> {
let mut cached_msg = lock_messages()?;
*cached_msg = vec![];
debug_assert!(cached_msg.is_empty());
Ok(())
}
#[wasm_bindgen]
pub fn set_message_cache(msg_cache: Vec<String>) -> ApiResult<()> {
let mut cached_msg = lock_messages()?;
if !cached_msg.is_empty() {
return Err(ApiError {
message: "Message cache not empty".to_owned(),
});
}
let new_cache: Result<Vec<CachedMessage>, serde_json::Error> =
msg_cache.iter().map(|m| serde_json::from_str(m)).collect();
*cached_msg = new_cache?;
Ok(())
}
#[wasm_bindgen]
pub fn dump_message_cache() -> ApiResult<Vec<String>> {
let cached_msg = lock_messages()?;
let res: Vec<String> = cached_msg
.iter()
.map(|m| serde_json::to_string(m).unwrap())
.collect();
Ok(res)
}
#[wasm_bindgen]
pub fn dump_device() -> ApiResult<String> {
let local_device = lock_local_device()?;
Ok(serde_json::to_string(&local_device.clone())?)
}
#[wasm_bindgen]
pub fn reset_device() -> ApiResult<()> {
let mut device = lock_local_device()?;
*device = Device::default();
reset_message_cache()?;
Ok(())
}
fn handle_transaction(
updated: HashMap<OutPoint, OwnedOutput>,
tx: &Transaction,
tweak_data: PublicKey,
) -> anyhow::Result<CachedMessage> {
let device = lock_local_device()?;
let sp_wallet = device.get_wallet();
let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return());
let mut commitment = [0u8; 32];
if op_return.is_some() {
commitment.copy_from_slice(&op_return.unwrap().script_pubkey.as_bytes()[2..]);
};
let commitment_str = commitment.to_lower_hex_string();
// If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both
// Basically a transaction that destroyed utxo is a transaction we sent.
let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated
.iter()
.filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent)
.collect();
let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated
.iter()
.filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent)
.collect();
let mut messages = lock_messages()?;
// empty utxo_destroyed means we received this transaction
if utxo_destroyed.is_empty() {
let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret(
&tweak_data,
&sp_wallet.get_client().get_scan_key(),
);
let shared_secret = AnkSharedSecret::new(shared_point);
let mut plaintext: Vec<u8> = vec![];
if let Some(message) = messages.iter_mut().find(|m| {
if m.status != CachedMessageStatus::CipherWaitingTx {
return false;
}
let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array());
if res.is_ok() {
plaintext = res.unwrap();
return true;
} else {
return false;
}
}) {
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
let prd = Prd::extract_from_message(&plaintext, commitment)?;
message.prd = Some(prd.to_string());
message.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string());
message.commitment = Some(commitment_str);
message.sender = Some(serde_json::from_str(&prd.sender)?);
message.status = CachedMessageStatus::Opened;
return Ok(message.clone());
} else {
// store it and wait for the message
let mut new_msg = CachedMessage::new();
let (outpoint, output) = utxo_created
.into_iter()
.next()
.expect("utxo_created shouldn't be empty");
new_msg.commitment = Some(commitment.to_lower_hex_string());
new_msg.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string());
new_msg.status = CachedMessageStatus::TxWaitingPrd;
messages.push(new_msg.clone());
return Ok(new_msg.clone());
}
} else {
// We're sender of the transaction, do nothing
return Ok(CachedMessage::new());
}
}
/// If the transaction has anything to do with us, we create/update the relevant `CachedMessage`
/// and return it to caller for persistent storage
fn process_transaction(
tx_hex: String,
blockheight: u32,
tweak_data_hex: String,
) -> anyhow::Result<Option<CachedMessage>> {
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
let updated: HashMap<OutPoint, OwnedOutput>;
{
let mut device = lock_local_device()?;
let wallet = device.get_mut_wallet();
updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
}
if updated.len() > 0 {
let updated_msg = handle_transaction(updated, &tx, tweak_data)?;
return Ok(Some(updated_msg));
}
Ok(None)
}
#[wasm_bindgen]
pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult<Option<CachedMessage>> {
let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?;
if let Some(error) = new_tx.error {
return Err(ApiError {
message: format!("NewTx returned with an error: {}", error),
});
}
if new_tx.tweak_data.is_none() {
return Err(ApiError {
message: "Missing tweak_data".to_owned(),
});
}
let msg =
process_transaction(new_tx.transaction, block_height, new_tx.tweak_data.unwrap())?;
Ok(msg)
}
#[wasm_bindgen]
pub fn parse_cipher(cipher_msg: String, fee_rate: u32) -> ApiResult<CachedMessage> {
// let's try to decrypt with keys we found in transactions but haven't used yet
let mut messages = lock_messages()?;
let cipher = Vec::from_hex(&cipher_msg.trim_matches('\"'))?;
let mut plain = vec![];
if let Some(message) = messages.iter_mut().find(|m| match m.status {
CachedMessageStatus::TxWaitingPrd | CachedMessageStatus::Opened => {
if let Ok(m) = m.try_decrypt_message(cipher.clone()) {
plain = m;
return true;
} else { return false }
}
_ => return false,
}) {
if message.status == CachedMessageStatus::TxWaitingPrd {
// debug!("Found message {}", String::from_utf8(plain.clone())?);
let mut commitment = [0u8; 32];
commitment.copy_from_slice(&Vec::from_hex(message.commitment.as_ref().unwrap())?);
let prd = Prd::extract_from_message(&plain, commitment)?;
message.prd = Some(prd.to_string());
message.sender = Some(serde_json::from_str(&prd.sender)?);
message.status = CachedMessageStatus::Opened;
} else {
// debug!("Found message {}", String::from_utf8(plain.clone())?);
// we're receiving a pcd for a prd we already have
let mut pcd = Value::from_str(&String::from_utf8(plain)?)?;
// check that the hash of the pcd is the same than commited in the prd
let pcd_commitment = AnkPcdHash::from_value(&pcd);
let prd: Prd = serde_json::from_str(message.prd.as_ref().unwrap()).unwrap();
if pcd_commitment.to_string() != prd.pcd_commitment {
return Err(ApiError {
message: format!("Pcd doesn't match commitment: expected {:?}\ngot {}", prd.pcd_commitment, pcd_commitment),
});
}
message.pcd = Some(pcd.clone());
// Now we decrypt all we can from the pcd
pcd.decrypt_fields(&prd.keys)?;
// we take a few mandatory informations
let process = pcd["process"].take();
let uuid = Uuid::parse_str(&prd.process_uuid).expect("prd can't have an invalid uuid");
// todo check that the uuid in the process we took from pcd is consistent
// we complete the process we keep in storage
let mut processes = lock_processes()?;
match prd.prd_type {
PrdType::Init => {
let relevant_process = RelevantProcess {
process: serde_json::from_value(process)?,
states: vec![ProcessState {
commited_in: OutPoint::null(), // At this point process is not commited yet
encrypted_pcd: message.pcd.clone().unwrap(),
keys: prd.keys.clone(),
validation_token: vec![],
}],
current_status: ProcessStatus::Active(message.shared_secrets.clone())
};
processes.insert(uuid, relevant_process);
},
_ => unimplemented!()
}
}
return Ok(message.clone());
} else {
// let's keep it in case we receive the transaction later
let mut new_msg = CachedMessage::new();
new_msg.status = CachedMessageStatus::CipherWaitingTx;
new_msg.cipher.push(cipher_msg);
messages.push(new_msg.clone());
return Ok(new_msg);
}
}
#[wasm_bindgen]
pub fn get_outputs() -> ApiResult<JsValue> {
let device = lock_local_device()?;
let outputs = device.get_wallet().get_outputs().clone();
Ok(JsValue::from_serde(&outputs.to_outpoints_list())?)
}
#[wasm_bindgen]
pub fn get_available_amount() -> ApiResult<u64> {
let device = lock_local_device()?;
Ok(device.get_wallet().get_outputs().get_balance().to_sat())
}
#[wasm_bindgen]
pub fn create_process_from_template(json: String) -> ApiResult<Process> {
let template_process: Process = serde_json::from_str(&json)?;
let mut new_process = Process::new(
template_process.html,
template_process.style,
template_process.script,
template_process.init_state,
template_process.commited_in
);
Ok(new_process)
}
#[derive(Debug, Tsify, Serialize, Deserialize, Default)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct createTransactionReturn {
pub txid: String,
pub transaction: String,
pub new_messages: Vec<CachedMessage>
}
#[wasm_bindgen]
pub fn create_process_init_transaction(
mut new_process: Process,
fee_rate: u32,
) -> ApiResult<createTransactionReturn> {
let pcd = new_process.init_state;
let roles = pcd.get("roles").ok_or(ApiError { message: "No roles in init_state".to_owned()})?;
let roles_map = roles.as_object().ok_or(ApiError { message: "roles is not an object".to_owned()})?.clone();
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
for (name, role_def) in roles_map {
let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?;
let fields: Vec<String> = role.validation_rules.iter().flat_map(|rule| rule.fields.clone()).collect();
for member in role.members {
if !all_members.contains_key(&member) {
all_members.insert(member.clone(), HashSet::new());
}
all_members.get_mut(&member).unwrap().extend(fields.clone());
}
}
let nb_recipients = all_members.len();
if nb_recipients == 0 {
return Err(ApiError {
message: "Can't create a process with 0 member".to_owned(),
});
}
let mut recipients: Vec<Recipient> = Vec::with_capacity(nb_recipients*2); // We suppose that will work most of the time
// we actually have multiple "recipients" in a technical sense for each social recipient
// that's necessary because we don't want to miss a notification because we don't have a device atm
for member in all_members.keys() {
let addresses = member.get_addresses();
for sp_address in addresses.into_iter() {
recipients.push(Recipient {
address: sp_address.into(),
amount: DEFAULT_AMOUNT,
nb_outputs: 1,
});
}
}
let mut fields2keys = Map::new();
let mut fields2cipher = Map::new();
Value::Object(pcd.clone()).encrypt_fields(&mut fields2keys, &mut fields2cipher);
new_process.init_state = fields2cipher.clone();
let local_device = lock_local_device()?;
let sp_wallet = local_device.get_wallet();
let sender: Member = local_device.to_member().ok_or(ApiError { message: "unpaired device".to_owned() })?;
// We first generate the prd with all the keys that we will keep to ourselves
let full_prd = Prd::new(
PrdType::Init,
Uuid::from_str(&new_process.uuid).expect("We can trust process to have a valid uuid"),
serde_json::to_string(&sender)?,
fields2cipher.clone(),
fields2keys.clone()
)?;
let prd_commitment = full_prd.create_commitment();
let freezed_utxos = lock_freezed_utxos()?;
let signed_psbt = create_transaction(
&vec![],
&freezed_utxos,
sp_wallet,
recipients,
Some(prd_commitment.as_byte_array().to_vec()),
Amount::from_sat(fee_rate.into()),
None,
)?;
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
let partial_secret = sp_wallet
.get_client()
.get_partial_secret_from_psbt(&signed_psbt)?;
let final_tx = signed_psbt.extract_tx()?;
let mut new_messages = vec![];
let mut shared_secrets = vec![]; // This is a bit ugly, but this way we can update the process status
for (member, visible_fields) in all_members {
let mut prd = full_prd.clone();
prd.filter_keys(visible_fields);
let prd_msg = prd.to_network_msg(sp_wallet)?;
let mut res = CachedMessage::new();
res.recipient = Some(member.clone());
res.prd = Some(prd_msg.clone());
res.sender = Some(sender.clone());
res.pcd = Some(Value::Object(pcd.clone()));
res.commitment = Some(prd_commitment.to_string());
res.status = CachedMessageStatus::Opened;
let addresses = member.get_addresses();
for sp_address in addresses.into_iter() {
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
&<SilentPaymentAddress>::try_from(sp_address)?.get_scan_key(),
&partial_secret,
);
let shared_secret = AnkSharedSecret::new(shared_point).to_byte_array().to_lower_hex_string();
let cipher = encrypt_with_key(
prd_msg.clone(),
shared_secret.clone(),
)?;
res.cipher.push(cipher);
res.shared_secrets.push(shared_secret.clone());
shared_secrets.push(shared_secret);
}
new_messages.push(res);
}
lock_messages()?.extend(new_messages.clone());
let mut processes = lock_processes()?;
let init_state = ProcessState {
commited_in: OutPoint::null(),
encrypted_pcd: Value::Object(fields2cipher),
keys: fields2keys,
validation_token: vec![]
};
// We are initializing a process, so we shouldn't have it in our cache yet
processes.insert(Uuid::parse_str(&new_process.uuid).unwrap(), RelevantProcess {
process: new_process,
states: vec![init_state],
current_status: ProcessStatus::Active(shared_secrets)
});
Ok(createTransactionReturn {
txid: final_tx.txid().to_string(),
transaction: serialize(&final_tx).to_lower_hex_string(),
new_messages
})
}
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct encryptWithNewKeyResult {
pub cipher: String,
pub key: String,
}
#[wasm_bindgen]
pub fn encrypt_with_key(plaintext: String, key: String) -> ApiResult<String> {
let nonce = Aes256Gcm::generate_nonce(&mut rand::thread_rng());
let mut aes_key = [0u8; 32];
aes_key.copy_from_slice(&Vec::from_hex(&key)?);
// encrypt
let aes_enc = Aes256Encryption::import_key(
Purpose::Arbitrary,
plaintext.into_bytes(),
aes_key,
nonce.into(),
)?;
let cipher = aes_enc.encrypt_with_aes_key()?;
Ok(cipher.to_lower_hex_string())
}
#[wasm_bindgen]
pub fn encrypt_with_new_key(plaintext: String) -> ApiResult<encryptWithNewKeyResult> {
let mut rng = rand::thread_rng();
// generate new key
let aes_key = Aes256Gcm::generate_key(&mut rng);
let nonce = Aes256Gcm::generate_nonce(&mut rng);
// encrypt
let aes_enc = Aes256Encryption::import_key(
Purpose::Arbitrary,
plaintext.into_bytes(),
aes_key.into(),
nonce.into(),
)?;
let cipher = aes_enc.encrypt_with_aes_key()?;
Ok(encryptWithNewKeyResult {
cipher: cipher.to_lower_hex_string(),
key: aes_key.to_lower_hex_string(),
})
}
#[wasm_bindgen]
pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult<String> {
let key_bin = Vec::from_hex(&key)?;
if key_bin.len() != 32 {
return Err(ApiError {
message: "key of invalid lenght".to_owned(),
});
}
let mut aes_key = [0u8; 32];
aes_key.copy_from_slice(&Vec::from_hex(&key)?);
let aes_dec = Aes256Decryption::new(Purpose::Arbitrary, Vec::from_hex(&cipher)?, aes_key)?;
let plain = String::from_utf8(aes_dec.decrypt_with_key()?)?;
Ok(plain)
}
#[wasm_bindgen]
pub fn create_faucet_msg() -> ApiResult<String> {
let sp_address = lock_local_device()?
.get_wallet()
.get_client()
.get_receiving_address();
let faucet_msg = FaucetMessage::new(sp_address.clone());
let network_msg = Envelope::new(AnkFlag::Faucet, &faucet_msg.to_string());
Ok(network_msg.to_string())
}