1771 lines
62 KiB
Rust
1771 lines
62 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, MutexGuard, OnceLock, PoisonError};
|
|
use std::time::{Duration, Instant};
|
|
use std::u32;
|
|
|
|
use rand::{thread_rng, Fill, Rng, RngCore};
|
|
use sdk_common::aes_gcm::aead::generic_array::GenericArray;
|
|
use sdk_common::aes_gcm::aes::cipher::ArrayLength;
|
|
use sdk_common::aes_gcm::Nonce;
|
|
use sdk_common::log::{debug, warn, info};
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Error as AnyhowError;
|
|
use anyhow::Result as AnyhowResult;
|
|
use sdk_common::aes_gcm::aead::{Aead, Payload};
|
|
use sdk_common::crypto::{
|
|
encrypt_with_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD,
|
|
};
|
|
use sdk_common::process::{lock_processes, Process, ProcessState};
|
|
use sdk_common::signature::{AnkHash, AnkMessageHash, AnkValidationNoHash, AnkValidationYesHash, Proof};
|
|
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::{sha256, sha256t, Hash};
|
|
use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine};
|
|
use sdk_common::sp_client::bitcoin::hex::{
|
|
self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError,
|
|
};
|
|
use sdk_common::sp_client::bitcoin::key::{Keypair, 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::{signature, MutexExt, MAX_PRD_PAYLOAD_SIZE};
|
|
use serde_json::{Error as SerdeJsonError, Map, Value};
|
|
|
|
use serde::{de, Deserialize, Serialize};
|
|
use tsify::{JsValueSerdeExt, Tsify};
|
|
use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
use sdk_common::device::Device;
|
|
use sdk_common::network::{
|
|
self, AnkFlag, CachedMessage, CachedMessageStatus, CommitMessage, Envelope, FaucetMessage,
|
|
NewTxMessage,
|
|
};
|
|
use sdk_common::pcd::{
|
|
AnkPcdHash, AnkPcdTag, 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::secrets::SecretsStore;
|
|
|
|
use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE};
|
|
use crate::wallet::{generate_sp_wallet, lock_freezed_utxos};
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub enum DiffStatus {
|
|
#[default]
|
|
None,
|
|
Rejected,
|
|
Validated,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct UserDiff {
|
|
pub new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state
|
|
pub value_commitment: String,
|
|
pub field: String,
|
|
pub previous_value: Value,
|
|
pub new_value: Value,
|
|
pub notify_user: bool,
|
|
pub need_validation: bool,
|
|
pub validation_status: DiffStatus,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct UpdatedProcess {
|
|
pub commitment_tx: OutPoint,
|
|
pub current_process: Process,
|
|
pub new_diffs: Vec<UserDiff>, // All diffs should have the same new_state_merkle_root
|
|
pub modified_state: Option<String>, // basically when we add/receive validation proofs for a state
|
|
// I think we should never have both new_state and modified_state
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct ApiReturn {
|
|
pub secrets: Option<SecretsStore>,
|
|
pub updated_process: Option<UpdatedProcess>,
|
|
pub new_tx_to_send: Option<NewTxMessage>,
|
|
pub ciphers_to_send: Vec<String>,
|
|
pub commit_to_send: Option<CommitMessage>,
|
|
}
|
|
|
|
pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct NewKey {
|
|
pub private_key: String,
|
|
pub x_only_public_key: String,
|
|
pub key_parity: bool
|
|
}
|
|
|
|
const IS_TESTNET: bool = true;
|
|
const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000);
|
|
|
|
pub static SHAREDSECRETS: OnceLock<Mutex<SecretsStore>> = OnceLock::new();
|
|
|
|
pub fn lock_shared_secrets() -> Result<MutexGuard<'static, SecretsStore>, anyhow::Error> {
|
|
SHAREDSECRETS
|
|
.get_or_init(|| Mutex::new(SecretsStore::new()))
|
|
.lock_anyhow()
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct ApiError {
|
|
pub message: String,
|
|
}
|
|
|
|
impl ApiError {
|
|
pub fn new(message: String) -> Self {
|
|
ApiError { message }
|
|
}
|
|
}
|
|
|
|
impl From<AnyhowError> for ApiError {
|
|
fn from(value: AnyhowError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<SpError> for ApiError {
|
|
fn from(value: SpError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<FromSliceError> for ApiError {
|
|
fn from(value: FromSliceError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<SerdeJsonError> for ApiError {
|
|
fn from(value: SerdeJsonError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<HexToBytesError> for ApiError {
|
|
fn from(value: HexToBytesError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<HexToArrayError> for ApiError {
|
|
fn from(value: HexToArrayError) -> Self {
|
|
ApiError::new(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::new(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::new(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::new(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::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<FromUtf8Error> for ApiError {
|
|
fn from(value: FromUtf8Error) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<ParseNetworkError> for ApiError {
|
|
fn from(value: ParseNetworkError) -> Self {
|
|
ApiError::new(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<ParseOutPointError> for ApiError {
|
|
fn from(value: ParseOutPointError) -> Self {
|
|
ApiError::new(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 get_new_keypair() -> NewKey {
|
|
let secp = Secp256k1::new();
|
|
let mut rng = thread_rng();
|
|
let keypair = Keypair::new(&secp, &mut rng);
|
|
|
|
let secret_hex = keypair.secret_bytes().to_lower_hex_string();
|
|
let (xonly, parity) = keypair.x_only_public_key();
|
|
|
|
NewKey {
|
|
private_key: secret_hex,
|
|
x_only_public_key: xonly.to_string(),
|
|
key_parity: if parity == Parity::Even { true } else { false }
|
|
}
|
|
}
|
|
|
|
#[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 is_paired() -> ApiResult<bool> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
Ok(local_device.get_pairing_commitment().is_some())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec<String>) -> ApiResult<()> {
|
|
let mut local_device = lock_local_device()?;
|
|
|
|
if local_device.get_pairing_commitment().is_some() {
|
|
return Err(ApiError::new("Already paired".to_owned()));
|
|
}
|
|
|
|
let local_address = local_device
|
|
.get_wallet()
|
|
.get_client()
|
|
.get_receiving_address();
|
|
|
|
if !sp_addresses.iter().any(|a| *a == local_address) {
|
|
sp_addresses.push(local_address);
|
|
}
|
|
|
|
local_device.pair(
|
|
OutPoint::from_str(&commitment_tx)?,
|
|
Member::new(
|
|
sp_addresses
|
|
.into_iter()
|
|
.map(|a| TryInto::<SilentPaymentAddress>::try_into(a).unwrap())
|
|
.collect(),
|
|
)?,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn unpair_device() -> ApiResult<()> {
|
|
let mut local_device = lock_local_device()?;
|
|
|
|
local_device.unpair();
|
|
|
|
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 login(previous_login_tx: String, fee_rate: u32) -> ApiResult<ApiReturn> {
|
|
// // We first create a transaction that spends both pairing tx outputs
|
|
// let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?;
|
|
|
|
// let device = lock_local_device()?;
|
|
// if !device.is_linked() {
|
|
// return Err(ApiError::new("Device is not linked".to_owned()));
|
|
// }
|
|
|
|
// let member = device.to_member().unwrap();
|
|
// let nb_outputs = member.get_addresses().len();
|
|
|
|
// let other_addresses = device.get_other_addresses();
|
|
|
|
// // We get the pairing process out of cache
|
|
// let commitment_txid = device.get_process_commitment().unwrap();
|
|
// let commitment_outpoint = OutPoint::new(commitment_txid, 0);
|
|
|
|
// let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone();
|
|
// let state = process.get_latest_state().unwrap().clone();
|
|
|
|
// let mut shared_secrets = Vec::new();
|
|
// for address in other_addresses {
|
|
// let shared_secret =
|
|
// process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?);
|
|
// if let Some(shared_secret) = shared_secret {
|
|
// shared_secrets.push(shared_secret);
|
|
// }
|
|
// }
|
|
|
|
// let mut decrypted_pcd = Map::new();
|
|
// state
|
|
// .encrypted_pcd
|
|
// .decrypt_fields(&state.keys, &mut decrypted_pcd)?;
|
|
|
|
// let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap();
|
|
|
|
// let wallet = device.get_wallet();
|
|
|
|
// let freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
// let recipients: Vec<Recipient> = device
|
|
// .to_member()
|
|
// .unwrap()
|
|
// .get_addresses()
|
|
// .iter()
|
|
// .map(|a| Recipient {
|
|
// address: a.clone(),
|
|
// amount: DEFAULT_AMOUNT,
|
|
// nb_outputs: 1,
|
|
// })
|
|
// .collect();
|
|
|
|
// let mut mandatory_inputs = Vec::new();
|
|
// for i in 0u32..nb_outputs.try_into().unwrap() {
|
|
// mandatory_inputs.push(OutPoint::new(previous_tx, i));
|
|
// }
|
|
|
|
// let signed_psbt = create_transaction(
|
|
// mandatory_inputs,
|
|
// &freezed_utxos,
|
|
// wallet,
|
|
// recipients,
|
|
// None,
|
|
// Amount::from_sat(fee_rate.into()),
|
|
// None,
|
|
// )?;
|
|
|
|
// // We send it in a TxProposal prd
|
|
// let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt);
|
|
|
|
// debug!("tx_proposal: {:?}", tx_proposal);
|
|
// // We encrypt the prd with the shared_secret for pairing process
|
|
// let prd_msg = tx_proposal.to_network_msg(wallet)?;
|
|
// debug!("prd_msg: {:?}", prd_msg);
|
|
// let mut ciphers = Vec::new();
|
|
// for shared_secret in shared_secrets {
|
|
// let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
// ciphers.push(cipher.to_lower_hex_string());
|
|
// }
|
|
// // We return the cipher
|
|
// Ok(ApiReturn {
|
|
// ciphers_to_send: ciphers,
|
|
// ..Default::default()
|
|
// })
|
|
// }
|
|
|
|
#[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_process_cache() -> ApiResult<()> {
|
|
let mut cached_processes = lock_processes()?;
|
|
|
|
*cached_processes = HashMap::new();
|
|
|
|
debug_assert!(cached_processes.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_process_cache() -> ApiResult<String> {
|
|
let cached_processes = lock_processes()?;
|
|
|
|
let serializable_cache = cached_processes
|
|
.iter()
|
|
.map(|(outpoint, process)| {
|
|
(
|
|
outpoint.to_string(),
|
|
serde_json::to_value(&process).unwrap(),
|
|
)
|
|
})
|
|
.collect::<serde_json::Map<String, Value>>();
|
|
|
|
let json_string = serde_json::to_string(&serializable_cache)
|
|
.map_err(|e| ApiError::new(format!("Failed to serialize process cache: {}", e)))?;
|
|
|
|
Ok(json_string)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn set_process_cache(processes: String) -> ApiResult<()> {
|
|
let processes: Map<String, Value> = serde_json::from_str(&processes)?;
|
|
|
|
let parsed_processes: anyhow::Result<HashMap<OutPoint, Process>> = processes.into_iter()
|
|
.map(|(key, value)| {
|
|
Ok((OutPoint::from_str(&key)?, serde_json::from_value(value)?))
|
|
})
|
|
.collect();
|
|
|
|
let mut cached_processes = lock_processes()?;
|
|
|
|
*cached_processes = parsed_processes?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn reset_shared_secrets() -> ApiResult<()> {
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
|
|
*shared_secrets = SecretsStore::new();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn set_shared_secrets(secrets: String) -> ApiResult<()>{
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
|
|
*shared_secrets = serde_json::from_str(&secrets)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[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_shared_secrets()?;
|
|
reset_process_cache()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_txid(transaction: String) -> ApiResult<String> {
|
|
let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?;
|
|
|
|
Ok(tx.txid().to_string())
|
|
}
|
|
|
|
fn handle_transaction(
|
|
updated: HashMap<OutPoint, OwnedOutput>,
|
|
tx: &Transaction,
|
|
public_data: PublicKey,
|
|
) -> AnyhowResult<ApiReturn> {
|
|
let b_scan: SecretKey;
|
|
let local_member: Member;
|
|
let sp_wallet: SpWallet;
|
|
{
|
|
let local_device = lock_local_device()?;
|
|
sp_wallet = local_device.get_wallet().clone();
|
|
b_scan = local_device.get_wallet().get_client().get_scan_key();
|
|
local_member = local_device.to_member();
|
|
}
|
|
|
|
let op_return: Vec<&sdk_common::sp_client::bitcoin::TxOut> = tx
|
|
.output
|
|
.iter()
|
|
.filter(|o| o.script_pubkey.is_op_return())
|
|
.collect();
|
|
if op_return.len() != 1 {
|
|
return Err(AnyhowError::msg(
|
|
"Transaction must have exactly one op_return output",
|
|
));
|
|
}
|
|
let commitment =
|
|
AnkPrdHash::from_slice(&op_return.first().unwrap().script_pubkey.as_bytes()[2..])?;
|
|
|
|
// 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 mut shared_secrets = lock_shared_secrets()?;
|
|
|
|
// empty utxo_destroyed means we received this transaction
|
|
if utxo_destroyed.is_empty() {
|
|
let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret(
|
|
&public_data,
|
|
&b_scan,
|
|
);
|
|
let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point);
|
|
|
|
// We keep the shared_secret as unconfirmed
|
|
shared_secrets.add_unconfirmed_secret(shared_secret);
|
|
|
|
// We also return it
|
|
let mut new_secret = SecretsStore::new();
|
|
new_secret.add_unconfirmed_secret(shared_secret);
|
|
|
|
// We hash the shared secret to commit into the prd connect
|
|
let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array());
|
|
|
|
// We still don't know who sent it, so we reply with a `Connect` prd
|
|
let prd_connect = Prd::new_connect(local_member, secret_hash, None);
|
|
|
|
let msg = prd_connect.to_network_msg(&sp_wallet)?;
|
|
|
|
// We encrypt the prd connect with the same secret
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?;
|
|
|
|
return Ok(ApiReturn {
|
|
secrets: Some(new_secret),
|
|
ciphers_to_send: vec![cipher.to_lower_hex_string()],
|
|
..Default::default()
|
|
})
|
|
} else {
|
|
// We're sender of the transaction, do nothing
|
|
return Ok(ApiReturn {
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
/// If the transaction has anything to do with us, we create/update the relevant process
|
|
/// and return it to caller for persistent storage
|
|
fn process_transaction(
|
|
tx_hex: String,
|
|
blockheight: u32,
|
|
tweak_data_hex: String,
|
|
) -> anyhow::Result<ApiReturn> {
|
|
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 {
|
|
return handle_transaction(updated, &tx, tweak_data);
|
|
}
|
|
|
|
Err(anyhow::Error::msg("Transaction is not our"))
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn parse_new_tx(new_tx_msg: String, block_height: u32) -> ApiResult<ApiReturn> {
|
|
let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?;
|
|
|
|
if let Some(error) = new_tx.error {
|
|
return Err(ApiError::new(format!(
|
|
"NewTx returned with an error: {}",
|
|
error
|
|
)));
|
|
}
|
|
|
|
if new_tx.tweak_data.is_none() {
|
|
return Err(ApiError::new("Missing tweak_data".to_owned()));
|
|
}
|
|
|
|
Ok(process_transaction(
|
|
new_tx.transaction,
|
|
block_height,
|
|
new_tx.tweak_data.unwrap(),
|
|
)?)
|
|
}
|
|
|
|
// #[wasm_bindgen]
|
|
// pub fn response_prd(
|
|
// root_commitment: String,
|
|
// prd_commitment: String, // The commitment to the Prd we respond to
|
|
// approval: bool,
|
|
// ) -> ApiResult<ApiReturn> {
|
|
// let prd_hash = AnkPrdHash::from_str(&prd_commitment)?;
|
|
// let outpoint = OutPoint::from_str(&root_commitment)?;
|
|
// let local_device = lock_local_device()?;
|
|
// let member = local_device
|
|
// .to_member();
|
|
|
|
// // find the prd in the registered process
|
|
// let mut processes = lock_processes()?;
|
|
// let process = processes
|
|
// .get_mut(&outpoint)
|
|
// .ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
// let prd_ref = process
|
|
// .get_impending_requests_mut()
|
|
// .into_iter()
|
|
// .find(|r| r.create_commitment() == prd_hash)
|
|
// .ok_or(ApiError::new(
|
|
// "Failed to find the prd in registered processes".to_owned(),
|
|
// ))?;
|
|
|
|
// match prd_ref.prd_type {
|
|
// PrdType::Update => {
|
|
// let pcd = Value::from_str(&prd_ref.payload)?;
|
|
// let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&pcd);
|
|
|
|
// let prd_response = Prd::new_response(
|
|
// OutPoint::from_str(&root_commitment)?,
|
|
// serde_json::to_string(&member)?,
|
|
// prd_ref.validation_tokens.clone(),
|
|
// pcd_hash,
|
|
// );
|
|
|
|
// let prd_msg = prd_response.to_network_msg(local_device.get_wallet())?;
|
|
|
|
// let roles = &pcd
|
|
// .get("roles")
|
|
// .ok_or(ApiError::new("No roles in pcd we respond to".to_owned()))?;
|
|
// let roles_map = roles
|
|
// .as_object()
|
|
// .ok_or(ApiError::new("roles is not an object".to_owned()))?
|
|
// .clone();
|
|
// let shared_secrets = lock_shared_secrets()?;
|
|
// let mut ciphers = vec![];
|
|
// for (_, role_def) in roles_map {
|
|
// let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?;
|
|
// for member in role.members {
|
|
// for sp_address in member.get_addresses() {
|
|
// if sp_address.to_string()
|
|
// == local_device
|
|
// .get_wallet()
|
|
// .get_client()
|
|
// .get_receiving_address()
|
|
// {
|
|
// continue;
|
|
// }
|
|
// if let Some(shared_secret) = shared_secrets.get_secret_for_address(sp_address.try_into()?) {
|
|
// let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
// ciphers.push(cipher.to_lower_hex_string());
|
|
// } else {
|
|
// continue;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// return Ok(ApiReturn {
|
|
// ciphers_to_send: ciphers,
|
|
// ..Default::default()
|
|
// });
|
|
// }
|
|
// _ => unimplemented!(),
|
|
// };
|
|
// }
|
|
|
|
fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult<String> {
|
|
match prd.prd_type {
|
|
PrdType::Confirm | PrdType::Response | PrdType::List => {
|
|
return Err(AnyhowError::msg("Invalid prd type"));
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
let outpoint = OutPoint::from_str(&prd.root_commitment)?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
let sender = local_device.to_member();
|
|
|
|
let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone());
|
|
|
|
// debug!("Sending confirm prd: {:?}", prd_confirm);
|
|
|
|
let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?;
|
|
|
|
Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string())
|
|
}
|
|
|
|
fn create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult<Vec<UserDiff>> {
|
|
let new_state_commitments = new_state.pcd_commitment.as_object().ok_or(AnyhowError::msg("new_state commitments is not an object"))?;
|
|
|
|
let device = lock_local_device()?;
|
|
let our_id = device.to_member();
|
|
let is_pairing = device.get_pairing_commitment().is_none();
|
|
|
|
let fields_to_validate = if new_state.encrypted_pcd != Value::Null {
|
|
new_state.get_fields_to_validate_for_member(&our_id)?
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
let new_state_root = &new_state.merkle_root;
|
|
let new_state_decrypted = match new_state.decrypt_pcd() {
|
|
Ok(val) => val,
|
|
Err(_) => Map::new()
|
|
};
|
|
|
|
let mut diffs = vec![];
|
|
if let Some(prev_state) = process.get_latest_commited_state() {
|
|
// We first decrypt as much as we can of the prev_state
|
|
let clear_prev_state = prev_state.decrypt_pcd()?;
|
|
// We just make a diff for values that are different from previous state
|
|
for (field, prev_hash) in prev_state.pcd_commitment.as_object().unwrap() {
|
|
if let Some(new_hash) = new_state_commitments.get(field.as_str()) {
|
|
if new_hash.as_str() == prev_hash.as_str() {
|
|
continue;
|
|
} else {
|
|
// There's a diff
|
|
let previous_value = clear_prev_state.get(field.as_str()).unwrap().clone();
|
|
let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null };
|
|
let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false };
|
|
diffs.push(UserDiff {
|
|
new_state_merkle_root: new_state_root.to_owned(),
|
|
value_commitment: new_hash.as_str().unwrap().to_string(),
|
|
field: field.to_owned(),
|
|
previous_value,
|
|
new_value,
|
|
notify_user: false,
|
|
need_validation,
|
|
validation_status: DiffStatus::None,
|
|
});
|
|
}
|
|
} else {
|
|
// We're missing a hash
|
|
return Err(AnyhowError::msg(format!("No commitment for field {} in new state", field)));
|
|
}
|
|
}
|
|
} else {
|
|
// All fields need a diff
|
|
for (field, hash) in new_state_commitments {
|
|
let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null };
|
|
let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false };
|
|
diffs.push(UserDiff {
|
|
new_state_merkle_root: new_state_root.to_owned(),
|
|
value_commitment: hash.as_str().unwrap().to_string(),
|
|
field: field.to_owned(),
|
|
previous_value: Value::Null,
|
|
new_value,
|
|
notify_user: false,
|
|
need_validation,
|
|
validation_status: DiffStatus::None,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(diffs)
|
|
}
|
|
|
|
fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult<ApiReturn> {
|
|
let local_device = lock_local_device()?;
|
|
let local_member = local_device.to_member();
|
|
let sp_wallet = local_device.get_wallet();
|
|
let secret_hash = AnkMessageHash::from_message(secret.as_byte_array());
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
if let Some(prev_proof) = prd.validation_tokens.get(0) {
|
|
// check that the proof is valid
|
|
prev_proof.verify()?;
|
|
// Check it's signed with our key
|
|
let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?;
|
|
if prev_proof.get_key() != local_address.get_spend_key() {
|
|
return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us"));
|
|
}
|
|
// Check it signs a prd connect that contains the commitment to the shared secret
|
|
let empty_prd = Prd::new_connect(local_member, secret_hash, None);
|
|
let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes());
|
|
if *msg.as_byte_array() != prev_proof.get_message() {
|
|
return Err(anyhow::Error::msg("Previous proof signs another message"));
|
|
}
|
|
// Now we can confirm the secret and link it to an address
|
|
let sender = serde_json::from_str::<Member>(&prd.sender)?;
|
|
let proof = prd.proof.unwrap();
|
|
let actual_sender = sender.get_address_for_key(&proof.get_key())
|
|
.ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?;
|
|
shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?);
|
|
let mut secrets_return = SecretsStore::new();
|
|
secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?);
|
|
return Ok(ApiReturn {
|
|
secrets: Some(secrets_return),
|
|
..Default::default()
|
|
})
|
|
} else {
|
|
let proof = prd.proof.unwrap();
|
|
let sender = serde_json::from_str::<Member>(&prd.sender)?;
|
|
let actual_sender = sender.get_address_for_key(&proof.get_key())
|
|
.ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?;
|
|
|
|
shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?);
|
|
|
|
let mut secrets_return = SecretsStore::new();
|
|
secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?);
|
|
|
|
let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof);
|
|
let msg = prd_connect.to_network_msg(sp_wallet)?;
|
|
let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?;
|
|
|
|
return Ok(ApiReturn {
|
|
ciphers_to_send: vec![cipher.to_lower_hex_string()],
|
|
secrets: Some(secrets_return),
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
fn handle_prd(
|
|
prd: Prd,
|
|
secret: AnkSharedSecretHash
|
|
) -> AnyhowResult<ApiReturn> {
|
|
debug!("handle_prd: {:#?}", prd);
|
|
// Connect is a bit different here because there's no associated process
|
|
// Let's handle that case separately
|
|
if prd.prd_type == PrdType::Connect {
|
|
return handle_prd_connect(prd, secret);
|
|
}
|
|
|
|
let outpoint = OutPoint::from_str(&prd.root_commitment)?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
let relevant_process = match processes.entry(outpoint) {
|
|
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
|
debug!("Creating new process for outpoint: {}", outpoint);
|
|
entry.insert(Process::new(outpoint))
|
|
}
|
|
};
|
|
|
|
match prd.prd_type {
|
|
PrdType::Update => {
|
|
// Compute the merkle tree root for the proposed new state to see if we already know about it
|
|
let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string();
|
|
if relevant_process.get_state_for_commitments_root(&update_merkle_root).is_ok() {
|
|
// We already know about that state
|
|
return Err(AnyhowError::msg("Received update for a state we already know"));
|
|
}
|
|
|
|
let new_state = ProcessState {
|
|
commited_in: OutPoint::from_str(&prd.root_commitment)?,
|
|
pcd_commitment: prd.pcd_commitments,
|
|
merkle_root: update_merkle_root.clone(),
|
|
keys: prd.keys,
|
|
..Default::default()
|
|
};
|
|
|
|
// Compute the diffs
|
|
// At this point we don't have the encrypted values
|
|
// But it can still be useful to track diffs
|
|
let diffs = create_diffs(&relevant_process, &new_state)?;
|
|
|
|
relevant_process.insert_concurrent_state(new_state);
|
|
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: outpoint,
|
|
current_process: relevant_process.clone(),
|
|
new_diffs: diffs,
|
|
..Default::default()
|
|
};
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
});
|
|
}
|
|
PrdType::Response => {
|
|
let mut to_update = relevant_process
|
|
.get_latest_concurrent_states_mut()?
|
|
.into_iter()
|
|
.find(|r| {
|
|
r.pcd_commitment == prd.pcd_commitments
|
|
})
|
|
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
|
|
|
to_update
|
|
.validation_tokens
|
|
.extend(prd.validation_tokens);
|
|
|
|
let updated_state = to_update.clone();
|
|
|
|
// We must return an update of the process
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: OutPoint::from_str(&prd.root_commitment)?,
|
|
current_process: relevant_process.clone(),
|
|
modified_state: Some(updated_state.merkle_root),
|
|
..Default::default()
|
|
};
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
});
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn handle_decrypted_message(
|
|
secret: AnkSharedSecretHash,
|
|
plain: Vec<u8>,
|
|
) -> anyhow::Result<ApiReturn> {
|
|
let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?;
|
|
if let Ok(prd) = Prd::extract_from_message(&plain, local_address) {
|
|
handle_prd(prd, secret)
|
|
} else {
|
|
Err(anyhow::Error::msg("Failed to handle decrypted message"))
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// Use the provided Map to update a state
|
|
/// The map uses hash commitment as keys, as in storage
|
|
pub fn update_process_state(init_commitment: String, state_id: String, hash2values: String) -> ApiResult<ApiReturn> {
|
|
let hash2values_map = serde_json::from_str::<Value>(&hash2values)?.to_value_object()?;
|
|
|
|
// Get the process
|
|
let outpoint = OutPoint::from_str(&init_commitment)?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
{
|
|
// First a mutable borrow of the process
|
|
let process = processes.get_mut(&outpoint)
|
|
.ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
// Get the state
|
|
let state = process.get_latest_concurrent_states_mut()?
|
|
.into_iter()
|
|
.find(|state| state.merkle_root == state_id)
|
|
.ok_or(ApiError::new("Unknown state".to_owned()))?;
|
|
|
|
// Update each value
|
|
// Check if there's already something
|
|
// If we have the key, decrypt and compare to the commitment
|
|
if state.encrypted_pcd.as_object().is_some() && !state.encrypted_pcd.as_object().unwrap().is_empty() {
|
|
return Err(ApiError::new("State already existing".to_owned()));
|
|
}
|
|
let state_commitments = state.pcd_commitment.to_value_object()?;
|
|
let mut new_encrypted_pcd: Map<String, Value> = Map::with_capacity(hash2values_map.len());
|
|
|
|
for (hash, value) in hash2values_map {
|
|
// Check the hash in pcd_commitment, get the corresponding field name
|
|
let (field, _) = state_commitments.iter().find(|(field, commitment)| *hash == **commitment)
|
|
.ok_or(ApiError::new(format!("Failed to find the commitment {}", hash)))?;
|
|
|
|
new_encrypted_pcd.insert(field.clone(), value);
|
|
}
|
|
|
|
// decrypt all we can and check it matches the commitment
|
|
state.encrypted_pcd = Value::Object(new_encrypted_pcd);
|
|
let commited_in = serialize(&state.commited_in);
|
|
|
|
let clear_pcd = state.decrypt_pcd()?;
|
|
|
|
for (i, (key, value)) in clear_pcd.iter().enumerate() {
|
|
// hash each value, and check the result against commitments
|
|
if let Some(expected) = state_commitments.get(key.as_str()) {
|
|
// value can already be the commitment, if we don't have the encryption key
|
|
if value.is_hex_string(Some(32)).is_ok() {
|
|
// check if the clear value is the commitment
|
|
if expected.as_str().unwrap() == value.as_str().unwrap() { continue; }
|
|
}
|
|
// Otherwise we hash the value whatever it is, it must match the commitment
|
|
let mut value_bin = value.to_string().into_bytes();
|
|
value_bin.push(i.try_into().unwrap());
|
|
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &commited_in);
|
|
if tagged_hash.as_byte_array().to_lower_hex_string() != expected.as_str().unwrap() {
|
|
// We set the encrypted pcd back to empty
|
|
state.encrypted_pcd = Value::Object(Map::new());
|
|
return Err(ApiError::new(format!("Retrieved value for {} doesn't match the commitment", key)));
|
|
}
|
|
} else {
|
|
// This shouldn't be possible
|
|
state.encrypted_pcd = Value::Object(Map::new());
|
|
return Err(ApiError::new(format!("Missing commitment for key {}", key)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If every value we can decrypt is valid, then we return the new state and diffs
|
|
// We borrow it again immutably
|
|
let process = processes.get(&outpoint).unwrap();
|
|
let state = process.get_latest_concurrent_states()?.into_iter().find(|s| s.merkle_root == state_id).unwrap();
|
|
let diffs = create_diffs(&process, &state)?;
|
|
|
|
let udpated_process = UpdatedProcess {
|
|
commitment_tx: outpoint,
|
|
current_process: process.clone(),
|
|
new_diffs: diffs,
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(udpated_process),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn parse_cipher(cipher_msg: String) -> ApiResult<ApiReturn> {
|
|
// Check that the cipher is not empty or too long
|
|
if cipher_msg.is_empty() || cipher_msg.len() > MAX_PRD_PAYLOAD_SIZE {
|
|
return Err(ApiError::new(
|
|
"Invalid cipher: empty or too long".to_owned(),
|
|
));
|
|
}
|
|
|
|
let cipher = Vec::from_hex(cipher_msg.trim_matches('"'))?;
|
|
|
|
let decrypt_res = lock_shared_secrets()?.try_decrypt(&cipher);
|
|
if let Ok((secret, plain)) = decrypt_res {
|
|
return handle_decrypted_message(secret, plain)
|
|
.map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e)));
|
|
}
|
|
|
|
Err(ApiError::new("Failed to decrypt cipher".to_owned()))
|
|
}
|
|
|
|
#[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())
|
|
}
|
|
|
|
fn get_shared_secrets_in_transaction(
|
|
psbt: &Psbt,
|
|
addresses: Vec<String>
|
|
) -> anyhow::Result<HashMap<SilentPaymentAddress, AnkSharedSecretHash>> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_wallet();
|
|
|
|
let partial_secret = sp_wallet
|
|
.get_client()
|
|
.get_partial_secret_from_psbt(&psbt)?;
|
|
|
|
let mut new_secrets = HashMap::new();
|
|
for address in addresses {
|
|
let sp_address = SilentPaymentAddress::try_from(address.as_str())?;
|
|
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
|
|
&sp_address.get_scan_key(),
|
|
&partial_secret,
|
|
);
|
|
|
|
let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point);
|
|
|
|
new_secrets.insert(sp_address, shared_secret);
|
|
}
|
|
|
|
Ok(new_secrets)
|
|
}
|
|
|
|
fn create_transaction_for_addresses(addresses: Vec<String>, fee_rate: u32) -> anyhow::Result<Psbt> {
|
|
let mut sp_addresses: Vec<SilentPaymentAddress> = Vec::with_capacity(addresses.len());
|
|
for address in &addresses {
|
|
let sp_address = SilentPaymentAddress::try_from(address.as_str())?;
|
|
sp_addresses.push(sp_address);
|
|
}
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_wallet();
|
|
let freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let mut recipients = Vec::with_capacity(addresses.len());
|
|
for address in addresses {
|
|
let recipient = Recipient {
|
|
address: address,
|
|
amount: DEFAULT_AMOUNT,
|
|
nb_outputs: 1,
|
|
};
|
|
recipients.push(recipient);
|
|
}
|
|
|
|
let signed_psbt = create_transaction(
|
|
vec![],
|
|
&freezed_utxos,
|
|
sp_wallet,
|
|
recipients,
|
|
None,
|
|
Amount::from_sat(fee_rate.into()),
|
|
None,
|
|
)?;
|
|
|
|
Ok(signed_psbt)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// We send a transaction that pays at least one output to each address of each member
|
|
/// The goal is to establish a shared_secret to be used as an encryption key for further communication
|
|
pub fn create_connect_transaction(members_str: Vec<String>, fee_rate: u32) -> ApiResult<ApiReturn> {
|
|
let mut members: Vec<Member> = vec![];
|
|
|
|
for member in members_str {
|
|
members.push(serde_json::from_str(&member)?)
|
|
}
|
|
|
|
let mut addresses = vec![];
|
|
for member in members {
|
|
addresses.extend(member.get_addresses().into_iter());
|
|
}
|
|
|
|
if addresses.is_empty() {
|
|
return Err(ApiError::new("No addresses to connect to".to_owned()));
|
|
}
|
|
|
|
let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?;
|
|
|
|
let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?;
|
|
|
|
let transaction = psbt.extract_tx()?;
|
|
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
let mut secrets_return = SecretsStore::new();
|
|
for (address, secret) in new_secrets {
|
|
shared_secrets.confirm_secret_for_address(secret, address);
|
|
secrets_return.confirm_secret_for_address(secret, address);
|
|
}
|
|
|
|
Ok(ApiReturn {
|
|
new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)),
|
|
secrets: Some(secrets_return),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_new_process(
|
|
init_state: String,
|
|
relay_address: String,
|
|
fee_rate: u32,
|
|
) -> ApiResult<ApiReturn> {
|
|
let pcd = <Value as Pcd>::new_from_string(&init_state)?;
|
|
|
|
// check that we have a proper roles map
|
|
let roles = pcd.extract_roles()?;
|
|
|
|
// Step 1: we create the encryption keys for each field and encrypt them
|
|
let mut fields2keys = Map::new();
|
|
let mut fields2cipher = Map::new();
|
|
let fields_to_encrypt: Vec<String> = pcd
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.map(|k| k.clone())
|
|
.collect();
|
|
|
|
pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher);
|
|
|
|
// We create a transaction that spends to the relay address
|
|
let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?;
|
|
|
|
// We take the secret out
|
|
let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?;
|
|
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
let mut secrets_return = SecretsStore::new();
|
|
for (address, secret) in new_secrets {
|
|
shared_secrets.confirm_secret_for_address(secret, address);
|
|
secrets_return.confirm_secret_for_address(secret, address);
|
|
}
|
|
|
|
let transaction = psbt.extract_tx()?;
|
|
|
|
// We now have the outpoint that will serve as id for the whole process
|
|
let outpoint = OutPoint::new(transaction.txid(), 0);
|
|
|
|
// We now need a hash that commits to the clear value of each field + the process id (or outpoint)
|
|
let pcd_commitment = Value::Object(pcd.hash_all_fields(outpoint)?);
|
|
|
|
let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(ApiError::new("Invalid merkle tree".to_owned()))?.to_lower_hex_string();
|
|
|
|
let mut process = Process::new(outpoint);
|
|
|
|
// We now create the first process state with all that data
|
|
let process_state = ProcessState {
|
|
commited_in: outpoint,
|
|
pcd_commitment: pcd_commitment.clone(),
|
|
merkle_root: merkle_root.clone(),
|
|
encrypted_pcd: Value::Object(fields2cipher.clone()),
|
|
keys: fields2keys.clone(),
|
|
validation_tokens: vec![],
|
|
};
|
|
|
|
let diffs = create_diffs(&process, &process_state)?;
|
|
|
|
process.insert_concurrent_state(process_state.clone())?;
|
|
|
|
{
|
|
let mut processes = lock_processes()?;
|
|
// If we already have an entry with this outpoint, something's wrong
|
|
if processes.contains_key(&outpoint) {
|
|
return Err(ApiError::new("There's already a process for this outpoint".to_owned()));
|
|
}
|
|
processes.insert(outpoint.clone(), process.clone());
|
|
}
|
|
|
|
let commit_msg = CommitMessage::new_first_commitment(transaction, pcd_commitment, roles);
|
|
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: outpoint,
|
|
current_process: process,
|
|
new_diffs: diffs,
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
secrets: Some(secrets_return),
|
|
commit_to_send: Some(commit_msg),
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn update_process(
|
|
init_commitment: String,
|
|
new_state_str: String,
|
|
) -> ApiResult<ApiReturn> {
|
|
let outpoint = OutPoint::from_str(&init_commitment)?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
let process = processes.get_mut(&outpoint)
|
|
.ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
let prev_state = process.get_latest_commited_state()
|
|
.ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?;
|
|
|
|
let last_state_commitments = &prev_state.pcd_commitment;
|
|
|
|
let clear_new_state = Value::from_str(&new_state_str)?.to_value_object()?;
|
|
|
|
let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.clone())?;
|
|
|
|
// We compare the new state with the previous one
|
|
let last_state_merkle_root = &prev_state.merkle_root;
|
|
|
|
if *last_state_merkle_root == new_state.merkle_root {
|
|
return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned()));
|
|
}
|
|
|
|
// We check that we don't have already a similar concurrent state
|
|
let concurrent_processes = process.get_latest_concurrent_states()?;
|
|
if concurrent_processes.iter().any(|p| p.merkle_root == new_state.merkle_root) {
|
|
return Err(ApiError::new("New state already known".to_owned()));
|
|
}
|
|
|
|
let diffs = create_diffs(&process, &new_state)?;
|
|
|
|
// Add the new state to the process
|
|
process.insert_concurrent_state(new_state.clone())?;
|
|
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: outpoint,
|
|
current_process: process.clone(),
|
|
new_diffs: diffs,
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_update_message(
|
|
init_commitment: String,
|
|
merkle_root_hex: String,
|
|
) -> ApiResult<ApiReturn> {
|
|
let mut processes = lock_processes()?;
|
|
|
|
let outpoint = OutPoint::from_str(&init_commitment)?;
|
|
|
|
let process = processes.get_mut(&outpoint)
|
|
.ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
let update_state = process.get_state_for_commitments_root(&merkle_root_hex)?;
|
|
|
|
// We must have at least the key for the roles field, otherwise we don't know who to send the message to
|
|
let clear_state = update_state.decrypt_pcd()?;
|
|
|
|
let roles = Value::Object(clear_state).extract_roles()?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_wallet();
|
|
let local_address = sp_wallet.get_client().get_receiving_address();
|
|
|
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
for (name, role) in roles {
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
for member in role.members {
|
|
// Check that we have a shared_secret with all members
|
|
if let Some(no_secret_address) = member.get_addresses().iter()
|
|
.find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none())
|
|
{
|
|
// We ignore it if we don't have a secret with ourselves
|
|
if *no_secret_address != local_address {
|
|
// for now we return an error to keep it simple
|
|
return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member)));
|
|
}
|
|
}
|
|
if !all_members.contains_key(&member) {
|
|
all_members.insert(member.clone(), HashSet::new());
|
|
}
|
|
all_members.get_mut(&member).unwrap().extend(fields.clone());
|
|
}
|
|
}
|
|
|
|
let sender: Member = local_device
|
|
.to_member();
|
|
|
|
// To allow the recipient to identify the pcd that contains only encrypted values, we compute the merkle tree of the encrypted pcd
|
|
// we then put the root in the payload of the prd update
|
|
let encrypted_pcd_hash = update_state.encrypted_pcd.hash_all_fields(OutPoint::null())?;
|
|
let encrypted_pcd_merkle_root = <Value as Pcd>::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap();
|
|
|
|
let full_prd = Prd::new_update(
|
|
outpoint,
|
|
serde_json::to_string(&sender)?,
|
|
serialize(&encrypted_pcd_merkle_root).to_lower_hex_string(),
|
|
update_state.keys.clone(),
|
|
update_state.pcd_commitment.clone(),
|
|
);
|
|
|
|
let mut ciphers = vec![];
|
|
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 addresses = member.get_addresses();
|
|
for sp_address in addresses.into_iter() {
|
|
// We skip our own device address, no point sending ourself a cipher
|
|
if sp_address == local_address {
|
|
continue;
|
|
}
|
|
|
|
// We shouldn't ever have error here since we already checked above
|
|
let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap();
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
}
|
|
|
|
if ciphers.is_empty() {
|
|
return Err(ApiError::new("Empty ciphers list".to_owned()));
|
|
}
|
|
|
|
process.insert_impending_request(full_prd);
|
|
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: outpoint,
|
|
current_process: process.clone(),
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
ciphers_to_send: ciphers,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn validate_state(init_commitment: String, merkle_root_hex: String) -> ApiResult<ApiReturn> {
|
|
add_validation_token(init_commitment, merkle_root_hex, true)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn refuse_state(init_commitment: String, merkle_root_hex: String) -> ApiResult<ApiReturn> {
|
|
add_validation_token(init_commitment, merkle_root_hex, false)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn evaluate_state(init_commitment: String, previous_state: Option<String>, state: String) -> ApiResult<ApiReturn> {
|
|
let prev_state: Option<ProcessState> = if let Some(s) = previous_state { Some(serde_json::from_str(&s)?) } else { None };
|
|
let process_state: ProcessState = serde_json::from_str(&state)?;
|
|
|
|
process_state.is_valid(prev_state.as_ref())?;
|
|
|
|
let clear_pcd = process_state.decrypt_pcd()?;
|
|
let roles = clear_pcd.extract_roles()?;
|
|
|
|
// We create a commit msg with the valid state
|
|
let outpoint: OutPoint = OutPoint::from_str(&init_commitment)?;
|
|
let commit_msg = CommitMessage::new_update_commitment(outpoint, process_state.pcd_commitment, roles);
|
|
|
|
Ok(ApiReturn {
|
|
commit_to_send: Some(commit_msg),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult<ApiReturn> {
|
|
let mut merkle_root = [0u8; 32];
|
|
let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?;
|
|
if pcd_commitment_vec.len() != 32 {
|
|
return Err(ApiError::new("pcd_commitment must be 32B long".to_owned()));
|
|
}
|
|
merkle_root.copy_from_slice(&pcd_commitment_vec);
|
|
|
|
let mut processes = lock_processes()?;
|
|
|
|
let outpoint = OutPoint::from_str(&init_commitment)?;
|
|
|
|
let process = processes.get_mut(&outpoint)
|
|
.ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?;
|
|
|
|
let message_hash = if approval {
|
|
AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(merkle_root))
|
|
} else {
|
|
AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(merkle_root))
|
|
};
|
|
|
|
let local_device = lock_local_device()?;
|
|
let sp_wallet = local_device.get_wallet();
|
|
let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?);
|
|
|
|
// we copy the state before modifying it
|
|
let previous_state = update_state.clone();
|
|
|
|
update_state.validation_tokens.push(proof);
|
|
|
|
let new_state = update_state.clone();
|
|
|
|
let updated_process = UpdatedProcess {
|
|
commitment_tx: OutPoint::from_str(&init_commitment)?,
|
|
current_process: process.clone(),
|
|
modified_state: Some((previous_state, new_state)),
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> ApiResult<ApiReturn> {
|
|
let mut merkle_root = [0u8; 32];
|
|
let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?;
|
|
if pcd_commitment_vec.len() != 32 {
|
|
return Err(ApiError::new("pcd_commitment must be 32B long".to_owned()));
|
|
}
|
|
merkle_root.copy_from_slice(&pcd_commitment_vec);
|
|
|
|
let mut processes = lock_processes()?;
|
|
|
|
let outpoint = OutPoint::from_str(&init_commitment)?;
|
|
|
|
let process = processes.get_mut(&outpoint)
|
|
.ok_or(ApiError::new("Unknown process".to_owned()))?;
|
|
|
|
let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?;
|
|
|
|
// We must have at least the key for the roles field, otherwise we don't know who to send the message to
|
|
let clear_state = update_state.decrypt_pcd()?.to_value_object()?;
|
|
|
|
let roles = Value::Object(clear_state).extract_roles()?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
let sp_wallet = local_device.get_wallet();
|
|
let local_address = sp_wallet.get_client().get_receiving_address();
|
|
|
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
for (name, role) in roles {
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
for member in role.members {
|
|
// Check that we have a shared_secret with all members
|
|
if let Some(no_secret_address) = member.get_addresses().iter()
|
|
.find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none())
|
|
{
|
|
// We ignore it if we don't have a secret with ourselves
|
|
if *no_secret_address != local_address {
|
|
// for now we return an error to keep it simple
|
|
return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member)));
|
|
}
|
|
}
|
|
if !all_members.contains_key(&member) {
|
|
all_members.insert(member.clone(), HashSet::new());
|
|
}
|
|
all_members.get_mut(&member).unwrap().extend(fields.clone());
|
|
}
|
|
}
|
|
|
|
let our_key = SilentPaymentAddress::try_from(local_address.as_str())?.get_spend_key();
|
|
let proof = update_state.validation_tokens.iter().find(|t| t.get_key() == our_key)
|
|
.ok_or(ApiError::new("We haven't added our validation token yet".to_owned()))?;
|
|
|
|
let sender: Member = local_device
|
|
.to_member();
|
|
|
|
let response_prd = Prd::new_response(
|
|
outpoint,
|
|
serde_json::to_string(&sender)?,
|
|
vec![*proof],
|
|
update_state.pcd_commitment.clone(),
|
|
);
|
|
let prd_msg = response_prd.to_network_msg(sp_wallet)?;
|
|
|
|
let mut ciphers = vec![];
|
|
for (member, visible_fields) in all_members {
|
|
let addresses = member.get_addresses();
|
|
for sp_address in addresses.into_iter() {
|
|
// We skip our own device address, no point sending ourself a cipher
|
|
if sp_address == local_address {
|
|
continue;
|
|
}
|
|
|
|
// We shouldn't ever have error here since we already checked above
|
|
let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap();
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
}
|
|
|
|
if ciphers.is_empty() {
|
|
return Err(ApiError::new("Empty ciphers list".to_owned()));
|
|
}
|
|
|
|
Ok(ApiReturn {
|
|
ciphers_to_send: ciphers,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[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 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());
|
|
|
|
Ok(faucet_msg.to_string())
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
struct UserDiff {
|
|
new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state
|
|
field: String,
|
|
previous_value: Value,
|
|
new_value: Value,
|
|
notify_user: bool,
|
|
need_validation: bool,
|
|
// validated: bool,
|
|
proof: Option<Proof>, // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct PcdUpdates {
|
|
pub previous_pcd: Option<Value>, // We don't have a previous state for creation
|
|
pub decrypted_pcds: HashMap<String, Value>, // Key is the merkle root of the whole state
|
|
pub modified_values: Vec<UserDiff>,
|
|
// pub proofs: HashMap<String, Option<Proof>>, // key is the merkle root of the whole state,
|
|
}
|
|
|
|
/// Get active update proposals for a given process outpoint
|
|
/// Returns a vector with the latest commited state first, if any, and all active proposals
|
|
#[wasm_bindgen]
|
|
pub fn get_update_proposals(process_outpoint: String) -> ApiResult<PcdUpdates> {
|
|
let outpoint = OutPoint::from_str(&process_outpoint)?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
|
|
let relevant_process = processes
|
|
.get(&outpoint)
|
|
.ok_or(ApiError::new("process not found".to_owned()))?;
|
|
|
|
let mut decrypted_pcds = HashMap::new();
|
|
let mut modified_values = Vec::new();
|
|
|
|
let previous_pcd = match relevant_process.get_latest_commited_state() {
|
|
Some(state) => {
|
|
let mut decrypted_pcd = Map::new();
|
|
state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd);
|
|
Some(Value::Object(decrypted_pcd))
|
|
}
|
|
None => None
|
|
};
|
|
|
|
let device = lock_local_device()?;
|
|
let member = device.to_member();
|
|
|
|
let is_pairing = device.get_pairing_commitment().is_none();
|
|
|
|
for state in relevant_process.get_latest_concurrent_states()? {
|
|
if state.encrypted_pcd == Value::Null {
|
|
// This is the last empty state, ignore it
|
|
continue;
|
|
}
|
|
|
|
let fields_to_validate = state.get_fields_to_validate_for_member(&member)?;
|
|
|
|
let mut decrypted_pcd = Map::new();
|
|
state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd)?;
|
|
let root = state.pcd_commitment.create_merkle_tree()?.root_hex().unwrap();
|
|
|
|
if let Some(ref previous_state) = previous_pcd {
|
|
for (key, value) in &decrypted_pcd {
|
|
let previous_value = previous_state.get(key).or_else(|| Some(&Value::Null)).unwrap();
|
|
if previous_value == value { continue; }
|
|
let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) };
|
|
let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false };
|
|
let diff = UserDiff {
|
|
new_state_merkle_root: root.clone(),
|
|
field: key.to_owned(),
|
|
previous_value: previous_value.clone(),
|
|
new_value: value.clone(),
|
|
need_validation,
|
|
notify_user,
|
|
proof: None
|
|
};
|
|
modified_values.push(diff);
|
|
}
|
|
} else {
|
|
for (key, value) in &decrypted_pcd {
|
|
let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) };
|
|
let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false };
|
|
let diff = UserDiff {
|
|
new_state_merkle_root: root.clone(),
|
|
field: key.to_owned(),
|
|
previous_value: Value::Null,
|
|
new_value: value.clone(),
|
|
need_validation,
|
|
notify_user,
|
|
proof: None
|
|
};
|
|
modified_values.push(diff);
|
|
}
|
|
|
|
}
|
|
decrypted_pcds.insert(root, Value::Object(decrypted_pcd));
|
|
}
|
|
|
|
Ok(PcdUpdates {
|
|
previous_pcd,
|
|
decrypted_pcds,
|
|
modified_values,
|
|
})
|
|
}
|