1880 lines
66 KiB
Rust
1880 lines
66 KiB
Rust
use std::any::Any;
|
|
use std::borrow::Borrow;
|
|
use std::collections::{BTreeMap, 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 futures_util::StreamExt;
|
|
use sdk_common::updates::{init_update_sink, StateUpdate, WasmUpdateSink};
|
|
use web_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::hash::AnkPcdHash;
|
|
use sdk_common::log::{self, debug, info, warn};
|
|
use sdk_common::backend_blindbit_wasm::wasm_bindgen_futures;
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use anyhow::Error as AnyhowError;
|
|
use anyhow::Result as AnyhowResult;
|
|
use sdk_common::aes_gcm::aead::{Aead, Payload};
|
|
use sdk_common::crypto::{
|
|
decrypt_with_key, encrypt_with_key, generate_key, verify_merkle_proof, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD
|
|
};
|
|
use sdk_common::process::{Process, ProcessState};
|
|
use sdk_common::serialization::{OutPointMemberMap, OutPointProcessMap};
|
|
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::{
|
|
SilentPaymentAddress,
|
|
Error as SpError,
|
|
};
|
|
use sdk_common::{js_sys::{Object, Reflect, Uint8Array}, signature, MutexExt, MAX_PRD_PAYLOAD_SIZE};
|
|
use serde_json::{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, CommitMessage, Envelope, FaucetMessage,
|
|
NewTxMessage,
|
|
};
|
|
use sdk_common::pcd::{
|
|
DataType, FileBlob, Member, Pcd, PcdCommitments, RoleDefinition, Roles, ValidationRule, PCD_VERSION, PcdSerializable
|
|
};
|
|
use sdk_common::prd::{AnkPrdHash, Prd, PrdType};
|
|
use sdk_common::silentpayments::{create_transaction as internal_create_transaction, sign_transaction as internal_sign_tx, SpWallet, TsUnsignedTransaction};
|
|
use sdk_common::sp_client::{FeeRate, OutputSpendStatus, OwnedOutput, Recipient, RecipientAddress, SilentPaymentUnsignedTransaction, SpClient, 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};
|
|
|
|
const EMPTYSTATEID: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
|
|
#[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 process_id: String,
|
|
pub state_id: String, // TODO add a merkle proof that the new_value belongs to that state
|
|
pub value_commitment: String,
|
|
pub field: String,
|
|
pub roles: Roles,
|
|
pub description: Option<String>,
|
|
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 process_id: OutPoint,
|
|
pub current_process: Process,
|
|
pub diffs: Vec<UserDiff>, // All diffs should have the same state_id
|
|
pub encrypted_data: BTreeMap<String, String>, // hashes in pcd commitment to ciphers
|
|
pub validated_state: Option<[u8; 32]>, // when we add/receive validation proofs for a 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 push_to_storage: Vec<String>, // hash of the requested data, must be in db
|
|
pub partial_tx: Option<TsUnsignedTransaction>,
|
|
}
|
|
|
|
pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
|
|
|
|
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<serde_wasm_bindgen::Error> for ApiError {
|
|
fn from(value: serde_wasm_bindgen::Error) -> 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()?;
|
|
|
|
let address = local_device
|
|
.get_sp_client()
|
|
.get_receiving_address()
|
|
.to_string();
|
|
Ok(address)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_member() -> ApiResult<Member> {
|
|
let local_device = lock_local_device()?;
|
|
let us = local_device.to_member();
|
|
|
|
Ok(us)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn restore_device(device: JsValue) -> ApiResult<()> {
|
|
let device: Device = serde_wasm_bindgen::from_value(device)?;
|
|
|
|
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: SpClient = 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(Network::from_core_arg(&network_str)?)?;
|
|
|
|
let our_address = set_new_device(sp_wallet)?;
|
|
|
|
Ok(our_address)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub async fn scan_blocks(tip_height: u32, blindbit_url: String) -> ApiResult<()> {
|
|
let (sink, mut scan_rx, mut state_rx) = WasmUpdateSink::new();
|
|
|
|
// Initialize the global sink
|
|
init_update_sink(sink);
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
while let Some(progress) = scan_rx.next().await {
|
|
log::info!(
|
|
"Scan progress: {}/{} ({}%)",
|
|
progress.current,
|
|
progress.end,
|
|
(progress.current * 100) / progress.end
|
|
);
|
|
}
|
|
});
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
while let Some(update) = state_rx.next().await {
|
|
match update {
|
|
StateUpdate::Update {
|
|
blkheight,
|
|
blkhash,
|
|
found_outputs,
|
|
found_inputs,
|
|
} => {
|
|
log::info!(
|
|
"Processing update at height {}: {} outputs, {} inputs",
|
|
blkheight, found_outputs.len(), found_inputs.len()
|
|
);
|
|
|
|
// Actually update the device's wallet state in WASM context
|
|
if let Ok(mut local_device) = lock_local_device() {
|
|
// Get direct access to the device's outputs to modify them
|
|
let device_outputs = local_device.get_mut_outputs();
|
|
|
|
// Mark found inputs as spent
|
|
for outpoint in found_inputs {
|
|
if let Some(output) = device_outputs.get_mut(&outpoint) {
|
|
output.spend_status = OutputSpendStatus::Spent(blkhash.to_byte_array());
|
|
} else {
|
|
// This should never happen, but we'll log it just in case
|
|
log::error!("Found a spent input that we don't know about: {}", outpoint.to_string());
|
|
}
|
|
}
|
|
|
|
// Add new found outputs to the device
|
|
device_outputs.extend(found_outputs);
|
|
|
|
// update last_scan
|
|
local_device.get_mut_sp_wallet().set_last_scan(blkheight.to_consensus_u32());
|
|
|
|
log::debug!("Updated device outputs and marked inputs as spent");
|
|
} else {
|
|
log::error!("Failed to lock local device for state update");
|
|
}
|
|
|
|
log::debug!("State update processed at height: {}", blkheight);
|
|
}
|
|
StateUpdate::NoUpdate { blkheight } => {
|
|
log::debug!("No update at height: {}", blkheight);
|
|
// We still need to update last_scan
|
|
if let Ok(mut local_device) = lock_local_device() {
|
|
let sp_wallet = local_device.get_mut_sp_wallet();
|
|
sp_wallet.set_last_scan(blkheight.to_consensus_u32());
|
|
} else {
|
|
log::error!("Failed to lock local device for last_scan update");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let (sp_client, owned_outpoints, last_scan) = {
|
|
let local_device = lock_local_device()?;
|
|
let owned_outpoints: HashSet<OutPoint> = local_device.get_sp_wallet().get_unspent_outputs().keys().map(|o| *o).collect();
|
|
let sp_client = local_device.get_sp_client().clone();
|
|
let last_scan = local_device.get_sp_wallet().get_last_scan();
|
|
|
|
(sp_client, owned_outpoints, last_scan)
|
|
};
|
|
|
|
let n_blocks_to_scan = tip_height - last_scan;
|
|
|
|
crate::wallet::scan_blocks(n_blocks_to_scan, &blindbit_url, sp_client, owned_outpoints, tip_height, last_scan, 10).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[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(process_id: 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_sp_client()
|
|
.get_receiving_address()
|
|
.to_string();
|
|
|
|
if !sp_addresses.iter().any(|a| *a == local_address) {
|
|
sp_addresses.push(local_address);
|
|
}
|
|
|
|
local_device.pair(
|
|
OutPoint::from_str(&process_id)?,
|
|
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(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_wallet() -> ApiResult<String> {
|
|
let device = lock_local_device()?;
|
|
|
|
Ok(serde_json::to_string(device.get_sp_client()).unwrap())
|
|
}
|
|
|
|
#[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 get_pairing_process_id() -> ApiResult<String> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
let pairing_commitment = local_device.get_pairing_commitment().ok_or(ApiError::new("Device is not paired".to_owned()))?;
|
|
|
|
Ok(pairing_commitment.to_string())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_device() -> ApiResult<Device> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
Ok(local_device.clone())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_neutered_device() -> ApiResult<Device> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
if local_device.get_pairing_commitment().is_none() {
|
|
return Err(ApiError::new("Device must be paired".to_owned()));
|
|
}
|
|
|
|
let client = local_device.get_sp_client();
|
|
let scan_key = client.get_scan_key();
|
|
let spend_pubkey: PublicKey = client.get_spend_key().into();
|
|
|
|
let neutered_client = SpClient::new(scan_key, SpendKey::Public(spend_pubkey), Network::Signet)?;
|
|
|
|
let mut neutered_device = Device::new(neutered_client);
|
|
|
|
neutered_device.pair(local_device.get_pairing_commitment().unwrap(), local_device.to_member());
|
|
|
|
Ok(neutered_device)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn reset_device() -> ApiResult<()> {
|
|
let mut device = lock_local_device()?;
|
|
|
|
*device = Device::default();
|
|
|
|
reset_shared_secrets()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_txid(transaction: String) -> ApiResult<String> {
|
|
let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?;
|
|
|
|
Ok(tx.txid().to_string())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_prevouts(transaction: String) -> ApiResult<Vec<String>> {
|
|
let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?;
|
|
|
|
Ok(tx.input.iter().map(|input| input.previous_output.to_string()).collect())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_opreturn(transaction: String) -> ApiResult<String> {
|
|
let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?;
|
|
|
|
let op_return_outputs: Vec<_> = tx
|
|
.output
|
|
.iter()
|
|
.filter(|o| o.script_pubkey.is_op_return())
|
|
.collect();
|
|
if op_return_outputs.len() != 1 {
|
|
return Err(ApiError::new(
|
|
"Transaction must contain exactly one op_return output".to_owned(),
|
|
));
|
|
}
|
|
let data = &op_return_outputs
|
|
.into_iter()
|
|
.next()
|
|
.unwrap()
|
|
.script_pubkey
|
|
.as_bytes()[2..];
|
|
if data.len() != 32 {
|
|
return Err(ApiError::new("commited data is not 32B long".to_owned()));
|
|
}
|
|
|
|
Ok(data.to_lower_hex_string())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn process_commit_new_state(mut process: Process, state_id: String, new_tip: String) -> ApiResult<Process> {
|
|
let state_id_array: [u8; 32] = Vec::from_hex(&state_id)?.try_into().unwrap();
|
|
let new_tip = OutPoint::new(Txid::from_str(&new_tip)?, 0);
|
|
let new_state: ProcessState;
|
|
if let Ok(commited_state) = process.get_state_for_id(&state_id_array) {
|
|
new_state = commited_state.clone();
|
|
} else {
|
|
new_state = ProcessState {
|
|
commited_in: process.get_process_tip()?,
|
|
state_id: state_id_array,
|
|
..Default::default()
|
|
};
|
|
}
|
|
|
|
process.remove_all_concurrent_states()?;
|
|
process.insert_concurrent_state(new_state)?;
|
|
process.update_states_tip(new_tip)?;
|
|
Ok(process)
|
|
}
|
|
|
|
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: SpClient;
|
|
{
|
|
let local_device = lock_local_device()?;
|
|
sp_wallet = local_device.get_sp_client().clone();
|
|
b_scan = local_device.get_sp_client().get_scan_key();
|
|
local_member = local_device.to_member();
|
|
}
|
|
|
|
// 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();
|
|
|
|
// 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);
|
|
|
|
let mut shared_secrets = lock_shared_secrets()?;
|
|
|
|
// We keep the shared_secret as unconfirmed
|
|
shared_secrets.add_unconfirmed_secret(shared_secret);
|
|
|
|
// We also return it for permanent storage
|
|
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,
|
|
members_list: &OutPointMemberMap
|
|
) -> 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()?;
|
|
updated = device.update_outputs_with_transaction(&tx, blockheight, tweak_data)?;
|
|
}
|
|
|
|
if updated.len() > 0 {
|
|
return handle_transaction(updated, &tx, tweak_data);
|
|
} else {
|
|
log::debug!("Transaction is not ours");
|
|
return Ok(ApiReturn {
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn parse_new_tx(new_tx_msg: String, block_height: u32, members_list: OutPointMemberMap) -> 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 message 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(),
|
|
&members_list
|
|
)?)
|
|
}
|
|
|
|
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 = prd.process_id;
|
|
|
|
let local_device = lock_local_device()?;
|
|
let sender = local_device.to_member();
|
|
|
|
let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone());
|
|
|
|
let prd_msg = prd_confirm.to_network_msg(local_device.get_sp_client())?;
|
|
|
|
Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string())
|
|
}
|
|
|
|
fn create_diffs(device: &MutexGuard<Device>, process: &Process, new_state: &ProcessState, members_list: &OutPointMemberMap) -> AnyhowResult<Vec<UserDiff>> {
|
|
let new_state_commitments = &new_state.pcd_commitment;
|
|
|
|
let our_id = device.get_pairing_commitment();
|
|
|
|
let fields_to_validate = if let Some(our_id) = our_id {
|
|
new_state.get_fields_to_validate_for_member(&our_id)?
|
|
} else {
|
|
// Device is unpaired, we just take all the fields in the `pairing` role
|
|
if let Some(pairing_role) = new_state.roles.get("pairing") {
|
|
pairing_role.validation_rules.iter().flat_map(|r| r.fields.clone()).collect()
|
|
} else {
|
|
return Err(AnyhowError::msg("Missing pairing role"))
|
|
}
|
|
};
|
|
|
|
let new_state_id = &new_state.state_id;
|
|
|
|
let new_public_data = &new_state.public_data;
|
|
|
|
let process_id = process.get_process_id()?.to_string();
|
|
let mut diffs = vec![];
|
|
for (field, hash) in new_state_commitments.iter() {
|
|
let need_validation = fields_to_validate.contains(field);
|
|
diffs.push(UserDiff {
|
|
process_id: process_id.clone(),
|
|
state_id: new_state_id.to_lower_hex_string(),
|
|
value_commitment: hash.to_lower_hex_string(),
|
|
field: field.to_owned(),
|
|
description: None, // TODO we don't use that for now, we'll see later
|
|
notify_user: false,
|
|
need_validation,
|
|
validation_status: DiffStatus::None,
|
|
roles: new_state.roles.clone(),
|
|
});
|
|
}
|
|
|
|
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_sp_client();
|
|
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_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 proof = prd.proof.unwrap();
|
|
let actual_sender = prd.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 actual_sender = prd.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,
|
|
members_list: &OutPointMemberMap,
|
|
processes: &OutPointProcessMap
|
|
) -> 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 = prd.process_id;
|
|
|
|
let mut relevant_process: Process = if let Some(relevant_process) = processes.0.get(&outpoint) {
|
|
relevant_process.clone()
|
|
} else {
|
|
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"))?;
|
|
if relevant_process.get_state_for_id(&update_merkle_root).is_ok() {
|
|
// We already know about that state
|
|
return Err(AnyhowError::msg("Received update for a state we already know"));
|
|
}
|
|
|
|
let commited_in = relevant_process.get_process_tip()?;
|
|
|
|
let new_state = ProcessState {
|
|
commited_in,
|
|
pcd_commitment: prd.pcd_commitments,
|
|
state_id: update_merkle_root.clone(),
|
|
keys: prd.keys,
|
|
roles: prd.roles,
|
|
public_data: prd.public_data,
|
|
..Default::default()
|
|
};
|
|
|
|
// Compute the diffs
|
|
let diffs = create_diffs(&lock_local_device()?, &relevant_process, &new_state, members_list)?;
|
|
|
|
relevant_process.insert_concurrent_state(new_state)?;
|
|
|
|
let updated_process = UpdatedProcess {
|
|
process_id: outpoint,
|
|
current_process: relevant_process.clone(),
|
|
diffs,
|
|
..Default::default()
|
|
};
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
});
|
|
}
|
|
PrdType::Response => {
|
|
let update_state_id = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?;
|
|
let mut to_update = relevant_process.get_state_for_id_mut(&update_state_id)?;
|
|
|
|
let new_validations = prd.validation_tokens;
|
|
let mut to_add: Vec<&Proof> = vec![];
|
|
|
|
for token in &new_validations {
|
|
let key = token.get_key();
|
|
|
|
// Check if the token key already exists
|
|
let already_exists = to_update
|
|
.validation_tokens
|
|
.iter()
|
|
.any(|existing_token| existing_token.get_key() == key);
|
|
|
|
if !already_exists {
|
|
debug!("Adding token with key {}", key);
|
|
to_add.push(token);
|
|
} else {
|
|
debug!("Token with key {} already exists, skipping", key);
|
|
}
|
|
}
|
|
|
|
// If there's no new proofs return early
|
|
if to_add.is_empty() {
|
|
return Err(AnyhowError::msg("No new validation tokens found in prd response"));
|
|
}
|
|
|
|
// We add the new tokens and return all that was updated
|
|
to_update.validation_tokens.extend(to_add);
|
|
|
|
let updated_state = to_update.clone();
|
|
|
|
let validated_state = Some(to_update.state_id);
|
|
let mut commit_msg = CommitMessage::new(
|
|
prd.process_id,
|
|
updated_state.pcd_commitment,
|
|
updated_state.roles,
|
|
updated_state.public_data,
|
|
updated_state.validation_tokens,
|
|
);
|
|
|
|
// We must return an update of the process
|
|
let updated_process = UpdatedProcess {
|
|
process_id: prd.process_id,
|
|
current_process: relevant_process.clone(),
|
|
validated_state,
|
|
..Default::default()
|
|
};
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
commit_to_send: Some(commit_msg),
|
|
..Default::default()
|
|
});
|
|
}
|
|
PrdType::Request => {
|
|
// We are being requested encrypted data for one or more states, to be uploaded on storage
|
|
let states: Vec<[u8; 32]> = serde_json::from_str(&prd.payload)?;
|
|
let requester = if let Some((requester_process_id, _)) = members_list.0.iter()
|
|
.find(|(outpoint, member)| {
|
|
**member == prd.sender
|
|
})
|
|
{
|
|
requester_process_id
|
|
} else {
|
|
return Err(AnyhowError::msg("Unknown requester"));
|
|
};
|
|
|
|
// diffs will trigger upload of the encrypted data on storage
|
|
let mut diffs = vec![];
|
|
// This will notify the requester and provide relevant information if needed
|
|
let mut ciphers = vec![];
|
|
let mut push_to_storage = vec![];
|
|
|
|
for state_id in states {
|
|
let state = match relevant_process.get_state_for_id(&state_id) {
|
|
Ok(state) => state,
|
|
Err(_) => {
|
|
debug!("Ignoring request for unknown state {}", state_id.to_lower_hex_string());
|
|
continue;
|
|
}
|
|
};
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_sp_client();
|
|
let local_address = sp_wallet.get_receiving_address().to_string();
|
|
|
|
let mut relevant_fields: HashSet<String> = HashSet::new();
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
for (name, role) in state.roles.iter() {
|
|
if !role.members.contains(&requester) {
|
|
// This role doesn't concern requester
|
|
continue;
|
|
}
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
relevant_fields.extend(fields);
|
|
}
|
|
|
|
let sender: Member = local_device
|
|
.to_member();
|
|
|
|
let mut full_prd = Prd::new_update(
|
|
outpoint,
|
|
sender,
|
|
state.roles.clone(),
|
|
state.public_data.clone(),
|
|
state.keys.clone(),
|
|
state.pcd_commitment.clone(),
|
|
);
|
|
|
|
full_prd.filter_keys(&relevant_fields);
|
|
let prd_msg = full_prd.to_network_msg(sp_wallet)?;
|
|
|
|
let addresses = prd.sender.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're answering a cipher
|
|
let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?)
|
|
.ok_or(AnyhowError::msg(format!("No secret for address {}", sp_address.as_str())))?;
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
|
|
let pcd_commitment = &state.pcd_commitment;
|
|
for (field, hash) in pcd_commitment.iter() {
|
|
// We only need field that are visible by requester
|
|
if !relevant_fields.contains(field.as_str()) {
|
|
continue;
|
|
}
|
|
let diff = UserDiff {
|
|
process_id: outpoint.to_string(),
|
|
state_id: state_id.to_lower_hex_string(),
|
|
value_commitment: hash.to_lower_hex_string(),
|
|
field: field.to_owned(),
|
|
..Default::default()
|
|
};
|
|
diffs.push(diff);
|
|
push_to_storage.push(hash.to_lower_hex_string());
|
|
}
|
|
}
|
|
|
|
let updated_process = Some(UpdatedProcess {
|
|
process_id: outpoint,
|
|
current_process: relevant_process.clone(),
|
|
diffs,
|
|
..Default::default()
|
|
});
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process,
|
|
ciphers_to_send: ciphers,
|
|
push_to_storage,
|
|
..Default::default()
|
|
});
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn handle_decrypted_message(
|
|
secret: AnkSharedSecretHash,
|
|
plain: Vec<u8>,
|
|
members_list: &OutPointMemberMap,
|
|
processes: &OutPointProcessMap
|
|
) -> anyhow::Result<ApiReturn> {
|
|
let local_address: SilentPaymentAddress = lock_local_device()?.get_address();
|
|
if let Ok(prd) = Prd::extract_from_message(&plain, local_address) {
|
|
handle_prd(prd, secret, members_list, processes)
|
|
} else {
|
|
Err(anyhow::Error::msg("Failed to handle decrypted message"))
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn parse_cipher(cipher_msg: String, members_list: OutPointMemberMap, processes: OutPointProcessMap) -> 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, &members_list, &processes)
|
|
.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()?;
|
|
Ok(JsValue::from_serde(device.get_outputs())?)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_available_amount() -> ApiResult<u64> {
|
|
let device = lock_local_device()?;
|
|
|
|
Ok(device.get_balance().to_sat())
|
|
}
|
|
|
|
fn get_shared_secrets_in_transaction(
|
|
unsigned_transaction: &SilentPaymentUnsignedTransaction,
|
|
sp_addresses: &[SilentPaymentAddress]
|
|
) -> anyhow::Result<HashMap<SilentPaymentAddress, AnkSharedSecretHash>> {
|
|
let mut new_secrets = HashMap::new();
|
|
for sp_address in sp_addresses {
|
|
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
|
|
&sp_address.get_scan_key(),
|
|
&unsigned_transaction.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(
|
|
device: &Device,
|
|
freezed_utxos: &HashSet<OutPoint>,
|
|
sp_addresses: &[SilentPaymentAddress],
|
|
fee_rate: FeeRate
|
|
) -> anyhow::Result<SilentPaymentUnsignedTransaction> {
|
|
let mut recipients = Vec::with_capacity(sp_addresses.len());
|
|
for sp_address in sp_addresses {
|
|
let recipient = Recipient {
|
|
address: RecipientAddress::SpAddress(*sp_address),
|
|
amount: DEFAULT_AMOUNT,
|
|
};
|
|
recipients.push(recipient);
|
|
}
|
|
|
|
// If we had mandatory inputs, we would add them at the top of the list
|
|
// Take spendable outputs and filter out the freezed utxos
|
|
let candidates_inputs: Vec<(OutPoint, OwnedOutput)> = device.get_outputs()
|
|
.into_iter()
|
|
.filter_map(|(outpoint, output)| {
|
|
if output.spend_status == OutputSpendStatus::Unspent && !freezed_utxos.contains(outpoint) {
|
|
Some((*outpoint, output.clone()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let mut tx = internal_create_transaction(
|
|
candidates_inputs,
|
|
device.get_sp_client(),
|
|
recipients,
|
|
None,
|
|
fee_rate,
|
|
)?;
|
|
|
|
let unsigned_transaction = SpClient::finalize_transaction(tx)?;
|
|
|
|
log::debug!("{:#?}", unsigned_transaction.unsigned_tx);
|
|
|
|
Ok(unsigned_transaction)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// We send a transaction that pays at least one output to each address
|
|
/// The goal can be to establish a shared_secret to be used as an encryption key for further communication
|
|
/// or if the recipient is a relay it can be the init transaction for a new process
|
|
pub fn create_transaction(addresses: Vec<String>, fee_rate: u32) -> ApiResult<ApiReturn> {
|
|
if addresses.is_empty() {
|
|
return Err(ApiError::new("No addresses to connect to".to_owned()));
|
|
}
|
|
|
|
let sp_addresses: anyhow::Result<Vec<SilentPaymentAddress>> = addresses.into_iter()
|
|
.map(|a| {
|
|
<SilentPaymentAddress>::try_from(a).map_err(|e| anyhow::Error::new(e))
|
|
})
|
|
.collect();
|
|
|
|
let sp_addresses = sp_addresses?;
|
|
|
|
let mut local_device = lock_local_device()?;
|
|
|
|
let mut freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let partial_tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &sp_addresses, FeeRate::from_sat_per_vb(fee_rate as f32))?;
|
|
let new_secrets = get_shared_secrets_in_transaction(&partial_tx, &sp_addresses)?;
|
|
|
|
let unsigned_tx = SpClient::finalize_transaction(partial_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);
|
|
}
|
|
|
|
let outputs = local_device.get_mut_outputs();
|
|
let new_txid = unsigned_tx.unsigned_tx.as_ref().unwrap().txid();
|
|
// We mark the utxos in the inputs as spent to prevent accidental double spends
|
|
for input in &unsigned_tx.unsigned_tx.as_ref().unwrap().input {
|
|
if let Some(output) = outputs.get_mut(&input.previous_output) {
|
|
output.spend_status = OutputSpendStatus::Spent(new_txid.to_byte_array());
|
|
}
|
|
}
|
|
|
|
Ok(ApiReturn {
|
|
secrets: Some(secrets_return),
|
|
partial_tx: Some(TsUnsignedTransaction::new(unsigned_tx)),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn sign_transaction(partial_tx: TsUnsignedTransaction) -> ApiResult<ApiReturn> {
|
|
let local_device = lock_local_device()?;
|
|
let partial_tweak = partial_tx.as_inner().partial_secret;
|
|
let tx = internal_sign_tx(local_device.get_sp_client(), partial_tx.to_inner())?;
|
|
let res = ApiReturn {
|
|
new_tx_to_send: Some(NewTxMessage::new(serialize(&tx).to_lower_hex_string(), Some(partial_tweak.secret_bytes().to_lower_hex_string()))),
|
|
..Default::default()
|
|
};
|
|
Ok(res)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_new_process(
|
|
private_data: Pcd,
|
|
roles: Roles,
|
|
public_data: Pcd,
|
|
relay_address: String,
|
|
fee_rate: u32,
|
|
members_list: OutPointMemberMap,
|
|
) -> ApiResult<ApiReturn> {
|
|
// At the very least we should have something in role
|
|
if roles.is_empty() {
|
|
return Err(ApiError { message: "Roles can't be empty".to_owned() });
|
|
}
|
|
// We create a transaction that spends to the relay address
|
|
let local_device = lock_local_device()?;
|
|
|
|
let mut freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let relay_address: SilentPaymentAddress = relay_address.try_into()?;
|
|
|
|
let fee_rate_checked = FeeRate::from_sat_per_vb(fee_rate as f32);
|
|
|
|
let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?;
|
|
|
|
let unsigned_transaction = SpClient::finalize_transaction(tx)?;
|
|
|
|
// We take the secret out
|
|
let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &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);
|
|
}
|
|
|
|
// We now have the outpoint that will serve as id for the whole process
|
|
let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0);
|
|
|
|
let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?;
|
|
|
|
let pcd_commitment = new_state.pcd_commitment.clone();
|
|
|
|
let mut process = Process::new(process_id);
|
|
|
|
let diffs = create_diffs(&local_device, &process, &new_state, &members_list)?;
|
|
|
|
let mut encrypted_data = BTreeMap::new();
|
|
|
|
let mut rng = thread_rng();
|
|
for (field, plain_value) in private_data.iter() {
|
|
let hash = pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?;
|
|
let key = generate_key(&mut rng);
|
|
let cipher = encrypt_with_key(&key, plain_value.as_slice())?;
|
|
new_state.keys.insert(field.to_owned(), key);
|
|
encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string());
|
|
}
|
|
|
|
process.insert_concurrent_state(new_state.clone())?;
|
|
|
|
let commit_msg = CommitMessage::new(
|
|
process_id,
|
|
pcd_commitment,
|
|
roles,
|
|
public_data,
|
|
vec![],
|
|
);
|
|
|
|
let updated_process = UpdatedProcess {
|
|
process_id,
|
|
current_process: process,
|
|
diffs,
|
|
encrypted_data,
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(ApiReturn {
|
|
secrets: Some(secrets_return),
|
|
commit_to_send: Some(commit_msg),
|
|
updated_process: Some(updated_process),
|
|
partial_tx: Some(TsUnsignedTransaction::new(unsigned_transaction)),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn update_process(
|
|
mut process: Process,
|
|
new_attributes: Pcd,
|
|
roles: Roles,
|
|
new_public_data: Pcd,
|
|
members_list: OutPointMemberMap,
|
|
) -> ApiResult<ApiReturn> {
|
|
let process_id = process.get_process_id()?;
|
|
|
|
let prev_state = process.get_latest_commited_state()
|
|
.ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?;
|
|
|
|
let mut prev_public_data = prev_state.public_data.clone();
|
|
for (field, value) in new_public_data.into_iter() {
|
|
prev_public_data.insert(field, value);
|
|
}
|
|
|
|
let mut new_state = ProcessState::new(
|
|
process.get_process_tip()?,
|
|
new_attributes.clone(),
|
|
prev_public_data,
|
|
roles.clone()
|
|
)?;
|
|
|
|
// TODO duplicate check is broken
|
|
// // We compare the new state with the previous one
|
|
// let last_state_merkle_root = &prev_state.state_id;
|
|
|
|
// if *last_state_merkle_root == new_state.state_id {
|
|
// 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.state_id == new_state.state_id) {
|
|
// return Err(ApiError::new("New state already known".to_owned()));
|
|
// }
|
|
|
|
let diffs = create_diffs(&lock_local_device()?, &process, &new_state, &members_list)?;
|
|
|
|
let all_fields: Vec<String> = new_attributes.iter().map(|(field, _)| field.clone()).collect();
|
|
let mut encrypted_data = BTreeMap::new();
|
|
|
|
let mut rng = thread_rng();
|
|
for (field, plain_value) in new_attributes.iter() {
|
|
let hash = new_state.pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?;
|
|
let key = generate_key(&mut rng);
|
|
let cipher = encrypt_with_key(&key, plain_value.as_slice())?;
|
|
new_state.keys.insert(field.to_owned(), key);
|
|
encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string());
|
|
}
|
|
|
|
// Add the new state to the process
|
|
process.insert_concurrent_state(new_state.clone())?;
|
|
|
|
let updated_process = UpdatedProcess {
|
|
process_id,
|
|
current_process: process,
|
|
diffs,
|
|
encrypted_data,
|
|
..Default::default()
|
|
};
|
|
|
|
let commit_msg = CommitMessage::new(
|
|
process_id,
|
|
new_state.pcd_commitment,
|
|
roles,
|
|
new_state.public_data,
|
|
vec![]
|
|
);
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
commit_to_send: Some(commit_msg),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn request_data(process_id: String, state_ids_str: Vec<String>, roles: JsValue, members_list: OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
let process_id = OutPoint::from_str(&process_id)?;
|
|
let local_device = lock_local_device()?;
|
|
let sender_pairing_id = local_device.get_pairing_commitment().ok_or(ApiError::new("Device not paired".to_owned()))?;
|
|
let local_address = local_device.get_address().to_string();
|
|
let roles: Vec<Roles> = serde_wasm_bindgen::from_value(roles)?;
|
|
|
|
let mut state_ids: Vec<[u8; 32]> = vec![];
|
|
for s in state_ids_str {
|
|
if (s.len() == 0 || s == String::from_utf8(Vec::from([0u8; 32])).unwrap()) { continue; }
|
|
let state_id: Result<[u8; 32], _> = Vec::from_hex(&s)?.try_into().map_err(|_| ApiError::new("Invalid state id".to_owned()));
|
|
if let Ok(state_id) = state_id {
|
|
state_ids.push(state_id);
|
|
}
|
|
}
|
|
|
|
let mut send_to: HashSet<SilentPaymentAddress> = HashSet::new();
|
|
for role in roles {
|
|
for (_, role_def) in role {
|
|
let pairing_ids = &role_def.members;
|
|
if !pairing_ids.contains(&sender_pairing_id) {
|
|
continue;
|
|
}
|
|
for pairing_id in pairing_ids {
|
|
if let Some(member) = members_list.0.get(pairing_id) {
|
|
for address in member.get_addresses() {
|
|
if address == local_address { continue };
|
|
send_to.insert(SilentPaymentAddress::try_from(address)?);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let prd_request = Prd::new_request(
|
|
process_id,
|
|
members_list.0.get(&sender_pairing_id).unwrap().clone(),
|
|
state_ids
|
|
);
|
|
|
|
let prd_msg = prd_request.to_network_msg(local_device.get_sp_client())?;
|
|
|
|
// For now, we just send the request to all members we share the data with, but this could be refined
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
let mut ciphers = vec![];
|
|
for address in send_to {
|
|
if let Some(secret) = shared_secrets.get_secret_for_address(address) {
|
|
let cipher = encrypt_with_key(secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
} else {
|
|
debug!("No shared secret");
|
|
}
|
|
}
|
|
|
|
Ok(ApiReturn {
|
|
ciphers_to_send: ciphers,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_update_message(
|
|
process: Process,
|
|
state_id: String,
|
|
members_list: OutPointMemberMap
|
|
) -> ApiResult<ApiReturn> {
|
|
let process_id = process.get_process_id()?;
|
|
let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?;
|
|
|
|
let update_state = process.get_state_for_id(&state_id)?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let local_address = local_device.get_address().to_string();
|
|
|
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
for (name, role) in update_state.roles.iter() {
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
for pairing_id in &role.members {
|
|
let member = if let Some(member) = members_list.0.get(pairing_id) {
|
|
member
|
|
} else {
|
|
continue;
|
|
};
|
|
// 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", pairing_id)));
|
|
}
|
|
}
|
|
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();
|
|
|
|
let full_prd = Prd::new_update(
|
|
process_id,
|
|
sender,
|
|
update_state.roles.clone(),
|
|
update_state.public_data.clone(),
|
|
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(local_device.get_sp_client())?;
|
|
|
|
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()?)
|
|
.ok_or(AnyhowError::msg("Failed to retrieve secret".to_owned()))?;
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
}
|
|
|
|
Ok(ApiReturn {
|
|
ciphers_to_send: ciphers,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn validate_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
add_validation_token(process, state_id, true, &members_list)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn refuse_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
add_validation_token(process, state_id, false, &members_list)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn evaluate_state(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state id".to_owned()))?;
|
|
let process_id = process.get_process_id()?;
|
|
let process_state = process.get_state_for_id(&state_id)?;
|
|
let previous_state = process.get_parent_state(&process_state.commited_in);
|
|
|
|
process_state.is_valid(previous_state, &members_list)?;
|
|
|
|
// We create a commit msg with the valid state
|
|
let commit_msg = CommitMessage::new(
|
|
process_id,
|
|
process_state.pcd_commitment.clone(),
|
|
process_state.roles.clone(),
|
|
process_state.public_data.clone(),
|
|
vec![]
|
|
);
|
|
|
|
Ok(ApiReturn {
|
|
commit_to_send: Some(commit_msg),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn add_validation_token(mut process: Process, state_id: String, approval: bool, members_list: &OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
let process_id = process.get_process_id()?;
|
|
let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?;
|
|
|
|
let update_state: &mut ProcessState = process.get_state_for_id_mut(&state_id)?;
|
|
|
|
let message_hash = if approval {
|
|
AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(state_id))
|
|
} else {
|
|
AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(state_id))
|
|
};
|
|
|
|
{
|
|
let local_device = lock_local_device()?;
|
|
let proof = Proof::new(message_hash, local_device.get_sp_client().get_spend_key().try_into()?);
|
|
update_state.validation_tokens.push(proof);
|
|
}
|
|
|
|
let mut commit_msg = CommitMessage::new(
|
|
process_id,
|
|
update_state.pcd_commitment.clone(),
|
|
update_state.roles.clone(),
|
|
update_state.public_data.clone(),
|
|
update_state.validation_tokens.clone()
|
|
);
|
|
|
|
let updated_process = UpdatedProcess {
|
|
process_id,
|
|
current_process: process.clone(),
|
|
validated_state: Some(state_id.clone()),
|
|
..Default::default()
|
|
};
|
|
|
|
let ciphers_to_send = new_response_prd(process_id, process.get_state_for_id(&state_id)?, members_list)?;
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
commit_to_send: Some(commit_msg),
|
|
ciphers_to_send,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_response_prd(process: Process, state_id: String, members_list: OutPointMemberMap) -> ApiResult<ApiReturn> {
|
|
let process_id = process.get_process_id()?;
|
|
let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?;
|
|
|
|
let update_state: &ProcessState = process.get_state_for_id(&state_id)?;
|
|
|
|
let ciphers = new_response_prd(process_id, update_state, &members_list)?;
|
|
|
|
Ok(ApiReturn {
|
|
ciphers_to_send: ciphers,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn new_response_prd(process_id: OutPoint, update_state: &ProcessState, members_list: &OutPointMemberMap) -> AnyhowResult<Vec<String>> {
|
|
let local_device = lock_local_device()?;
|
|
let local_address = local_device.get_address().to_string();
|
|
|
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
|
let shared_secrets = lock_shared_secrets()?;
|
|
for (name, role) in update_state.roles.iter() {
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
for pairing_id in &role.members {
|
|
let member = if let Some(member) = members_list.0.get(pairing_id) {
|
|
member
|
|
} else {
|
|
continue;
|
|
};
|
|
// 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(AnyhowError::msg(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(AnyhowError::msg("We haven't added our validation token yet".to_owned()))?;
|
|
|
|
let sender: Member = local_device
|
|
.to_member();
|
|
|
|
let response_prd = Prd::new_response(
|
|
process_id,
|
|
sender,
|
|
vec![*proof],
|
|
update_state.pcd_commitment.clone(),
|
|
);
|
|
let prd_msg = response_prd.to_network_msg(local_device.get_sp_client())?;
|
|
|
|
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()?)
|
|
.ok_or(AnyhowError::msg("Failed to retrieve secret".to_owned()))?;
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
}
|
|
|
|
Ok(ciphers)
|
|
}
|
|
|
|
#[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_sp_client()
|
|
.get_receiving_address()
|
|
.to_string();
|
|
|
|
let faucet_msg = FaucetMessage::new(sp_address);
|
|
|
|
Ok(faucet_msg.to_string())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_storages(process_outpoint: String) -> ApiResult<Vec<String>> {
|
|
let outpoint = OutPoint::from_str(&process_outpoint)?;
|
|
|
|
Ok(vec![])
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn is_child_role(parent_roles: String, child_roles: String) -> ApiResult<()> {
|
|
let parent_roles: BTreeMap<String, RoleDefinition> = serde_json::from_str(&parent_roles)?;
|
|
let child_roles: BTreeMap<String, RoleDefinition> = serde_json::from_str(&child_roles)?;
|
|
|
|
for (_, child_role) in &child_roles {
|
|
for child_member in &child_role.members {
|
|
let mut is_in_parent = false;
|
|
for (_, parent_role) in &parent_roles {
|
|
if parent_role.members.contains(&child_member) {
|
|
is_in_parent = true;
|
|
}
|
|
if is_in_parent { break }
|
|
}
|
|
if !is_in_parent {
|
|
return Err(ApiError::new("child role contains a member not in parent".to_owned()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn decrypt_data(key: &[u8], data: &[u8]) -> ApiResult<Vec<u8>> {
|
|
let mut key_buf = [0u8; 32];
|
|
|
|
if key.len() != 32 {
|
|
return Err(ApiError::new("key must be 32B long".to_owned()));
|
|
}
|
|
|
|
key_buf.copy_from_slice(key);
|
|
|
|
// decrypt the data
|
|
let clear = decrypt_with_key(&key_buf, data)?;
|
|
|
|
Ok(clear)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn encode_binary(data: JsValue) -> ApiResult<Pcd> {
|
|
let map: BTreeMap<String, FileBlob> =
|
|
serde_wasm_bindgen::from_value(data)?;
|
|
|
|
let res = TryInto::<Pcd>::try_into(map)?;
|
|
Ok(res)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn encode_json(json_data: JsValue) -> ApiResult<Pcd> {
|
|
let value: Value = serde_wasm_bindgen::from_value(json_data)?;
|
|
let res = TryInto::<Pcd>::try_into(value)?;
|
|
Ok(res)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn decode_value(value: Vec<u8>) -> ApiResult<JsValue> {
|
|
// Try FileBlob first
|
|
if let Ok(file_blob) = sdk_common::pcd::FileBlob::deserialize_from_pcd(&value) {
|
|
let u8IntArray = Uint8Array::from(file_blob.data.as_slice());
|
|
let res = Object::new();
|
|
Reflect::set(&res, &JsValue::from_str("type"), &JsValue::from_str(&file_blob.r#type)).unwrap();
|
|
Reflect::set(&res, &JsValue::from_str("data"), &JsValue::from(u8IntArray)).unwrap();
|
|
return Ok(JsValue::from(res));
|
|
}
|
|
// Try JSON next
|
|
if let Ok(json) = serde_json::Value::deserialize_from_pcd(&value) {
|
|
return Ok(serde_wasm_bindgen::to_value(&json)?);
|
|
}
|
|
Err(ApiError::new("Invalid or unsupported PCD data".to_owned()))
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn hash_value(value: JsValue, commited_in: String, label: String) -> ApiResult<String> {
|
|
let outpoint = OutPoint::from_str(&commited_in)?;
|
|
let encoded_value = if let Ok(file_blob) = serde_wasm_bindgen::from_value::<FileBlob>(value.clone()) {
|
|
file_blob.serialize_to_pcd()?
|
|
} else if let Ok(json) = serde_wasm_bindgen::from_value::<Value>(value) {
|
|
json.serialize_to_pcd()?
|
|
} else {
|
|
return Err(ApiError::new("Invalid or unsupported PCD data".to_owned()));
|
|
};
|
|
let hash = AnkPcdHash::from_pcd_value(encoded_value.as_slice(), label.as_bytes(), &outpoint);
|
|
Ok(hash.as_byte_array().to_lower_hex_string())
|
|
}
|
|
|
|
#[derive(Tsify, Serialize, Deserialize)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct MerkleProofResult {
|
|
pub proof: String,
|
|
pub root: String,
|
|
pub attribute: String,
|
|
pub attribute_index: usize,
|
|
pub total_leaves_count: usize,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// Generate a merkle proof for a specific attribute in a process state.
|
|
///
|
|
/// This function creates a merkle proof that proves the existence of a specific attribute
|
|
/// in a given state of a process. The proof can be used to verify that the attribute
|
|
/// was indeed part of the state without revealing the entire state.
|
|
///
|
|
/// # Arguments
|
|
/// * `process_state` - The process state object as a JavaScript value
|
|
/// * `attribute_name` - The name of the attribute to generate a proof for
|
|
///
|
|
/// # Returns
|
|
/// A MerkleProofResult object containing:
|
|
/// * `proof` - The merkle proof as a hex string
|
|
/// * `root` - The merkle root (state_id) as a hex string
|
|
/// * `attribute` - The attribute name that was proven
|
|
/// * `attribute_index` - The index of the attribute in the merkle tree
|
|
/// * `total_leaves_count` - The total number of leaves in the merkle tree
|
|
///
|
|
/// # Errors
|
|
/// * "Failed to deserialize process state" - If the process state cannot be deserialized from JsValue
|
|
/// * "Attribute not found in state" - If the attribute doesn't exist in the state
|
|
pub fn get_merkle_proof(process_state: JsValue, attribute_name: String) -> ApiResult<MerkleProofResult> {
|
|
// Deserialize the process state from JsValue
|
|
let state: ProcessState = serde_wasm_bindgen::from_value(process_state)
|
|
.map_err(|_| ApiError::new("Failed to deserialize process state".to_owned()))?;
|
|
|
|
// Create merkle tree from the PCD commitments
|
|
let merkle_tree = state.pcd_commitment.create_merkle_tree()?;
|
|
|
|
// Find the index of the attribute in the commitments
|
|
let attribute_index = state.pcd_commitment.find_index_of(&attribute_name)
|
|
.ok_or(ApiError::new("Attribute not found in state".to_owned()))?;
|
|
|
|
// Generate the merkle proof for the attribute
|
|
let proof = merkle_tree.proof(&[attribute_index]);
|
|
|
|
// Convert the proof to a format that can be serialized to JavaScript
|
|
let proof_bytes = proof.to_bytes();
|
|
let proof_hex = proof_bytes.to_lower_hex_string();
|
|
|
|
Ok(MerkleProofResult {
|
|
proof: proof_hex,
|
|
root: state.state_id.to_lower_hex_string(),
|
|
attribute: attribute_name,
|
|
attribute_index: attribute_index,
|
|
total_leaves_count: merkle_tree.leaves_len(),
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// Validate a merkle proof for a specific attribute.
|
|
///
|
|
/// This function verifies that a merkle proof is valid and proves the existence
|
|
/// of a specific attribute in a given state. It checks that the proof correctly
|
|
/// leads to the claimed root when combined with the attribute hash.
|
|
///
|
|
/// # Arguments
|
|
/// * `proof_result` - a JsValue expected to contain a MerkleProofResult with the proof and metadata
|
|
/// * `hash` - The hash of the attribute data as a hex string (the leaf value)
|
|
///
|
|
/// # Returns
|
|
/// A boolean indicating whether the proof is valid
|
|
///
|
|
/// # Errors
|
|
/// * "serde_wasm_bindgen deserialization error" - If the proof is not a valid MerkleProofResult
|
|
/// * "Invalid proof format" - If the proof cannot be parsed
|
|
/// * "Invalid hash format" - If the hash is not a valid 32-byte hex string
|
|
/// * "Invalid root format" - If the root is not a valid 32-byte hex string
|
|
pub fn validate_merkle_proof(proof_result: JsValue, hash: String) -> ApiResult<bool> {
|
|
let proof_result: MerkleProofResult = serde_wasm_bindgen::from_value(proof_result)?;
|
|
let root_bytes: [u8; 32] = Vec::from_hex(&proof_result.root)?
|
|
.try_into()
|
|
.map_err(|_| ApiError::new("Invalid root format".to_owned()))?;
|
|
|
|
let proof_bytes = Vec::from_hex(&proof_result.proof)
|
|
.map_err(|_| ApiError::new("Invalid proof format".to_owned()))?;
|
|
|
|
let index = proof_result.attribute_index;
|
|
let total_leaves_count = proof_result.total_leaves_count;
|
|
|
|
let hash_bytes: [u8; 32] = Vec::from_hex(&hash)?.try_into()
|
|
.map_err(|_| ApiError::new("Invalid hash format".to_owned()))?;
|
|
|
|
let res = verify_merkle_proof(&proof_bytes, &root_bytes, index, &hash_bytes, total_leaves_count)?;
|
|
|
|
Ok(res)
|
|
}
|