792 lines
25 KiB
Rust
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())
|
|
}
|