1475 lines
48 KiB
Rust
1475 lines
48 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, 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::{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, MAX_PRD_PAYLOAD_SIZE};
|
|
use serde_json::{Error as SerdeJsonError, Map, Value};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use tsify::{JsValueSerdeExt, Tsify};
|
|
use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
use sdk_common::device::Device;
|
|
use sdk_common::network::{
|
|
self, AnkFlag, CachedMessage, CachedMessageStatus, CommitMessage, Envelope, FaucetMessage,
|
|
NewTxMessage,
|
|
};
|
|
use sdk_common::pcd::{
|
|
compare_maps, 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 crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE};
|
|
use crate::wallet::{generate_sp_wallet, lock_freezed_utxos};
|
|
use crate::{lock_messages, CACHEDMESSAGES};
|
|
|
|
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct ApiReturn {
|
|
pub updated_cached_msg: Vec<CachedMessage>,
|
|
pub updated_process: Option<(String, Process)>,
|
|
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>;
|
|
|
|
const IS_TESTNET: bool = true;
|
|
const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000);
|
|
|
|
#[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 restore_device(device_str: String) -> ApiResult<()> {
|
|
let device: Device = serde_json::from_str(&device_str)?;
|
|
|
|
let mut local_device = lock_local_device()?;
|
|
|
|
*local_device = device;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_device_from_sp_wallet(sp_wallet: String) -> ApiResult<String> {
|
|
let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?;
|
|
|
|
let our_address = set_new_device(sp_wallet)?;
|
|
|
|
Ok(our_address)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult<String> {
|
|
let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?;
|
|
|
|
let our_address = set_new_device(sp_wallet)?;
|
|
|
|
Ok(our_address)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec<String>) -> ApiResult<()> {
|
|
let mut local_device = lock_local_device()?;
|
|
|
|
if local_device.is_linked() {
|
|
return Err(ApiError::new("Already paired".to_owned()));
|
|
}
|
|
|
|
sp_addresses.push(
|
|
local_device
|
|
.get_wallet()
|
|
.get_client()
|
|
.get_receiving_address(),
|
|
);
|
|
|
|
local_device.pair(
|
|
OutPoint::from_str(&commitment_tx)?.txid,
|
|
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_message_cache() -> ApiResult<()> {
|
|
let mut cached_msg = lock_messages()?;
|
|
|
|
*cached_msg = vec![];
|
|
|
|
debug_assert!(cached_msg.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn set_message_cache(msg_cache: Vec<String>) -> ApiResult<()> {
|
|
let mut cached_msg = lock_messages()?;
|
|
|
|
if !cached_msg.is_empty() {
|
|
return Err(ApiError::new("Message cache not empty".to_owned()));
|
|
}
|
|
|
|
let new_cache: Result<Vec<CachedMessage>, serde_json::Error> =
|
|
msg_cache.iter().map(|m| serde_json::from_str(m)).collect();
|
|
|
|
*cached_msg = new_cache?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_message_cache() -> ApiResult<Vec<String>> {
|
|
let cached_msg = lock_messages()?;
|
|
|
|
let res: Vec<String> = cached_msg
|
|
.iter()
|
|
.map(|m| serde_json::to_string(m).unwrap())
|
|
.collect();
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn dump_device() -> ApiResult<String> {
|
|
let local_device = lock_local_device()?;
|
|
|
|
Ok(serde_json::to_string(&local_device.clone())?)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn reset_device() -> ApiResult<()> {
|
|
let mut device = lock_local_device()?;
|
|
|
|
*device = Device::default();
|
|
|
|
reset_message_cache()?;
|
|
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,
|
|
tweak_data: PublicKey,
|
|
) -> AnyhowResult<ApiReturn> {
|
|
let b_scan = lock_local_device()?.get_wallet().get_client().get_scan_key();
|
|
|
|
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 messages = lock_messages()?;
|
|
|
|
// empty utxo_destroyed means we received this transaction
|
|
if utxo_destroyed.is_empty() {
|
|
let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret(
|
|
&tweak_data,
|
|
&b_scan,
|
|
);
|
|
let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point);
|
|
|
|
let mut plaintext: Vec<u8> = vec![];
|
|
if let Some(message) = messages.iter_mut().find(|m| {
|
|
if m.status != CachedMessageStatus::CipherWaitingTx {
|
|
return false;
|
|
}
|
|
let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array());
|
|
if res.is_ok() {
|
|
plaintext = res.unwrap();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}) {
|
|
// Calling this check that the prd we found check with the hashed commitment in transaction
|
|
// We also check the signed proof that is included in the prd
|
|
let prd = Prd::extract_from_message_with_commitment(&plaintext, &commitment)?;
|
|
|
|
// for now the previous method doesn't error if proof is missing,
|
|
// We must define if there are cases where a valid prd doesn't have proof
|
|
|
|
let outpoint = OutPoint::from_str(&prd.root_commitment)?;
|
|
|
|
handle_decrypted_message(plaintext, Some(shared_secret), Some(outpoint))
|
|
} else {
|
|
// store it and wait for the message
|
|
let mut new_msg = CachedMessage::new();
|
|
new_msg.commitment = Some(commitment.as_byte_array().to_lower_hex_string());
|
|
new_msg
|
|
.shared_secrets
|
|
.push(shared_secret.to_byte_array().to_lower_hex_string());
|
|
new_msg.status = CachedMessageStatus::TxWaitingPrd;
|
|
|
|
messages.push(new_msg.clone());
|
|
|
|
return Ok(ApiReturn {
|
|
updated_cached_msg: vec![new_msg.clone()],
|
|
..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, fee_rate: 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(),
|
|
)?)
|
|
}
|
|
|
|
fn try_decrypt_with_processes(
|
|
cipher: &[u8],
|
|
processes: MutexGuard<HashMap<OutPoint, Process>>,
|
|
) -> Option<(Vec<u8>, SilentPaymentAddress, OutPoint)> {
|
|
let nonce = Nonce::from_slice(&cipher[..12]);
|
|
|
|
for (outpoint, process) in processes.iter() {
|
|
for (address, secret) in process.get_all_secrets() {
|
|
let engine = Aes256Gcm::new(&secret.to_byte_array().into());
|
|
if let Ok(plain) = engine.decrypt(
|
|
&nonce,
|
|
Payload {
|
|
msg: &cipher[12..],
|
|
aad: AAD,
|
|
},
|
|
) {
|
|
return Some((plain, address, *outpoint));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// Produce a proof and append it to a prd
|
|
pub fn add_validation_token_to_prd(
|
|
root_commitment: String,
|
|
prd_commitment: String,
|
|
approval: bool,
|
|
) -> ApiResult<ApiReturn> {
|
|
let prd_hash = AnkPrdHash::from_str(&prd_commitment)?;
|
|
let outpoint = OutPoint::from_str(&root_commitment)?;
|
|
|
|
// 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(),
|
|
))?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let wallet = local_device.get_wallet();
|
|
|
|
let spend_key: SecretKey = wallet.get_client().get_spend_key().try_into()?;
|
|
|
|
match prd_ref.prd_type {
|
|
PrdType::Update => {
|
|
let new_state = Value::from_str(&prd_ref.payload)?;
|
|
|
|
let new_state_commitment = new_state.tagged_hash();
|
|
|
|
let message_hash = if approval {
|
|
AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_commitment))
|
|
} else {
|
|
AnkHash::ValidationNo(AnkValidationNoHash::from_commitment(new_state_commitment))
|
|
};
|
|
|
|
let proof = Proof::new(message_hash, spend_key);
|
|
|
|
prd_ref.validation_tokens.push(proof);
|
|
|
|
Ok(ApiReturn {
|
|
updated_process: Some((root_commitment, process.clone())),
|
|
..Default::default()
|
|
})
|
|
}
|
|
_ => return Err(ApiError::new("Can't validate that prd".to_owned())),
|
|
}
|
|
}
|
|
|
|
#[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()
|
|
.ok_or(ApiError::new("Unpaired device".to_owned()))?;
|
|
|
|
// 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_hash: AnkPcdHash = AnkPcdHash::from_value(&Value::from_str(&prd_ref.payload)?);
|
|
|
|
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 mut ciphers = vec![];
|
|
for (sp_address, shared_secret) in process.get_all_secrets() {
|
|
if sp_address.to_string()
|
|
== local_device
|
|
.get_wallet()
|
|
.get_client()
|
|
.get_receiving_address()
|
|
{
|
|
continue;
|
|
}
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
}
|
|
|
|
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 member = match local_device.to_member() {
|
|
Some(member) => member,
|
|
None => {
|
|
// This might be because we're pairing, let's see if our address is part of sender of the initial prd
|
|
let remote_member: Member = serde_json::from_str(&prd.sender)?;
|
|
let addresses = remote_member.get_addresses();
|
|
let this_device_address = local_device
|
|
.get_wallet()
|
|
.get_client()
|
|
.get_receiving_address();
|
|
if let Some(_) = addresses.into_iter().find(|a| *a == this_device_address) {
|
|
remote_member
|
|
} else {
|
|
return Err(AnyhowError::msg("Must pair device first"));
|
|
}
|
|
}
|
|
};
|
|
|
|
let pcd_commitment = AnkPcdHash::from_str(&prd.payload)?;
|
|
|
|
let prd_confirm = Prd::new_confirm(outpoint, member, pcd_commitment);
|
|
|
|
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 send_data(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult<ApiReturn> {
|
|
let pcd = &prd.payload;
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), pcd.as_bytes())?;
|
|
|
|
Ok(ApiReturn {
|
|
ciphers_to_send: vec![cipher.to_lower_hex_string()],
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn decrypt_with_cached_messages(
|
|
cipher: &[u8],
|
|
messages: &mut MutexGuard<Vec<CachedMessage>>
|
|
) -> anyhow::Result<Option<(Vec<u8>, AnkSharedSecretHash)>> {
|
|
let nonce = Nonce::from_slice(&cipher[..12]);
|
|
|
|
for message in messages.iter_mut() {
|
|
for shared_secret in message.shared_secrets.iter() {
|
|
let aes_key = match AnkSharedSecretHash::from_str(shared_secret) {
|
|
Ok(key) => key,
|
|
Err(_) => {
|
|
debug!(
|
|
"Invalid shared secret for message{}: {}",
|
|
message.id, shared_secret
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let engine = Aes256Gcm::new(aes_key.as_byte_array().into());
|
|
|
|
let plain = match engine.decrypt(
|
|
&nonce,
|
|
Payload {
|
|
msg: &cipher[12..],
|
|
aad: AAD,
|
|
},
|
|
) {
|
|
Ok(plain) => plain,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let commitment = AnkPrdHash::from_str(
|
|
message
|
|
.commitment
|
|
.as_ref()
|
|
.ok_or(anyhow::Error::msg("Missing commitment".to_owned()))?,
|
|
)?;
|
|
// A message matched against a new transaction must be a prd
|
|
// We just check the commitment while we're at it
|
|
let _ = Prd::extract_from_message_with_commitment(&plain, &commitment)?;
|
|
// Update the message status
|
|
message.status = CachedMessageStatus::NoStatus;
|
|
message.shared_secrets = vec![]; // this way we won't check it again
|
|
|
|
return Ok(Some((
|
|
plain,
|
|
aes_key,
|
|
)));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
fn decrypt_with_known_processes(cipher: &[u8], processes: MutexGuard<HashMap<OutPoint, Process>>) -> anyhow::Result<Option<(Vec<u8>, OutPoint)>> {
|
|
let nonce = Nonce::from_slice(&cipher[..12]);
|
|
|
|
for (outpoint, process) in processes.iter() {
|
|
for (address, secret) in process.get_all_secrets() {
|
|
debug!("Attempting decryption with key {} for {}", secret, address);
|
|
let engine = Aes256Gcm::new(secret.as_byte_array().into());
|
|
|
|
if let Ok(plain) = engine.decrypt(
|
|
&nonce,
|
|
Payload {
|
|
msg: &cipher[12..],
|
|
aad: AAD,
|
|
},
|
|
) {
|
|
return Ok(Some((plain, *outpoint)));
|
|
}
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Prd can come with an attached transaction or without
|
|
/// It's useful to commit it in a transaction in the case there are multiple recipients (guarantee that everyone gets the same payload)
|
|
/// Or if for some reasons we don't want to use the same shared secret again
|
|
fn handle_prd(
|
|
plain: &[u8],
|
|
new_shared_secret: Option<AnkSharedSecretHash>,
|
|
) -> AnyhowResult<ApiReturn> {
|
|
// We already checked the commitment if any
|
|
let prd = Prd::extract_from_message(plain)?;
|
|
|
|
debug!("found prd: {:#?}", prd);
|
|
|
|
let proof_key = prd.proof.unwrap().get_key();
|
|
|
|
let sp_address = serde_json::from_str::<Member>(&prd.sender)?
|
|
.get_addresses()
|
|
.into_iter()
|
|
.find_map(|address| {
|
|
let parsed_address = SilentPaymentAddress::try_from(address.as_str()).ok()?;
|
|
let spend_key = parsed_address.get_spend_key().x_only_public_key().0;
|
|
if spend_key == proof_key {
|
|
Some(parsed_address)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.ok_or_else(|| anyhow::Error::msg("No matching address found for the proof key"))?;
|
|
|
|
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);
|
|
let shared_secret = new_shared_secret
|
|
.ok_or_else(|| anyhow::Error::msg("Missing shared secret for new process"))?;
|
|
let mut shared_secrets = HashMap::new();
|
|
shared_secrets.insert(sp_address, shared_secret);
|
|
entry.insert(Process::new(vec![], shared_secrets, vec![]))
|
|
}
|
|
};
|
|
|
|
match prd.prd_type {
|
|
PrdType::Confirm => {
|
|
// It must match a prd we sent previously
|
|
// We send the whole data in a pcd
|
|
debug!("Received confirm prd {:#?}", prd);
|
|
let original_request = relevant_process
|
|
.get_impending_requests()
|
|
.into_iter()
|
|
.find(|r| {
|
|
if r.prd_type != PrdType::Update {
|
|
return false;
|
|
}
|
|
let hash = Value::from_str(&r.payload).unwrap().tagged_hash();
|
|
hash.to_string() == prd.payload
|
|
})
|
|
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
|
let shared_secret = relevant_process
|
|
.get_shared_secret_for_address(&sp_address)
|
|
.ok_or(anyhow::Error::msg(
|
|
"Missing shared secret for address in original request",
|
|
))?;
|
|
|
|
return send_data(original_request, &shared_secret);
|
|
}
|
|
PrdType::Update | PrdType::TxProposal | PrdType::Message => {
|
|
// Those all have some new data we don't know about yet
|
|
// We send a Confirm to get the pcd
|
|
// Add the prd to our list of actions for this process
|
|
relevant_process.insert_impending_request(prd.clone());
|
|
let shared_secret = relevant_process
|
|
.get_shared_secret_for_address(&sp_address)
|
|
.ok_or(anyhow::Error::msg(
|
|
"Missing shared secret for address in original request",
|
|
))?;
|
|
|
|
let cipher = confirm_prd(prd, &shared_secret)?;
|
|
|
|
return Ok(ApiReturn {
|
|
ciphers_to_send: vec![cipher],
|
|
updated_process: Some((outpoint.to_string(), relevant_process.clone())),
|
|
..Default::default()
|
|
});
|
|
}
|
|
PrdType::Response => {
|
|
// We must know of a prd update that the response answers to
|
|
let original_request = relevant_process
|
|
.get_impending_requests_mut()
|
|
.into_iter()
|
|
.find(|r| {
|
|
if r.prd_type != PrdType::Update {
|
|
return false;
|
|
}
|
|
let hash = Value::from_str(&r.payload).unwrap().tagged_hash();
|
|
hash.to_string() == prd.payload
|
|
})
|
|
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
|
|
|
// Once we found the prd update, we can add the received proofs as validation tokens
|
|
original_request
|
|
.validation_tokens
|
|
.extend(prd.validation_tokens);
|
|
|
|
// We must return an update of the process
|
|
let updated_process = (prd.root_commitment, relevant_process.clone());
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some(updated_process),
|
|
..Default::default()
|
|
});
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn handle_pcd(plain: Vec<u8>, root_commitment: OutPoint) -> AnyhowResult<ApiReturn> {
|
|
let pcd = Value::from_str(&String::from_utf8(plain)?)?;
|
|
|
|
let pcd_commitment = pcd.tagged_hash();
|
|
|
|
let mut processes = lock_processes()?;
|
|
let relevant_process = processes.get_mut(&root_commitment).unwrap();
|
|
|
|
// We match the pcd with a prd and act accordingly
|
|
let prd = relevant_process
|
|
.get_impending_requests_mut()
|
|
.into_iter()
|
|
.find(|r| *r.payload == pcd_commitment.to_string())
|
|
.ok_or(AnyhowError::msg("Failed to retrieve the matching prd"))?;
|
|
|
|
// We update the process and return it
|
|
prd.payload = pcd.to_string();
|
|
|
|
return Ok(ApiReturn {
|
|
updated_process: Some((root_commitment.to_string(), relevant_process.clone())),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
fn handle_decrypted_message(
|
|
plain: Vec<u8>,
|
|
shared_secret: Option<AnkSharedSecretHash>,
|
|
root_commitment: Option<OutPoint>,
|
|
) -> anyhow::Result<ApiReturn> {
|
|
// Try to handle as PRD first
|
|
handle_prd(&plain, shared_secret)
|
|
.or_else(|_| {
|
|
// If PRD handling fails, try to handle as PCD
|
|
handle_pcd(
|
|
plain,
|
|
root_commitment.ok_or(anyhow::Error::msg(
|
|
"root_commitment must be known for a pcd",
|
|
))?,
|
|
)
|
|
})
|
|
.map_err(|e| anyhow::Error::msg(format!("Failed to handle decrypted message: {}", e)))
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn parse_cipher(cipher_msg: String) -> ApiResult<ApiReturn> {
|
|
// We lock message cache and processes to prevent race conditions
|
|
let mut messages = lock_messages()?;
|
|
let processes = lock_processes()?;
|
|
|
|
// 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('"'))?;
|
|
|
|
// Try decrypting with cached messages first
|
|
if let Ok(Some((plain, shared_secret))) = decrypt_with_cached_messages(&cipher, &mut messages) {
|
|
return handle_decrypted_message(plain, Some(shared_secret), None)
|
|
.map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e)));
|
|
}
|
|
|
|
// If that fails, try decrypting with known processes
|
|
if let Ok(Some((plain, root_commitment))) = decrypt_with_known_processes(&cipher, processes) {
|
|
return handle_decrypted_message(plain, None, Some(root_commitment))
|
|
.map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e)));
|
|
}
|
|
|
|
// If both decryption attempts fail, we keep it just in case we receive the transaction later
|
|
let mut return_msg = CachedMessage::new();
|
|
return_msg.cipher = vec![cipher_msg];
|
|
return_msg.status = CachedMessageStatus::CipherWaitingTx;
|
|
|
|
messages.push(return_msg.clone());
|
|
|
|
Ok(ApiReturn {
|
|
updated_cached_msg: vec![return_msg],
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_outputs() -> ApiResult<JsValue> {
|
|
let device = lock_local_device()?;
|
|
let outputs = device.get_wallet().get_outputs().clone();
|
|
Ok(JsValue::from_serde(&outputs.to_outpoints_list())?)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn get_available_amount() -> ApiResult<u64> {
|
|
let device = lock_local_device()?;
|
|
|
|
Ok(device.get_wallet().get_outputs().get_balance().to_sat())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// This takes a reference to a process and creates a commit msg for the latest state
|
|
pub fn create_commit_message(
|
|
init_commitment_outpoint: String,
|
|
relay_address: String,
|
|
fee_rate: u32,
|
|
) -> ApiResult<ApiReturn> {
|
|
let outpoint = OutPoint::from_str(&init_commitment_outpoint)?;
|
|
|
|
if let Some(process) = lock_processes()?.get(&outpoint) {
|
|
match process.get_number_of_states() {
|
|
0 => Err(ApiError::new("Process has no states".to_owned())),
|
|
1 => {
|
|
// This is a creation
|
|
let state = process.get_latest_state().unwrap();
|
|
if state.commited_in.vout != u32::MAX {
|
|
return Err(ApiError::new("Latest state is already commited".to_owned()));
|
|
}
|
|
let encrypted_pcd = state.encrypted_pcd.clone();
|
|
let keys = state.keys.clone();
|
|
|
|
let freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_wallet();
|
|
|
|
let signed_psbt = create_transaction(
|
|
vec![],
|
|
&freezed_utxos,
|
|
sp_wallet,
|
|
vec![Recipient {
|
|
address: relay_address,
|
|
amount: Amount::from_sat(1000),
|
|
nb_outputs: 1,
|
|
}],
|
|
None,
|
|
Amount::from_sat(fee_rate.into()),
|
|
None,
|
|
)?;
|
|
|
|
let tx = signed_psbt.extract_tx()?;
|
|
|
|
Ok(ApiReturn {
|
|
commit_to_send: Some(CommitMessage::new_first_commitment(
|
|
tx,
|
|
encrypted_pcd.as_object().unwrap().clone(),
|
|
keys,
|
|
)),
|
|
..Default::default()
|
|
})
|
|
}
|
|
_ => {
|
|
// We're updating an existing process
|
|
// Check that initial outpoint is not a placeholder and that latest state has a commited_in of null
|
|
if outpoint.vout != u32::MAX {
|
|
return Err(ApiError::new(
|
|
"Initial outpoint is a placeholder".to_owned(),
|
|
));
|
|
}
|
|
let state = process.get_latest_state().unwrap();
|
|
if state.commited_in != OutPoint::null() {
|
|
return Err(ApiError::new("Latest state is already commited".to_owned()));
|
|
}
|
|
let encrypted_pcd = state.encrypted_pcd.clone();
|
|
let keys = state.keys.clone();
|
|
// We just send the message with the outpoint
|
|
return Ok(ApiReturn {
|
|
commit_to_send: Some(CommitMessage::new_update_commitment(
|
|
outpoint,
|
|
encrypted_pcd.as_object().unwrap().clone(),
|
|
keys,
|
|
)),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
return Err(ApiError::new("Process not found".to_owned()));
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
/// We assume that the provided tx outpoint exist
|
|
pub fn create_update_transaction(
|
|
init_commitment: Option<String>,
|
|
new_state: String,
|
|
fee_rate: u32,
|
|
) -> ApiResult<ApiReturn> {
|
|
let pcd = Value::from_str(&new_state)?;
|
|
let pcd_map = pcd
|
|
.as_object()
|
|
.ok_or(ApiError::new("new_state must be an object".to_owned()))?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
|
|
let commitment_outpoint: OutPoint;
|
|
let relevant_process: &mut Process;
|
|
if let Some(s) = init_commitment {
|
|
// We're updating an existing contract
|
|
let outpoint = OutPoint::from_str(&s)?;
|
|
|
|
if let Some(p) = processes.get_mut(&outpoint) {
|
|
// compare the provided new_state with the process defined template
|
|
let previous_state = &p.get_state_at(0).unwrap().encrypted_pcd;
|
|
if !compare_maps(previous_state.as_object().unwrap(), pcd_map) {
|
|
return Err(ApiError::new(
|
|
"Provided updated state is not consistent with the process template".to_owned(),
|
|
));
|
|
}
|
|
relevant_process = p;
|
|
commitment_outpoint = outpoint;
|
|
} else {
|
|
// This is a process we don't know about, so we insert a new entry
|
|
processes.insert(outpoint, Process::default());
|
|
relevant_process = processes.get_mut(&outpoint).unwrap();
|
|
commitment_outpoint = outpoint;
|
|
}
|
|
} else {
|
|
// This is a creation with an init state, the commitment will come later
|
|
// We need a placeholder to keep track of the process before it's commited on chain
|
|
// We can take the hash of the init_state as a txid, and set the vout to the max as it is very unlikely to ever have a real commitment that will look like this
|
|
let dummy = pcd.tagged_hash();
|
|
|
|
let dummy_outpoint = OutPoint::new(Txid::from_slice(dummy.as_byte_array())?, u32::MAX);
|
|
|
|
processes.insert(dummy_outpoint, Process::default());
|
|
|
|
relevant_process = processes.get_mut(&dummy_outpoint).unwrap();
|
|
commitment_outpoint = dummy_outpoint;
|
|
}
|
|
|
|
// We assume that all processes must have a roles key
|
|
let roles = pcd
|
|
.get("roles")
|
|
.ok_or(ApiError::new("No roles in new_state".to_owned()))?;
|
|
let roles_map = roles
|
|
.as_object()
|
|
.ok_or(ApiError::new("roles is not an object".to_owned()))?
|
|
.clone();
|
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
|
for (name, role_def) in roles_map {
|
|
let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?;
|
|
let fields: Vec<String> = role
|
|
.validation_rules
|
|
.iter()
|
|
.flat_map(|rule| rule.fields.clone())
|
|
.collect();
|
|
for member in role.members {
|
|
if !all_members.contains_key(&member) {
|
|
all_members.insert(member.clone(), HashSet::new());
|
|
}
|
|
all_members.get_mut(&member).unwrap().extend(fields.clone());
|
|
}
|
|
}
|
|
|
|
let nb_recipients = all_members.len();
|
|
if nb_recipients == 0 {
|
|
return Err(ApiError::new(
|
|
"Can't create a process with 0 member".to_owned(),
|
|
));
|
|
}
|
|
|
|
let mut recipients: Vec<Recipient> = Vec::with_capacity(nb_recipients * 2); // We suppose that will work most of the time
|
|
// we actually have multiple "recipients" in a technical sense for each social recipient
|
|
// that's necessary because we don't want to miss a notification because we don't have a device atm
|
|
for member in all_members.keys() {
|
|
let addresses = member.get_addresses();
|
|
for sp_address in addresses.into_iter() {
|
|
recipients.push(Recipient {
|
|
address: sp_address.into(),
|
|
amount: DEFAULT_AMOUNT,
|
|
nb_outputs: 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
let mut fields2keys = Map::new();
|
|
let mut fields2cipher = Map::new();
|
|
let encrypted_pcd = pcd.clone();
|
|
let fields_to_encrypt: Vec<String> = encrypted_pcd
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.map(|k| k.clone())
|
|
.collect();
|
|
encrypted_pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher);
|
|
|
|
let local_device = lock_local_device()?;
|
|
|
|
let sp_wallet = local_device.get_wallet();
|
|
|
|
let sender: Member = local_device
|
|
.to_member()
|
|
.ok_or(ApiError::new("unpaired device".to_owned()))?;
|
|
|
|
// We first generate the prd with all the keys that we will keep to ourselves
|
|
let full_prd = Prd::new_update(
|
|
commitment_outpoint,
|
|
serde_json::to_string(&sender)?,
|
|
fields2cipher.clone(),
|
|
fields2keys.clone(),
|
|
);
|
|
|
|
let prd_commitment = full_prd.create_commitment();
|
|
|
|
let freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let signed_psbt = create_transaction(
|
|
vec![],
|
|
&freezed_utxos,
|
|
sp_wallet,
|
|
recipients,
|
|
Some(prd_commitment.as_byte_array().to_vec()),
|
|
Amount::from_sat(fee_rate.into()),
|
|
None,
|
|
)?;
|
|
|
|
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
|
|
|
|
let partial_secret = sp_wallet
|
|
.get_client()
|
|
.get_partial_secret_from_psbt(&signed_psbt)?;
|
|
|
|
let final_tx = signed_psbt.extract_tx()?;
|
|
|
|
let mut ciphers = vec![];
|
|
for (member, visible_fields) in all_members {
|
|
let mut prd = full_prd.clone();
|
|
prd.filter_keys(visible_fields);
|
|
// we hash the payload
|
|
prd.payload = Value::from_str(&prd.payload)
|
|
.unwrap()
|
|
.tagged_hash()
|
|
.to_string();
|
|
let prd_msg = prd.to_network_msg(sp_wallet)?;
|
|
|
|
let addresses = member.get_addresses();
|
|
for sp_address in addresses.into_iter() {
|
|
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
|
|
&<SilentPaymentAddress>::try_from(sp_address.as_str())?.get_scan_key(),
|
|
&partial_secret,
|
|
);
|
|
|
|
let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point);
|
|
|
|
let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?;
|
|
ciphers.push(cipher.to_lower_hex_string());
|
|
relevant_process
|
|
.insert_shared_secret(SilentPaymentAddress::try_from(sp_address)?, shared_secret);
|
|
}
|
|
}
|
|
relevant_process.insert_impending_request(full_prd);
|
|
relevant_process.insert_state(ProcessState {
|
|
commited_in: OutPoint::null(),
|
|
encrypted_pcd: Value::Object(fields2cipher),
|
|
keys: fields2keys,
|
|
validation_tokens: vec![],
|
|
});
|
|
|
|
// Create the new_tx message
|
|
let new_tx_msg = NewTxMessage::new(serialize(&final_tx).to_lower_hex_string(), None);
|
|
|
|
Ok(ApiReturn {
|
|
new_tx_to_send: Some(new_tx_msg),
|
|
updated_process: Some((commitment_outpoint.to_string(), relevant_process.clone())),
|
|
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())
|
|
}
|
|
|
|
/// 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<Vec<String>> {
|
|
let outpoint = OutPoint::from_str(&process_outpoint)?;
|
|
|
|
let mut processes = lock_processes()?;
|
|
|
|
// TODO: We clone the process to prevent double borrowing issue, this can certainly be improved
|
|
let relevant_process = processes
|
|
.get(&outpoint)
|
|
.ok_or(ApiError::new("process not found".to_owned()))?;
|
|
|
|
let mut updated_process = relevant_process.clone();
|
|
|
|
let update_proposals: Vec<&Prd> = relevant_process
|
|
.get_impending_requests()
|
|
.into_iter()
|
|
.filter(|r| r.prd_type == PrdType::Update)
|
|
.collect();
|
|
|
|
if update_proposals.is_empty() {
|
|
return Err(ApiError::new(format!(
|
|
"No active update proposals for process {}",
|
|
process_outpoint
|
|
)));
|
|
}
|
|
|
|
let mut res = vec![];
|
|
|
|
// We first push the last commited state, if any
|
|
match relevant_process.get_latest_commited_state() {
|
|
Some(state) => res.push(serde_json::to_string(state)?),
|
|
None => ()
|
|
}
|
|
|
|
// Maybe that's the right place for adding a new state with what we've got from the prd update
|
|
// We should probably iterate on every update proposals and see which one don't have a state yet
|
|
let mut update_states = false;
|
|
for proposal in update_proposals {
|
|
// Is there a state that matches this proposal? If not, let's add it
|
|
debug!("Trying proposal {:#?}", proposal);
|
|
let pcd = match Value::from_str(&proposal.payload) {
|
|
Ok(value) => value,
|
|
Err(e) => continue
|
|
};
|
|
debug!("found pcd {:#?}", pcd);
|
|
let pcd_hash = AnkPcdHash::from_value(&pcd);
|
|
// We look for a pending state for the exact same state as the one in the proposal
|
|
if let None = relevant_process.get_latest_concurrent_states()
|
|
.into_iter()
|
|
.find(|state| {
|
|
AnkPcdHash::from_value(&state.encrypted_pcd) == pcd_hash
|
|
})
|
|
{
|
|
// If not, we first add a new state
|
|
updated_process.insert_state(ProcessState {
|
|
commited_in: OutPoint::new(Txid::from_str(&pcd_hash.to_string())?, u32::MAX),
|
|
encrypted_pcd: pcd.clone(),
|
|
keys: proposal.keys.clone(),
|
|
validation_tokens: proposal.validation_tokens.clone()
|
|
});
|
|
update_states = true;
|
|
}
|
|
// We add the decrypted state to our return variable
|
|
let mut decrypted_pcd = Map::new();
|
|
pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?;
|
|
res.push(serde_json::to_string(&decrypted_pcd)?);
|
|
}
|
|
|
|
if update_states {
|
|
// We replace the process
|
|
processes.insert(outpoint, updated_process);
|
|
}
|
|
// else we do nothing
|
|
|
|
Ok(res)
|
|
}
|