sdk_client/src/api.rs

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)
}