Merge branch 'demo' into dev
This commit is contained in:
commit
fcbc848be6
@ -8,7 +8,6 @@ name = "sdk_client"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_client" }
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@ -18,8 +17,8 @@ wasm-logger = "0.2.0"
|
||||
rand = "0.8.5"
|
||||
log = "0.4.6"
|
||||
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
|
||||
aes-gcm = "0.10.3"
|
||||
aes = "0.8.3"
|
||||
sdk_common = { path = "../../../sdk_common" }
|
||||
#sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" }
|
||||
shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" }
|
||||
img-parts = "0.3.0"
|
||||
|
||||
|
@ -1,73 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use sp_backend::bitcoin::PublicKey;
|
||||
//use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
||||
use std::marker::Copy;
|
||||
use tsify::Tsify;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Copy)]
|
||||
pub enum Role {
|
||||
Manager,
|
||||
#[default]
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct SilentPaymentAddress {
|
||||
version: u8,
|
||||
scan_pubkey: PublicKey,
|
||||
m_pubkey: PublicKey,
|
||||
is_testnet: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ItemMember {
|
||||
pub role: Role,
|
||||
pub sp_address: SilentPaymentAddress,
|
||||
//pre_id: hash(password, part1)
|
||||
//shard,
|
||||
//priv_key_mainnet_spend, (enc)
|
||||
//priv_key_mainnet_scan,
|
||||
//priv_key_signet_scan,
|
||||
}
|
||||
|
||||
impl ItemMember {
|
||||
pub fn new(role: Role, sp_address: SilentPaymentAddress) -> Self {
|
||||
ItemMember { role, sp_address }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Prdlist {
|
||||
//pub id: String,
|
||||
//pub version: String,
|
||||
pub gestionnaires: Vec<ItemMember>,
|
||||
// pub gestionnaires: Box<Vec<ItemMember>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[wasm_bindgen]
|
||||
struct RequestBody {
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub fn send_PrdRequest(prdlist: &Prdlist) -> Result<(), JsValue> {
|
||||
let managers: Vec<&ItemMember> = prdlist
|
||||
.gestionnaires
|
||||
.iter()
|
||||
.filter(|m| m.role == Role::Manager)
|
||||
.collect();
|
||||
for manager in managers {
|
||||
let request_body = RequestBody {
|
||||
message: "Asking for the Prd list".to_string(),
|
||||
};
|
||||
|
||||
let json_body = serde_json::to_string(&request_body).map_err(|e| {
|
||||
JsValue::from_str(&format!("Failed to serialize request body: {:?}", e))
|
||||
})?;
|
||||
println!("Sending request to manager {:?}", manager.sp_address);
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -1,32 +1,57 @@
|
||||
use std::any::Any;
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::sync::{Mutex, OnceLock, PoisonError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rand::Rng;
|
||||
use log::{debug, warn};
|
||||
use rand::{thread_rng, Fill, Rng, RngCore};
|
||||
|
||||
use anyhow::Error as AnyhowError;
|
||||
use serde_json::Error as SerdeJsonError;
|
||||
use sdk_common::crypto::{
|
||||
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose,
|
||||
};
|
||||
use sdk_common::sp_client::bitcoin::blockdata::fee_rate;
|
||||
use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize};
|
||||
use sdk_common::sp_client::bitcoin::hashes::HashEngine;
|
||||
use sdk_common::sp_client::bitcoin::hashes::{sha256, Hash};
|
||||
use sdk_common::sp_client::bitcoin::hex::{
|
||||
parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError,
|
||||
};
|
||||
use sdk_common::sp_client::bitcoin::key::Secp256k1;
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
|
||||
use sdk_common::sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid};
|
||||
use sdk_common::sp_client::silentpayments::utils as sp_utils;
|
||||
use sdk_common::sp_client::silentpayments::{Error as SpError, Network as SpNetwork};
|
||||
use serde_json::{Error as SerdeJsonError, Value};
|
||||
use shamir::SecretData;
|
||||
use sp_backend::bitcoin::consensus::deserialize;
|
||||
use sp_backend::bitcoin::hex::{FromHex, HexToBytesError};
|
||||
use sp_backend::bitcoin::secp256k1::{PublicKey, SecretKey};
|
||||
use sp_backend::bitcoin::{Transaction, Txid};
|
||||
use sp_backend::silentpayments::Error as SpError;
|
||||
|
||||
use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
||||
use tsify::Tsify;
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use sp_backend::spclient::{derive_keys_from_seed, OutputList, SpClient};
|
||||
use sp_backend::spclient::{SpWallet, SpendKey};
|
||||
use sdk_common::network::{
|
||||
self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, CipherMessage, FaucetMessage,
|
||||
NewTxMessage,
|
||||
};
|
||||
use sdk_common::silentpayments::{
|
||||
create_transaction, create_transaction_for_address_with_shared_secret,
|
||||
create_transaction_spend_outpoint, map_outputs_to_sp_address,
|
||||
};
|
||||
|
||||
use crate::images;
|
||||
use crate::network::{BitcoinNetworkMsg, BitcoinTopic, AnkNetworkMsg, AnkTopic};
|
||||
use crate::silentpayments::check_transaction;
|
||||
use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS};
|
||||
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_connected_user, User, UserWallets, CONNECTED_USER};
|
||||
use crate::{images, lock_messages, CACHEDMESSAGES};
|
||||
|
||||
use crate::process::Process;
|
||||
|
||||
@ -71,16 +96,48 @@ impl From<HexToBytesError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sp_backend::bitcoin::secp256k1::Error> for ApiError {
|
||||
fn from(value: sp_backend::bitcoin::secp256k1::Error) -> Self {
|
||||
impl From<HexToArrayError> for ApiError {
|
||||
fn from(value: HexToArrayError) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sp_backend::bitcoin::consensus::encode::Error> for ApiError {
|
||||
fn from(value: sp_backend::bitcoin::consensus::encode::Error) -> Self {
|
||||
impl From<sdk_common::sp_client::bitcoin::psbt::PsbtParseError> for ApiError {
|
||||
fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sdk_common::sp_client::bitcoin::psbt::ExtractTxError> for ApiError {
|
||||
fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sdk_common::sp_client::bitcoin::secp256k1::Error> for ApiError {
|
||||
fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sdk_common::sp_client::bitcoin::consensus::encode::Error> for ApiError {
|
||||
fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for ApiError {
|
||||
fn from(value: FromUtf8Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
@ -134,9 +191,26 @@ pub fn generate_sp_wallet(
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_receiving_address(pre_id: String) -> ApiResult<String> {
|
||||
if let Some(my_wallets) = lock_connected_users()?.get(&pre_id) {
|
||||
Ok(my_wallets.recover.get_client().get_receiving_address())
|
||||
pub fn get_recover_address() -> ApiResult<String> {
|
||||
if let Ok(my_wallets) = lock_connected_user() {
|
||||
Ok(my_wallets
|
||||
.try_get_recover()?
|
||||
.get_client()
|
||||
.get_receiving_address())
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: "Unknown user pre_id".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_main_address() -> ApiResult<String> {
|
||||
if let Ok(my_wallets) = lock_connected_user() {
|
||||
Ok(my_wallets
|
||||
.try_get_main()?
|
||||
.get_client()
|
||||
.get_receiving_address())
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: "Unknown user pre_id".to_owned(),
|
||||
@ -161,7 +235,7 @@ pub fn create_user(
|
||||
|
||||
let user_wallets = UserWallets::new(
|
||||
Some(sp_wallet_main),
|
||||
sp_wallet_recover,
|
||||
Some(sp_wallet_recover),
|
||||
Some(sp_wallet_revoke),
|
||||
);
|
||||
|
||||
@ -169,7 +243,9 @@ pub fn create_user(
|
||||
|
||||
let outputs = user_wallets.get_all_outputs();
|
||||
|
||||
lock_connected_users()?.insert(user.pre_id.clone(), user_wallets);
|
||||
// Setting CONNECTED_USER to user
|
||||
let mut connected_user = lock_connected_user()?;
|
||||
*connected_user = user_wallets;
|
||||
|
||||
let generate_user = createUserReturn {
|
||||
user,
|
||||
@ -197,60 +273,38 @@ pub struct get_process_return(Vec<Process>);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_processes() -> ApiResult<get_process_return> {
|
||||
let number_managers: u8 = 5;
|
||||
|
||||
let birthday_signet = 50000;
|
||||
let mut members: Vec<String> = Vec::with_capacity((number_managers) as usize);
|
||||
|
||||
for _ in 0..number_managers {
|
||||
//add sp_client
|
||||
let sp_wallet = generate_sp_wallet(None, birthday_signet, true)?;
|
||||
let sp_address = sp_wallet.get_client().get_receiving_address();
|
||||
members.push(sp_address);
|
||||
}
|
||||
let MEMBERS: [String;5] = [
|
||||
"tsp1qqdvmxycf3c3tf2qhpev0npx25rj05270d6j2pcsrfk2qn5gdy0rpwq6hd9u9sztl3fwmrzzqafzl3ymkq86aqfz5jl5egdkz72tqmhcnrswdz3pk".to_owned(),
|
||||
"tsp1qqwafwn7dcr9d6ta0w8fjtd9s53u72x9qmmtgd8adqr7454xl90a5jq3vw23l2x8ypt55nrg7trl9lwz5xr5j357ucu4sf9rfmvc0zujcpqcps6rm".to_owned(),
|
||||
"tsp1qqw02t5hmg5rxpjdkmjdnnmhvuc76wt6vlqdmn2zafnh6axxjd6e2gqcz04gzvnkzf572mur8spyx2a2s8sqzll2ymdpyz59cpl96j4zuvcdvrzxz".to_owned(),
|
||||
"tsp1qqgpay2r5jswm7vcv24xd94shdf90w30vxtql9svw7qnlnrzd6xt02q7s7z57uw0sssh6c0xddcrryq4mxup93jsh3gfau3autrawl8umkgsyupkm".to_owned(),
|
||||
"tsp1qqtsqmtgnxp0lsmnxyxcq52zpgxwugwlq8urlprs5pr5lwyqc789gjqhx5qra6g4rszsq43pms6nguee2l9trx905rk5sgntek05hnf7say4ru69y".to_owned(),
|
||||
];
|
||||
//instances of process
|
||||
let process1 = Process {
|
||||
id: 1,
|
||||
name: String::from("CREATE_ID"),
|
||||
id: 6,
|
||||
name: String::from("Messaging"),
|
||||
version: String::from("1.0"),
|
||||
members: members.clone(),
|
||||
html: crate::process::HTML_CREATE_ID.to_owned(),
|
||||
members: MEMBERS.to_vec(),
|
||||
html: crate::process::HTML_MESSAGING.to_owned(),
|
||||
style: crate::process::CSS.to_owned(),
|
||||
script: "".to_owned(),
|
||||
};
|
||||
let process2 = Process {
|
||||
id: 2,
|
||||
name: String::from("UPDATE_ID"),
|
||||
id: 7,
|
||||
name: String::from("Kotpart"),
|
||||
version: String::from("1.0"),
|
||||
members: members.clone(),
|
||||
html: crate::process::HTML_UPDATE_ID.to_owned(),
|
||||
style: crate::process::CSSUPDATE.to_owned(),
|
||||
script: crate::process::JSUPDATE.to_owned(),
|
||||
members: MEMBERS.to_vec(),
|
||||
html: crate::process::HTML_MESSAGING.to_owned(),
|
||||
style: crate::process::CSS.to_owned(),
|
||||
script: "".to_owned(),
|
||||
};
|
||||
let process3 = Process {
|
||||
id: 3,
|
||||
name: String::from("RECOVER"),
|
||||
id: 8,
|
||||
name: String::from("Storage"),
|
||||
version: String::from("1.0"),
|
||||
members: members.clone(),
|
||||
html: crate::process::HTML_RECOVER.to_owned(),
|
||||
style: crate::process::CSS.to_owned(),
|
||||
script: "".to_owned(),
|
||||
};
|
||||
let process4 = Process {
|
||||
id: 4,
|
||||
name: String::from("REVOKE_IMAGE"),
|
||||
version: String::from("1.0"),
|
||||
members: members.clone(),
|
||||
html: crate::process::HTML_REVOKE_IMAGE.to_owned(),
|
||||
style: crate::process::CSS.to_owned(),
|
||||
script: "".to_owned(),
|
||||
};
|
||||
let process5 = Process {
|
||||
id: 5,
|
||||
name: String::from("REVOKE"),
|
||||
version: String::from("1.0"),
|
||||
members: members.clone(),
|
||||
html: crate::process::HTML_REVOKE.to_owned(),
|
||||
members: MEMBERS.to_vec(),
|
||||
html: crate::process::HTML_MESSAGING.to_owned(),
|
||||
style: crate::process::CSS.to_owned(),
|
||||
script: "".to_owned(),
|
||||
};
|
||||
@ -260,8 +314,6 @@ pub fn get_processes() -> ApiResult<get_process_return> {
|
||||
data_process.push(process1);
|
||||
data_process.push(process2);
|
||||
data_process.push(process3);
|
||||
data_process.push(process4);
|
||||
data_process.push(process5);
|
||||
Ok(get_process_return(data_process))
|
||||
}
|
||||
|
||||
@ -288,7 +340,7 @@ impl shamir_shares {
|
||||
}
|
||||
|
||||
#[derive(Debug, Tsify, Serialize, Deserialize)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[tsify(from_wasm_abi, into_wasm_abi)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct outputs_list(Vec<OutputList>);
|
||||
|
||||
@ -317,54 +369,665 @@ pub fn login_user(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn check_transaction_for_silent_payments(
|
||||
tx_hex: String,
|
||||
tweak_data_hex: String,
|
||||
) -> ApiResult<()> {
|
||||
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
|
||||
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
|
||||
fn handle_recover_transaction(
|
||||
updated: HashMap<OutPoint, OwnedOutput>,
|
||||
tx: &Transaction,
|
||||
sp_wallet: &mut SpWallet,
|
||||
tweak_data: PublicKey,
|
||||
fee_rate: u32,
|
||||
) -> anyhow::Result<CachedMessage> {
|
||||
let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return());
|
||||
let commitment = if op_return.is_none() {
|
||||
vec![]
|
||||
} else {
|
||||
op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec()
|
||||
};
|
||||
let commitment_str = commitment.to_lower_hex_string();
|
||||
|
||||
check_transaction(tx, tweak_data);
|
||||
// If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both
|
||||
// Basically a transaction that destroyed utxo is a transaction we sent.
|
||||
let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated
|
||||
.iter()
|
||||
.filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent)
|
||||
.collect();
|
||||
let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated
|
||||
.iter()
|
||||
.filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent)
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let mut messages = lock_messages()?;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_bitcoin_network_msg(msg: Vec<u8>) -> ApiResult<()> {
|
||||
let parsed_msg = BitcoinNetworkMsg::new(&msg)?;
|
||||
|
||||
match parsed_msg.topic {
|
||||
BitcoinTopic::RawTx => {
|
||||
let tx = deserialize::<Transaction>(parsed_msg.data)?;
|
||||
let tweak_data = PublicKey::from_slice(parsed_msg.addon)?;
|
||||
check_transaction(tx, tweak_data);
|
||||
// empty utxo_destroyed means we received this transaction
|
||||
if utxo_destroyed.is_empty() {
|
||||
// We first check for faucet transactions
|
||||
if let Some(pos) = messages.iter().position(|m| {
|
||||
if m.status == CachedMessageStatus::FaucetWaiting {
|
||||
m.commitment.as_ref() == Some(&commitment_str)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
BitcoinTopic::RawBlock => (),
|
||||
}) {
|
||||
let message = messages.get_mut(pos).unwrap();
|
||||
match message.status {
|
||||
CachedMessageStatus::FaucetWaiting => {
|
||||
message.status = CachedMessageStatus::FaucetComplete;
|
||||
message.commited_in = utxo_created
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|(outpoint, _)| *outpoint);
|
||||
return Ok(message.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_4nk_msg(raw: String) -> Option<String>{
|
||||
if let Ok(msg) = AnkNetworkMsg::new(&raw) {
|
||||
match msg.topic {
|
||||
AnkTopic::Faucet => {
|
||||
match Txid::from_str(msg.content) {
|
||||
Ok(txid) => {
|
||||
// return the txid for verification
|
||||
Some(txid.to_string())
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Invalid txid with a \"faucet\" message: {}", e.to_string());
|
||||
None
|
||||
// Actually this is unreachable
|
||||
CachedMessageStatus::FaucetComplete => return Ok(message.clone()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// we inspect inputs looking for links with previous tx
|
||||
for input in tx.input.iter() {
|
||||
if let Some(pos) = messages.iter().position(|m| {
|
||||
debug!("{:?}", Some(input.previous_output));
|
||||
m.confirmed_by == Some(input.previous_output)
|
||||
}) {
|
||||
let message = messages.get_mut(pos).unwrap();
|
||||
// If we are receiver, that's pretty much it, just set status to complete
|
||||
message.status = CachedMessageStatus::Complete;
|
||||
return Ok(message.clone());
|
||||
} else if let Some(pos) = messages
|
||||
.iter()
|
||||
.position(|m| m.commited_in == Some(input.previous_output))
|
||||
{
|
||||
// sender needs to spent it back again to receiver
|
||||
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
|
||||
|
||||
let message = messages.get_mut(pos).unwrap();
|
||||
|
||||
message.confirmed_by = Some(outpoint.clone());
|
||||
message.status = CachedMessageStatus::MustSpendConfirmation;
|
||||
|
||||
// Caller must interpret this message as "do spend confirmed_by outpoint to receiver"
|
||||
return Ok(message.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// if we've found nothing we are being notified
|
||||
let shared_point = sp_utils::receiving::calculate_shared_point(
|
||||
&tweak_data,
|
||||
&sp_wallet.get_client().get_scan_key(),
|
||||
);
|
||||
let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?);
|
||||
|
||||
debug!(
|
||||
"Shared secret: {}",
|
||||
shared_secret.to_byte_array().to_lower_hex_string()
|
||||
);
|
||||
|
||||
let mut plaintext: Vec<u8> = vec![];
|
||||
if let Some(cipher_pos) = messages.iter().position(|m| {
|
||||
if m.status != CachedMessageStatus::CipherWaitingTx {
|
||||
return false;
|
||||
}
|
||||
let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array());
|
||||
if res.is_ok() {
|
||||
plaintext = res.unwrap();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) {
|
||||
let message = messages.get_mut(cipher_pos).unwrap();
|
||||
|
||||
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
|
||||
|
||||
let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?;
|
||||
message.commited_in = Some(outpoint.clone());
|
||||
message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
|
||||
message.commitment = Some(commitment_str);
|
||||
message.plaintext = Some(cipher_msg.message);
|
||||
message.sender = Some(cipher_msg.sender);
|
||||
message.recipient = Some(sp_wallet.get_client().get_receiving_address());
|
||||
message.status = CachedMessageStatus::ReceivedMustConfirm;
|
||||
|
||||
return Ok(message.clone());
|
||||
} else {
|
||||
// store it and wait for the message
|
||||
let mut new_msg = CachedMessage::new();
|
||||
let (outpoint, output) = utxo_created
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("utxo_created shouldn't be empty");
|
||||
new_msg.commited_in = Some(outpoint.clone());
|
||||
new_msg.commitment = Some(commitment.to_lower_hex_string());
|
||||
new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address());
|
||||
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
|
||||
new_msg.status = CachedMessageStatus::TxWaitingCipher;
|
||||
messages.push(new_msg.clone());
|
||||
return Ok(new_msg.clone());
|
||||
}
|
||||
} else {
|
||||
log::debug!("Can't parse message as a valid 4nk message: {}", raw);
|
||||
None
|
||||
// We are sender of a transaction
|
||||
// We only need to return the message
|
||||
// eiter this is notification, a challenge, or response to a challenge
|
||||
// if notification, commitment is the same than in the message
|
||||
// if challenge or response, commitment is H(commitment | b_scan), b_scan being different depending on who we are
|
||||
if let Some(message) = messages.iter().find(|m| {
|
||||
if commitment.is_empty() || m.commitment.is_none() {
|
||||
return false;
|
||||
}
|
||||
match m.status {
|
||||
CachedMessageStatus::SentWaitingConfirmation => {
|
||||
// commitment we're looking for is simply what's in the message
|
||||
m.commitment
|
||||
.as_ref()
|
||||
.map(|c| Vec::from_hex(&c).unwrap())
|
||||
.unwrap()
|
||||
== commitment
|
||||
}
|
||||
CachedMessageStatus::MustSpendConfirmation
|
||||
| CachedMessageStatus::ReceivedMustConfirm => {
|
||||
// we compute the potential commitment
|
||||
let m_commitment = m
|
||||
.commitment
|
||||
.as_ref()
|
||||
.map(|c| Vec::from_hex(&c).unwrap())
|
||||
.unwrap();
|
||||
let mut buf = [0u8; 64];
|
||||
buf[..32].copy_from_slice(&m_commitment);
|
||||
buf[32..]
|
||||
.copy_from_slice(&sp_wallet.get_client().get_scan_key().secret_bytes());
|
||||
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&buf).unwrap();
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
hash.to_byte_array().to_vec() == commitment
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}) {
|
||||
return Ok(message.clone());
|
||||
} else {
|
||||
return Err(anyhow::Error::msg(
|
||||
"We spent a transaction for a commitment we don't know",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the transaction has anything to do with us, we create/update the relevant `CachedMessage`
|
||||
/// and return it to caller for persistent storage
|
||||
fn process_transaction(
|
||||
tx_hex: String,
|
||||
blockheight: u32,
|
||||
tweak_data_hex: String,
|
||||
fee_rate: u32,
|
||||
) -> anyhow::Result<CachedMessage> {
|
||||
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
|
||||
|
||||
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
|
||||
|
||||
let mut connected_user = lock_connected_user()?;
|
||||
if let Ok(recover) = connected_user.try_get_mut_recover() {
|
||||
let updated = recover.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
|
||||
|
||||
if updated.len() > 0 {
|
||||
let updated_msg =
|
||||
handle_recover_transaction(updated, &tx, recover, tweak_data, fee_rate)?;
|
||||
return Ok(updated_msg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(main) = connected_user.try_get_mut_main() {
|
||||
let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
|
||||
if updated.len() > 0 {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(revoke) = connected_user.try_get_mut_revoke() {
|
||||
let updated = revoke.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
|
||||
if updated.len() > 0 {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::Error::msg("No output found"))
|
||||
}
|
||||
|
||||
fn process_new_tx_error(msg: NewTxMessage) -> anyhow::Result<CachedMessage> {
|
||||
// how do we match this error with the cached message?
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage> {
|
||||
if let Ok(ank_msg) = serde_json::from_str::<AnkNetworkMsg>(&raw) {
|
||||
match ank_msg.flag {
|
||||
AnkFlag::NewTx => {
|
||||
let tx_message = serde_json::from_str::<NewTxMessage>(&ank_msg.content)?;
|
||||
if let Some(ref error) = tx_message.error {
|
||||
// Transaction failed to broadcast
|
||||
// we can retry later or check the availability of our spent output, depending on the actual error
|
||||
// we should probably look up the cached message and record the error
|
||||
log::error!("{}", error);
|
||||
// let updated = process_new_tx_error(tx_message)?;
|
||||
let updated = CachedMessage::new();
|
||||
return Ok(updated);
|
||||
}
|
||||
if tx_message.tweak_data.is_none() {
|
||||
return Err(ApiError {
|
||||
message: "Missing tweak_data".to_owned(),
|
||||
});
|
||||
}
|
||||
let network_msg = process_transaction(
|
||||
tx_message.transaction,
|
||||
0,
|
||||
tx_message.tweak_data.unwrap(),
|
||||
fee_rate,
|
||||
)?;
|
||||
return Ok(network_msg);
|
||||
}
|
||||
AnkFlag::Faucet => {
|
||||
let faucet_msg = serde_json::from_str::<FaucetMessage>(&ank_msg.content)?;
|
||||
if let Some(error) = faucet_msg.error {
|
||||
debug!("Faucet msg returned with an error: {}", error);
|
||||
}
|
||||
unimplemented!();
|
||||
}
|
||||
AnkFlag::Cipher => {
|
||||
// let's try to decrypt with keys we found in transactions but haven't used yet
|
||||
let mut messages = lock_messages()?;
|
||||
let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?;
|
||||
let cipher_pos = messages.iter().position(|m| {
|
||||
debug!("Trying message: {:?}", m);
|
||||
if m.status != CachedMessageStatus::TxWaitingCipher {
|
||||
return false;
|
||||
}
|
||||
m.try_decrypt_cipher(cipher.clone()).is_ok()
|
||||
});
|
||||
if cipher_pos.is_some() {
|
||||
let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap();
|
||||
let plain = message.try_decrypt_cipher(cipher).unwrap();
|
||||
let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?;
|
||||
message.plaintext = Some(cipher_msg.message);
|
||||
message.sender = Some(cipher_msg.sender);
|
||||
message.ciphertext = Some(ank_msg.content);
|
||||
message.status = CachedMessageStatus::ReceivedMustConfirm;
|
||||
return Ok(message.clone());
|
||||
} else {
|
||||
// let's keep it in case we receive the transaction later
|
||||
let mut new_msg = CachedMessage::new();
|
||||
new_msg.status = CachedMessageStatus::CipherWaitingTx;
|
||||
new_msg.ciphertext = Some(ank_msg.content);
|
||||
messages.push(new_msg.clone());
|
||||
return Ok(new_msg);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: format!("Can't parse message as a valid 4nk message: {}", raw),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_outpoints_for_user() -> ApiResult<outputs_list> {
|
||||
let connected_user = lock_connected_user()?;
|
||||
if connected_user.is_not_empty() {
|
||||
Ok(outputs_list(connected_user.get_all_outputs()))
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: "No user logged in".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_available_amount_for_user(recover: bool) -> ApiResult<u64> {
|
||||
let connected_user = lock_connected_user()?;
|
||||
if recover {
|
||||
if let Ok(recover_wallet) = connected_user.try_get_recover() {
|
||||
Ok(recover_wallet.get_outputs().get_balance().to_sat())
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: "User doesn't have recover wallet available".to_owned(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(ApiError {
|
||||
message: "No user logged in".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult<bool> {
|
||||
let transaction = deserialize::<Transaction>(&Vec::from_hex(&tx)?)?;
|
||||
let txid = transaction.txid();
|
||||
let connected_user = lock_connected_user()?;
|
||||
|
||||
if let Some(_) = connected_user
|
||||
.try_get_recover()?
|
||||
.get_outputs()
|
||||
.to_outpoints_list()
|
||||
.iter()
|
||||
.find(|(outpoint, output)| outpoint.txid == txid)
|
||||
{
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Tsify, Serialize, Deserialize)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct createTransactionReturn {
|
||||
pub txid: String,
|
||||
pub transaction: String,
|
||||
pub new_network_msg: CachedMessage,
|
||||
}
|
||||
|
||||
/// This is what we call to answer a confirmation as a sender
|
||||
#[wasm_bindgen]
|
||||
pub fn answer_confirmation_transaction(
|
||||
message_id: u32,
|
||||
fee_rate: u32,
|
||||
) -> ApiResult<createTransactionReturn> {
|
||||
let mut messages = lock_messages()?;
|
||||
let message: &mut CachedMessage;
|
||||
if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) {
|
||||
if m.sender.is_none() || m.commited_in.is_none() {
|
||||
return Err(ApiError {
|
||||
message: "Invalid network message".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
message = m;
|
||||
} else {
|
||||
return Err(ApiError {
|
||||
message: format!("Can't find message for id {}", message_id),
|
||||
});
|
||||
}
|
||||
|
||||
let sp_address: SilentPaymentAddress =
|
||||
message.recipient.as_ref().unwrap().as_str().try_into()?;
|
||||
|
||||
let connected_user = lock_connected_user()?;
|
||||
|
||||
let sp_wallet: &SpWallet;
|
||||
if sp_address.get_network() != SpNetwork::Mainnet {
|
||||
sp_wallet = connected_user.try_get_recover()?;
|
||||
} else {
|
||||
sp_wallet = connected_user.try_get_main()?;
|
||||
}
|
||||
|
||||
let recipient = Recipient {
|
||||
address: sp_address.into(),
|
||||
amount: Amount::from_sat(0), // we'll set amount to what's available in the confirmed_by output we don't want change
|
||||
nb_outputs: 1,
|
||||
};
|
||||
|
||||
let confirmed_by = message.confirmed_by.clone().unwrap();
|
||||
let commited_in = message.commited_in.clone().unwrap();
|
||||
|
||||
let signed_psbt = create_transaction_spend_outpoint(
|
||||
&confirmed_by,
|
||||
sp_wallet,
|
||||
recipient,
|
||||
&commited_in.txid,
|
||||
Amount::from_sat(fee_rate.into()),
|
||||
)?;
|
||||
|
||||
let final_tx = signed_psbt.extract_tx()?;
|
||||
|
||||
message.status = CachedMessageStatus::Complete;
|
||||
|
||||
Ok(createTransactionReturn {
|
||||
txid: final_tx.txid().to_string(),
|
||||
transaction: serialize(&final_tx).to_lower_hex_string(),
|
||||
new_network_msg: message.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// This is what we call to confirm as a receiver
|
||||
#[wasm_bindgen]
|
||||
pub fn create_confirmation_transaction(
|
||||
message_id: u32,
|
||||
fee_rate: u32,
|
||||
) -> ApiResult<createTransactionReturn> {
|
||||
let mut messages = lock_messages()?;
|
||||
let message: &mut CachedMessage;
|
||||
if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) {
|
||||
if m.sender.is_none() || m.commited_in.is_none() {
|
||||
return Err(ApiError {
|
||||
message: "Invalid network message".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
message = m;
|
||||
} else {
|
||||
return Err(ApiError {
|
||||
message: format!("Can't find message for id {}", message_id),
|
||||
});
|
||||
}
|
||||
|
||||
let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?;
|
||||
let connected_user = lock_connected_user()?;
|
||||
|
||||
let sp_wallet: &SpWallet;
|
||||
if sp_address.get_network() != SpNetwork::Mainnet {
|
||||
sp_wallet = connected_user.try_get_recover()?;
|
||||
} else {
|
||||
sp_wallet = connected_user.try_get_main()?;
|
||||
}
|
||||
|
||||
let recipient = Recipient {
|
||||
address: sp_address.into(),
|
||||
amount: Amount::from_sat(0),
|
||||
nb_outputs: 1,
|
||||
};
|
||||
|
||||
let commited_in = message.commited_in.clone().unwrap();
|
||||
|
||||
let signed_psbt = create_transaction_spend_outpoint(
|
||||
&commited_in,
|
||||
sp_wallet,
|
||||
recipient,
|
||||
&commited_in.txid,
|
||||
Amount::from_sat(fee_rate.into()),
|
||||
)?;
|
||||
|
||||
// what's the vout of the output sent to sender?
|
||||
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
|
||||
let recipients_vouts = sp_address2vouts
|
||||
.get::<String>(&sp_address.into())
|
||||
.expect("recipients didn't change")
|
||||
.as_slice();
|
||||
|
||||
let final_tx = signed_psbt.extract_tx()?;
|
||||
|
||||
message.confirmed_by = Some(OutPoint {
|
||||
txid: final_tx.txid(),
|
||||
vout: recipients_vouts[0] as u32,
|
||||
});
|
||||
|
||||
Ok(createTransactionReturn {
|
||||
txid: final_tx.txid().to_string(),
|
||||
transaction: serialize(&final_tx).to_lower_hex_string(),
|
||||
new_network_msg: message.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn create_notification_transaction(
|
||||
address: String,
|
||||
message: CipherMessage,
|
||||
fee_rate: u32,
|
||||
) -> ApiResult<createTransactionReturn> {
|
||||
let sp_address: SilentPaymentAddress = address.as_str().try_into()?;
|
||||
|
||||
let connected_user = lock_connected_user()?;
|
||||
|
||||
let sp_wallet: &SpWallet;
|
||||
if sp_address.get_network() != SpNetwork::Mainnet {
|
||||
sp_wallet = connected_user.try_get_recover()?;
|
||||
} else {
|
||||
sp_wallet = connected_user.try_get_main()?;
|
||||
}
|
||||
|
||||
let recipient = Recipient {
|
||||
address: sp_address.into(),
|
||||
amount: Amount::from_sat(1200),
|
||||
nb_outputs: 1,
|
||||
};
|
||||
|
||||
let commitment = create_commitment(serde_json::to_string(&message)?);
|
||||
|
||||
let signed_psbt = create_transaction_for_address_with_shared_secret(
|
||||
recipient,
|
||||
sp_wallet,
|
||||
Some(&commitment),
|
||||
Amount::from_sat(fee_rate.into()),
|
||||
)?;
|
||||
|
||||
let psbt = Psbt::from_str(&signed_psbt)?;
|
||||
|
||||
let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&psbt)?;
|
||||
|
||||
let shared_point =
|
||||
sp_utils::sending::calculate_shared_point(&sp_address.get_scan_key(), &partial_secret);
|
||||
|
||||
let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?);
|
||||
|
||||
debug!(
|
||||
"Created transaction with secret {}",
|
||||
shared_secret.to_byte_array().to_lower_hex_string()
|
||||
);
|
||||
|
||||
let cipher = encrypt_with_key(
|
||||
serde_json::to_string(&message)?,
|
||||
shared_secret.to_byte_array().to_lower_hex_string(),
|
||||
)?;
|
||||
|
||||
// update our cache
|
||||
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt)?;
|
||||
let recipients_vouts = sp_address2vouts
|
||||
.get::<String>(&address)
|
||||
.expect("recipients didn't change")
|
||||
.as_slice();
|
||||
// for now let's just take the smallest vout that belongs to the recipient
|
||||
let final_tx = psbt.extract_tx()?;
|
||||
let mut new_msg = CachedMessage::new();
|
||||
new_msg.plaintext = Some(message.message);
|
||||
new_msg.ciphertext = Some(cipher);
|
||||
new_msg.commitment = Some(commitment);
|
||||
new_msg.commited_in = Some(OutPoint {
|
||||
txid: final_tx.txid(),
|
||||
vout: recipients_vouts[0] as u32,
|
||||
});
|
||||
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
|
||||
new_msg.recipient = Some(address);
|
||||
new_msg.sender = Some(sp_wallet.get_client().get_receiving_address());
|
||||
new_msg.status = CachedMessageStatus::SentWaitingConfirmation;
|
||||
lock_messages()?.push(new_msg.clone());
|
||||
|
||||
Ok(createTransactionReturn {
|
||||
txid: final_tx.txid().to_string(),
|
||||
transaction: serialize(&final_tx).to_lower_hex_string(),
|
||||
new_network_msg: new_msg,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Tsify, Serialize, Deserialize)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct encryptWithNewKeyResult {
|
||||
pub cipher: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt_with_key(plaintext: String, key: String) -> ApiResult<String> {
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut rand::thread_rng());
|
||||
|
||||
let mut aes_key = [0u8; 32];
|
||||
aes_key.copy_from_slice(&Vec::from_hex(&key)?);
|
||||
|
||||
// encrypt
|
||||
let aes_enc = Aes256Encryption::import_key(
|
||||
Purpose::Arbitrary,
|
||||
plaintext.into_bytes(),
|
||||
aes_key,
|
||||
nonce.into(),
|
||||
)?;
|
||||
|
||||
let cipher = aes_enc.encrypt_with_aes_key()?;
|
||||
|
||||
Ok(cipher.to_lower_hex_string())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt_with_new_key(plaintext: String) -> ApiResult<encryptWithNewKeyResult> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// generate new key
|
||||
let aes_key = Aes256Gcm::generate_key(&mut rng);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
||||
|
||||
// encrypt
|
||||
let aes_enc = Aes256Encryption::import_key(
|
||||
Purpose::Arbitrary,
|
||||
plaintext.into_bytes(),
|
||||
aes_key.into(),
|
||||
nonce.into(),
|
||||
)?;
|
||||
|
||||
let cipher = aes_enc.encrypt_with_aes_key()?;
|
||||
|
||||
Ok(encryptWithNewKeyResult {
|
||||
cipher: cipher.to_lower_hex_string(),
|
||||
key: aes_key.to_lower_hex_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult<String> {
|
||||
let key_bin = Vec::from_hex(&key)?;
|
||||
if key_bin.len() != 32 {
|
||||
return Err(ApiError {
|
||||
message: "key of invalid lenght".to_owned(),
|
||||
});
|
||||
}
|
||||
let mut aes_key = [0u8; 32];
|
||||
aes_key.copy_from_slice(&Vec::from_hex(&key)?);
|
||||
let aes_dec = Aes256Decryption::new(Purpose::Arbitrary, Vec::from_hex(&cipher)?, aes_key)?;
|
||||
|
||||
let plain = String::from_utf8(aes_dec.decrypt_with_key()?)?;
|
||||
Ok(plain)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn create_faucet_msg() -> ApiResult<CachedMessage> {
|
||||
let user = lock_connected_user()?;
|
||||
let sp_address = user.try_get_recover()?.get_client().get_receiving_address();
|
||||
|
||||
let mut commitment = [0u8; 64];
|
||||
thread_rng().fill_bytes(&mut commitment);
|
||||
|
||||
let mut cached_msg = CachedMessage::new();
|
||||
cached_msg.recipient = Some(sp_address);
|
||||
cached_msg.commitment = Some(commitment.to_lower_hex_string());
|
||||
cached_msg.status = CachedMessageStatus::FaucetWaiting;
|
||||
lock_messages()?.push(cached_msg.clone());
|
||||
Ok(cached_msg)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn create_commitment(payload_to_hash: String) -> String {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&payload_to_hash.as_bytes());
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
hash.to_byte_array().to_lower_hex_string()
|
||||
}
|
||||
|
@ -1,447 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use sp_backend::{
|
||||
bitcoin::{
|
||||
consensus::serde::hex,
|
||||
hex::DisplayHex,
|
||||
key::constants::SECRET_KEY_SIZE,
|
||||
secp256k1::{ecdh::SharedSecret, SecretKey},
|
||||
Txid,
|
||||
},
|
||||
silentpayments::sending::SilentPaymentAddress,
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::{
|
||||
cipher::consts::{U32, U8},
|
||||
Aes256,
|
||||
};
|
||||
use aes_gcm::{
|
||||
aead::{Aead, AeadInPlace, KeyInit, Nonce},
|
||||
AeadCore, Aes256Gcm, AesGcm, Key, TagSize,
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2;
|
||||
|
||||
const THIRTYTWO: usize = 32;
|
||||
|
||||
pub struct HalfKey([u8; HALFKEYSIZE]);
|
||||
|
||||
impl TryFrom<Vec<u8>> for HalfKey {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Vec<u8>) -> std::prelude::v1::Result<Self, Error> {
|
||||
if value.len() == HALFKEYSIZE {
|
||||
let mut buf = [0u8; HALFKEYSIZE];
|
||||
buf.copy_from_slice(&value);
|
||||
Ok(HalfKey(buf))
|
||||
} else {
|
||||
Err(Error::msg("Invalid length for HalfKey"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HalfKey {
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_inner(&self) -> Vec<u8> {
|
||||
self.0.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Purpose {
|
||||
Login,
|
||||
ThirtyTwoBytes,
|
||||
}
|
||||
|
||||
pub type CipherText = Vec<u8>;
|
||||
|
||||
pub type EncryptedKey = Vec<u8>;
|
||||
|
||||
pub struct Aes256Decryption {
|
||||
pub purpose: Purpose,
|
||||
cipher_text: CipherText,
|
||||
aes_key: [u8; 32],
|
||||
nonce: [u8; 12],
|
||||
}
|
||||
|
||||
impl Aes256Decryption {
|
||||
pub fn new(
|
||||
purpose: Purpose,
|
||||
cipher_text: CipherText,
|
||||
encrypted_aes_key: Vec<u8>, // If shared_secret is none this is actually the aes_key
|
||||
shared_secret: Option<SharedSecret>, // We don't need that for certain purpose, like Login
|
||||
) -> Result<Self> {
|
||||
let mut aes_key = [0u8; 32];
|
||||
if let Some(shared_secret) = shared_secret {
|
||||
if encrypted_aes_key.len() <= 12 {
|
||||
return Err(Error::msg("encrypted_aes_key is shorter than nonce length"));
|
||||
} // Actually we could probably test that if the remnant is not a multiple of 32, something's wrong
|
||||
// take the first 12 bytes form encrypted_aes_key as nonce
|
||||
let (decrypt_key_nonce, encrypted_key) = encrypted_aes_key.split_at(12);
|
||||
// decrypt key with shared_secret obtained from transaction
|
||||
let decrypt_key_cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
let aes_key_plain = decrypt_key_cipher
|
||||
.decrypt(decrypt_key_nonce.into(), encrypted_key)
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
if aes_key_plain.len() != 32 {
|
||||
return Err(Error::msg("Invalid length for decrypted key"));
|
||||
}
|
||||
aes_key.copy_from_slice(&aes_key_plain);
|
||||
} else {
|
||||
if encrypted_aes_key.len() != 32 {
|
||||
return Err(Error::msg("Invalid length for decrypted key"));
|
||||
}
|
||||
aes_key.copy_from_slice(&encrypted_aes_key);
|
||||
}
|
||||
if cipher_text.len() <= 12 {
|
||||
return Err(Error::msg("cipher_text is shorter than nonce length"));
|
||||
}
|
||||
let (message_nonce, message_cipher) = cipher_text.split_at(12);
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce.copy_from_slice(message_nonce);
|
||||
Ok(Self {
|
||||
purpose,
|
||||
cipher_text: message_cipher.to_vec(),
|
||||
aes_key,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt_with_key(&self) -> Result<Vec<u8>> {
|
||||
match self.purpose {
|
||||
Purpose::Login => {
|
||||
let half_key = self.decrypt_login()?;
|
||||
Ok(half_key.to_inner())
|
||||
}
|
||||
Purpose::ThirtyTwoBytes => {
|
||||
let thirty_two_buf = self.decrypt_thirty_two()?;
|
||||
Ok(thirty_two_buf.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_login(&self) -> Result<HalfKey> {
|
||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||
let plain = cipher
|
||||
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
if plain.len() != SECRET_KEY_SIZE / 2 {
|
||||
return Err(Error::msg("Plain text of invalid lenght for a login"));
|
||||
}
|
||||
let mut key_half = [0u8; SECRET_KEY_SIZE / 2];
|
||||
key_half.copy_from_slice(&plain);
|
||||
Ok(HalfKey(key_half))
|
||||
}
|
||||
|
||||
fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> {
|
||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||
let plain = cipher
|
||||
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
if plain.len() != THIRTYTWO {
|
||||
return Err(Error::msg("Plain text of invalid length, should be 32"));
|
||||
}
|
||||
let mut thirty_two = [0u8; THIRTYTWO];
|
||||
thirty_two.copy_from_slice(&plain);
|
||||
Ok(thirty_two)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Aes256Encryption {
|
||||
pub purpose: Purpose,
|
||||
plaintext: Vec<u8>,
|
||||
aes_key: [u8; 32],
|
||||
nonce: [u8; 12],
|
||||
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
||||
}
|
||||
|
||||
impl Aes256Encryption {
|
||||
pub fn new(purpose: Purpose, plaintext: Vec<u8>) -> Result<Self> {
|
||||
let mut rng = thread_rng();
|
||||
let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
||||
let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into();
|
||||
Self::import_key(purpose, plaintext, aes_key, nonce)
|
||||
}
|
||||
|
||||
pub fn set_shared_secret(
|
||||
&mut self,
|
||||
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
||||
) {
|
||||
self.shared_secrets = shared_secrets;
|
||||
}
|
||||
|
||||
pub fn encrypt_keys_with_shared_secrets(
|
||||
&self,
|
||||
) -> Result<HashMap<SilentPaymentAddress, EncryptedKey>> {
|
||||
let mut res = HashMap::new();
|
||||
let mut rng = thread_rng();
|
||||
|
||||
for (_, sp_address2shared_secret) in self.shared_secrets.iter() {
|
||||
for (sp_address, shared_secret) in sp_address2shared_secret {
|
||||
let cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
||||
let encrypted_key = cipher
|
||||
.encrypt(&nonce, self.aes_key.as_slice())
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
|
||||
let mut ciphertext = Vec::<u8>::with_capacity(nonce.len() + encrypted_key.len());
|
||||
ciphertext.extend(nonce);
|
||||
ciphertext.extend(encrypted_key);
|
||||
|
||||
res.insert(sp_address.to_owned(), ciphertext);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn import_key(
|
||||
purpose: Purpose,
|
||||
plaintext: Vec<u8>,
|
||||
aes_key: [u8; 32],
|
||||
nonce: [u8; 12],
|
||||
) -> Result<Self> {
|
||||
if plaintext.len() == 0 {
|
||||
return Err(Error::msg("Can't create encryption for an empty message"));
|
||||
}
|
||||
Ok(Self {
|
||||
purpose,
|
||||
plaintext,
|
||||
aes_key,
|
||||
nonce,
|
||||
shared_secrets: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_with_aes_key(&self) -> Result<CipherText> {
|
||||
match self.purpose {
|
||||
Purpose::Login => self.encrypt_login(),
|
||||
Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt_login(&self) -> Result<CipherText> {
|
||||
let half_key: HalfKey = self.plaintext.clone().try_into()?;
|
||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||
let cipher_text = cipher
|
||||
.encrypt(&self.nonce.into(), half_key.as_slice())
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
||||
res.extend_from_slice(&self.nonce);
|
||||
res.extend_from_slice(&cipher_text);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn encrypt_thirty_two(&self) -> Result<CipherText> {
|
||||
if self.plaintext.len() != 32 {
|
||||
return Err(Error::msg("Invalid length, should be 32"));
|
||||
}
|
||||
let mut thirty_two = [0u8; 32];
|
||||
thirty_two.copy_from_slice(&self.plaintext);
|
||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||
let cipher_text = cipher
|
||||
.encrypt(&self.nonce.into(), thirty_two.as_slice())
|
||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
||||
log::info!("{}", cipher_text.len());
|
||||
res.extend_from_slice(&self.nonce);
|
||||
res.extend_from_slice(&cipher_text);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz";
|
||||
const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08";
|
||||
const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b";
|
||||
const ALICE_SHARED_SECRET: &str =
|
||||
"ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106";
|
||||
const BOB_SHARED_SECRET: &str =
|
||||
"15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521";
|
||||
|
||||
#[test]
|
||||
fn new_aes_empty_plaintext() {
|
||||
let plaintext = Vec::new();
|
||||
let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext);
|
||||
|
||||
assert!(aes_enc.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_encrypt_login_invalid_length() {
|
||||
let plaintext = "example";
|
||||
let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec());
|
||||
|
||||
assert!(aes_enc_short.is_ok());
|
||||
|
||||
let cipher = aes_enc_short.unwrap().encrypt_with_aes_key();
|
||||
|
||||
assert!(cipher.is_err());
|
||||
|
||||
let plaintext = [1u8; 64];
|
||||
let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec());
|
||||
|
||||
assert!(aes_enc_long.is_ok());
|
||||
|
||||
let cipher = aes_enc_long.unwrap().encrypt_with_aes_key();
|
||||
|
||||
assert!(cipher.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_encrypt_login() {
|
||||
let plaintext = [1u8; HALFKEYSIZE];
|
||||
let aes_key = Aes256Gcm::generate_key(&mut thread_rng());
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut thread_rng());
|
||||
let aes_enc = Aes256Encryption::import_key(
|
||||
Purpose::Login,
|
||||
plaintext.to_vec(),
|
||||
aes_key.into(),
|
||||
nonce.into(),
|
||||
);
|
||||
|
||||
assert!(aes_enc.is_ok());
|
||||
|
||||
let cipher = aes_enc.unwrap().encrypt_with_aes_key();
|
||||
|
||||
assert!(cipher.is_ok());
|
||||
|
||||
let mut plain_key = [0u8; 32];
|
||||
plain_key.copy_from_slice(&aes_key.to_vec());
|
||||
|
||||
let aes_dec =
|
||||
Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key.to_vec(), None);
|
||||
|
||||
assert!(aes_dec.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_encrypt_key() {
|
||||
let plaintext = [1u8; HALFKEYSIZE];
|
||||
let mut aes_enc = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()).unwrap();
|
||||
|
||||
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
||||
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
||||
HashMap::new();
|
||||
sp_address2shared_secrets.insert(
|
||||
ALICE_SP_ADDRESS.try_into().unwrap(),
|
||||
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
||||
);
|
||||
shared_secrets.insert(
|
||||
Txid::from_str(TRANSACTION).unwrap(),
|
||||
sp_address2shared_secrets,
|
||||
);
|
||||
|
||||
aes_enc.set_shared_secret(shared_secrets);
|
||||
|
||||
let sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
||||
|
||||
assert!(sp_address2encrypted_keys.is_ok());
|
||||
|
||||
let encrypted_key = sp_address2encrypted_keys
|
||||
.unwrap()
|
||||
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
||||
.cloned();
|
||||
|
||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||
|
||||
assert!(ciphertext.is_ok());
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::Login,
|
||||
ciphertext.unwrap(),
|
||||
encrypted_key.unwrap(),
|
||||
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
||||
);
|
||||
|
||||
assert!(aes_dec.is_ok());
|
||||
|
||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||
|
||||
assert!(retrieved_plain.is_ok());
|
||||
|
||||
assert!(retrieved_plain.unwrap() == plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_encrypt_key_many() {
|
||||
let plaintext = [1u8; THIRTYTWO];
|
||||
let mut aes_enc =
|
||||
Aes256Encryption::new(Purpose::ThirtyTwoBytes, plaintext.to_vec()).unwrap();
|
||||
|
||||
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
||||
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
||||
HashMap::new();
|
||||
sp_address2shared_secrets.insert(
|
||||
ALICE_SP_ADDRESS.try_into().unwrap(),
|
||||
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
||||
);
|
||||
sp_address2shared_secrets.insert(
|
||||
BOB_SP_ADDRESS.try_into().unwrap(),
|
||||
SharedSecret::from_str(BOB_SHARED_SECRET).unwrap(),
|
||||
);
|
||||
shared_secrets.insert(
|
||||
Txid::from_str(TRANSACTION).unwrap(),
|
||||
sp_address2shared_secrets,
|
||||
);
|
||||
|
||||
aes_enc.set_shared_secret(shared_secrets);
|
||||
|
||||
let mut sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
||||
|
||||
assert!(sp_address2encrypted_keys.is_ok());
|
||||
|
||||
// Alice
|
||||
let encrypted_key = sp_address2encrypted_keys
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
||||
.cloned();
|
||||
|
||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::ThirtyTwoBytes,
|
||||
ciphertext.unwrap(),
|
||||
encrypted_key.unwrap(),
|
||||
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
||||
);
|
||||
|
||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||
|
||||
assert!(retrieved_plain.unwrap() == plaintext);
|
||||
|
||||
// Bob
|
||||
let encrypted_key = sp_address2encrypted_keys
|
||||
.unwrap()
|
||||
.get(&BOB_SP_ADDRESS.try_into().unwrap())
|
||||
.cloned();
|
||||
|
||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::ThirtyTwoBytes,
|
||||
ciphertext.unwrap(),
|
||||
encrypted_key.unwrap(),
|
||||
Some(SharedSecret::from_str(BOB_SHARED_SECRET).unwrap()),
|
||||
);
|
||||
|
||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||
|
||||
assert!(retrieved_plain.unwrap() == plaintext);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use anyhow::{Error, Result};
|
||||
use img_parts::{jpeg::Jpeg, Bytes, ImageEXIF};
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::SecretKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sp_backend::bitcoin::secp256k1::SecretKey;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackUpImage(Vec<u8>);
|
||||
|
@ -1,18 +1,27 @@
|
||||
#![allow(warnings)]
|
||||
use anyhow::Error;
|
||||
use sdk_common::crypto::AnkSharedSecret;
|
||||
use sdk_common::network::CachedMessage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
use tsify::Tsify;
|
||||
|
||||
mod Prd_list;
|
||||
pub mod api;
|
||||
mod crypto;
|
||||
mod images;
|
||||
mod network;
|
||||
mod peers;
|
||||
mod process;
|
||||
mod silentpayments;
|
||||
mod user;
|
||||
|
||||
pub static CACHEDMESSAGES: OnceLock<Mutex<Vec<CachedMessage>>> = OnceLock::new();
|
||||
|
||||
pub fn lock_messages() -> Result<MutexGuard<'static, Vec<CachedMessage>>, Error> {
|
||||
CACHEDMESSAGES
|
||||
.get_or_init(|| Mutex::new(vec![]))
|
||||
.lock_anyhow()
|
||||
}
|
||||
|
||||
pub(crate) trait MutexExt<T> {
|
||||
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
use anyhow::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tsify::Tsify;
|
||||
|
||||
const RAWTXTOPIC: &'static str = "rawtx";
|
||||
const RAWBLOCKTOPIC: &'static str = "rawblock";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum BitcoinTopic {
|
||||
RawTx,
|
||||
RawBlock,
|
||||
}
|
||||
|
||||
impl BitcoinTopic {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::RawTx => RAWTXTOPIC,
|
||||
Self::RawBlock => RAWBLOCKTOPIC,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
||||
#[tsify(from_wasm_abi, into_wasm_abi)]
|
||||
pub struct BitcoinNetworkMsg<'a> {
|
||||
pub topic: BitcoinTopic,
|
||||
pub data: &'a [u8],
|
||||
pub sequence: &'a [u8],
|
||||
pub addon: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> BitcoinNetworkMsg<'a> {
|
||||
pub fn new(raw_msg: &'a [u8]) -> Result<Self> {
|
||||
let topic: BitcoinTopic;
|
||||
let data: &[u8];
|
||||
let sequence: &[u8];
|
||||
let addon: &[u8];
|
||||
let addon_len: usize;
|
||||
let raw_msg_len = raw_msg.len();
|
||||
|
||||
if raw_msg.starts_with(RAWTXTOPIC.as_bytes()) {
|
||||
topic = BitcoinTopic::RawTx;
|
||||
addon_len = 33;
|
||||
} else if raw_msg.starts_with(RAWBLOCKTOPIC.as_bytes()) {
|
||||
topic = BitcoinTopic::RawBlock;
|
||||
addon_len = 0;
|
||||
} else {
|
||||
return Err(Error::msg("Unknown prefix"));
|
||||
}
|
||||
|
||||
data = &raw_msg[topic.as_str().as_bytes().len()..raw_msg_len - 4 - addon_len];
|
||||
sequence = &raw_msg[raw_msg_len - 4 - addon_len..];
|
||||
addon = &raw_msg[raw_msg_len - addon_len..];
|
||||
|
||||
Ok(Self {
|
||||
topic,
|
||||
data,
|
||||
sequence,
|
||||
addon,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AnkTopic {
|
||||
Faucet,
|
||||
}
|
||||
|
||||
impl AnkTopic {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Faucet => "faucet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnkNetworkMsg<'a> {
|
||||
pub topic: AnkTopic,
|
||||
pub content: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> AnkNetworkMsg<'a> {
|
||||
pub fn new(raw: &'a str) -> Result<Self> {
|
||||
if raw.starts_with(AnkTopic::Faucet.as_str()) {
|
||||
Ok(Self {
|
||||
topic: AnkTopic::Faucet,
|
||||
content: &raw[AnkTopic::Faucet.as_str().len()..],
|
||||
})
|
||||
} else {
|
||||
Err(Error::msg("Unknown 4nk message"))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,149 +1,68 @@
|
||||
use std::fmt::DebugStruct;
|
||||
|
||||
use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
||||
use tsify::Tsify;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub const HTML_CREATE_ID: &str = "
|
||||
pub const HTML_KOTPART: &str = "
|
||||
<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Create an Id</h3>
|
||||
<div><a href='#'>Processes</a></div>
|
||||
<h3>Send encrypted messages</h3>
|
||||
<div>
|
||||
<a href='#' id='send messages'>Send Messages</a>
|
||||
</div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' /><hr/>
|
||||
<input type='hidden' id='currentpage' value='creatid' />
|
||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||
<div class='side-by-side'>
|
||||
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
|
||||
<div>
|
||||
<a href='#' id='displayrecover'>Recover</a>
|
||||
</div>
|
||||
</div>
|
||||
</form><br/>
|
||||
<div id='passwordalert' class='passwordalert'></div>
|
||||
</div>
|
||||
";
|
||||
|
||||
pub const HTML_UPDATE_ID: &str = "
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div>
|
||||
<h3>Update an Id</h3>
|
||||
</div>
|
||||
<hr />
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='firstName'>First Name:</label>
|
||||
<input type='text' id='firstName' name='firstName' required />
|
||||
|
||||
<label for='lastName'>Last Name:</label>
|
||||
<input type='text' id='lastName' name='lastName' required />
|
||||
|
||||
<label for='Birthday'>Birthday:</label>
|
||||
<input type='date' id='Birthday' name='birthday' />
|
||||
|
||||
<label for='file'>File:</label>
|
||||
<input type='file' id='fileInput' name='file' />
|
||||
|
||||
<label>Third parties:</label>
|
||||
<div id='sp-address-block'>
|
||||
<div class='side-by-side'>
|
||||
<input
|
||||
type='text'
|
||||
name='sp-address'
|
||||
id='sp-address'
|
||||
placeholder='sp address'
|
||||
form='no-form'
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
class='circle-btn bg-secondary'
|
||||
id='add-sp-address-btn'
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='div-text-area'>
|
||||
<textarea
|
||||
name='bio'
|
||||
id=''
|
||||
cols='30'
|
||||
rows='10'
|
||||
placeholder='Bio'
|
||||
></textarea>
|
||||
</div>
|
||||
<button type='submit' class='bg-primary'>Update</button>
|
||||
<label for='sp_address'>Send to:</label>
|
||||
<input type='text' id='sp_address' />
|
||||
<hr/>
|
||||
<label for='message'>Message:</label>
|
||||
<input type='message' id='message' />
|
||||
<hr/>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
";
|
||||
|
||||
pub const HTML_RECOVER: &str = "
|
||||
pub const HTML_STORAGE: &str = "
|
||||
<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Recover my Id</h3>
|
||||
<div><a href='#'>Processes</a></div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' />
|
||||
<input type='hidden' id='currentpage' value='recover' />
|
||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||
<div class='side-by-side'>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
|
||||
<h3>Send encrypted messages</h3>
|
||||
<div>
|
||||
<a href='#' id='displaycreateid'>Create an Id</a>
|
||||
</div>
|
||||
</div><hr/>
|
||||
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
|
||||
</form><br/>
|
||||
<div id='passwordalert' class='passwordalert'></div>
|
||||
</div>
|
||||
";
|
||||
|
||||
pub const HTML_REVOKE_IMAGE: &str = "
|
||||
<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Revoke image</h3>
|
||||
<div><a href='#' id='displayupdateanid'>Update an Id</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='card-revoke'>
|
||||
<a href='#' download='revoke_4NK.jpg' id='revoke'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
|
||||
<path
|
||||
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class='image-container'>
|
||||
<img src='assets/4nk_revoke.jpg' alt='' />
|
||||
</div>
|
||||
</div>
|
||||
";
|
||||
|
||||
pub const HTML_REVOKE: &str = "
|
||||
<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Revoke an Id</h3>
|
||||
<div>
|
||||
<a href='#' id='displayrecover'>Recover</a>
|
||||
<a href='#' id='send messages'>Send Messages</a>
|
||||
</div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' />
|
||||
<label for='sp_address'>Send to:</label>
|
||||
<input type='text' id='sp_address' />
|
||||
<hr/>
|
||||
<div class='image-container'>
|
||||
<label class='image-label'>Revoke image</label>
|
||||
<img src='assets/revoke.jpeg' alt='' />
|
||||
<label for='message'>Message:</label>
|
||||
<input type='message' id='message' />
|
||||
<hr/>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
";
|
||||
|
||||
pub const HTML_MESSAGING: &str = "
|
||||
<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Send encrypted messages</h3>
|
||||
<div>
|
||||
<a href='#' id='send messages'>Send Messages</a>
|
||||
</div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<div id='our_address' class='our_address'></div>
|
||||
<label for='sp_address'>Send to:</label>
|
||||
<input type='text' id='sp_address' />
|
||||
<hr/>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
|
||||
<label for='message'>Message:</label>
|
||||
<input type='message' id='message' />
|
||||
<hr/>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
";
|
||||
|
@ -1,76 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use sp_backend::silentpayments::utils::receiving::calculate_shared_secret;
|
||||
use sp_backend::{
|
||||
bitcoin::{
|
||||
secp256k1::{PublicKey, Scalar, XOnlyPublicKey},
|
||||
Transaction,
|
||||
},
|
||||
silentpayments::receiving::Label,
|
||||
};
|
||||
|
||||
use crate::user::{lock_connected_users, CONNECTED_USERS};
|
||||
|
||||
type FoundOutputs = HashMap<Option<Label>, HashMap<XOnlyPublicKey, Scalar>>;
|
||||
|
||||
pub fn check_transaction(tx: Transaction, tweak_data: PublicKey) -> Result<FoundOutputs> {
|
||||
let connected_users = lock_connected_users()?;
|
||||
|
||||
let pubkeys_to_check: HashMap<XOnlyPublicKey, u32> = (0u32..)
|
||||
.zip(tx.output)
|
||||
.filter_map(|(i, o)| {
|
||||
if o.script_pubkey.is_p2tr() {
|
||||
let xonly = XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..])
|
||||
.expect("Transaction is invalid");
|
||||
Some((xonly, i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Check the transaction for all connected users
|
||||
for (pre_id, keys) in connected_users.clone() {
|
||||
let recover = keys.recover;
|
||||
let shared_secret =
|
||||
calculate_shared_secret(tweak_data, recover.get_client().get_scan_key())?;
|
||||
let res = recover
|
||||
.get_client()
|
||||
.sp_receiver
|
||||
.scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?;
|
||||
|
||||
if res.len() > 0 {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
if let Some(main) = keys.main {
|
||||
let shared_secret =
|
||||
calculate_shared_secret(tweak_data, main.get_client().get_scan_key())?;
|
||||
let res = main
|
||||
.get_client()
|
||||
.sp_receiver
|
||||
.scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?;
|
||||
|
||||
if res.len() > 0 {
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(revoke) = keys.revoke {
|
||||
let shared_secret =
|
||||
calculate_shared_secret(tweak_data, revoke.get_client().get_scan_key())?;
|
||||
let res = revoke
|
||||
.get_client()
|
||||
.sp_receiver
|
||||
.scan_transaction(&shared_secret, pubkeys_to_check.keys().cloned().collect())?;
|
||||
|
||||
if res.len() > 0 {
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HashMap::new())
|
||||
}
|
@ -1,18 +1,13 @@
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes_gcm::aead::Aead;
|
||||
use aes_gcm::AeadCore;
|
||||
use aes_gcm::KeyInit;
|
||||
use aes_gcm::{aead::Buffer, Aes256Gcm, Key};
|
||||
use anyhow::{Error, Result};
|
||||
use rand::{self, thread_rng, Rng, RngCore};
|
||||
use sdk_common::sp_client::bitcoin::hashes::Hash;
|
||||
use sdk_common::sp_client::bitcoin::hashes::HashEngine;
|
||||
use sdk_common::sp_client::bitcoin::hex::{DisplayHex, FromHex};
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::SecretKey;
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::ThirtyTwoByteHash;
|
||||
use sdk_common::sp_client::spclient::SpClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use sp_backend::bitcoin::hashes::Hash;
|
||||
use sp_backend::bitcoin::hashes::HashEngine;
|
||||
use sp_backend::bitcoin::hex::{DisplayHex, FromHex};
|
||||
use sp_backend::bitcoin::secp256k1::SecretKey;
|
||||
use sp_backend::bitcoin::secp256k1::ThirtyTwoByteHash;
|
||||
use sp_backend::spclient::SpClient;
|
||||
use tsify::Tsify;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@ -23,40 +18,45 @@ use std::io::{Cursor, Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use sp_backend::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||
use sp_backend::silentpayments::bitcoin_hashes::sha256;
|
||||
use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
||||
use sp_backend::spclient::SpendKey;
|
||||
use sp_backend::spclient::{OutputList, SpWallet};
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||
use sdk_common::sp_client::silentpayments::bitcoin_hashes::sha256;
|
||||
use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress;
|
||||
use sdk_common::sp_client::spclient::SpendKey;
|
||||
use sdk_common::sp_client::spclient::{OutputList, SpWallet};
|
||||
|
||||
use crate::crypto::{Aes256Decryption, Aes256Encryption, HalfKey, Purpose};
|
||||
use crate::peers::Peer;
|
||||
use crate::user;
|
||||
use crate::MutexExt;
|
||||
use sdk_common::crypto::{
|
||||
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, HalfKey, KeyInit, Purpose,
|
||||
};
|
||||
|
||||
type PreId = String;
|
||||
|
||||
const MANAGERS_NUMBER: u8 = 10;
|
||||
const QUORUM_SHARD: f32 = 0.8;
|
||||
|
||||
type UsersMap = HashMap<PreId, UserWallets>;
|
||||
pub static CONNECTED_USERS: OnceLock<Mutex<UsersMap>> = OnceLock::new();
|
||||
pub static CONNECTED_USER: OnceLock<Mutex<UserWallets>> = OnceLock::new();
|
||||
|
||||
pub fn lock_connected_users() -> Result<MutexGuard<'static, UsersMap>> {
|
||||
CONNECTED_USERS
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
pub fn lock_connected_user() -> Result<MutexGuard<'static, UserWallets>> {
|
||||
CONNECTED_USER
|
||||
.get_or_init(|| Mutex::new(UserWallets::default()))
|
||||
.lock_anyhow()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct UserWallets {
|
||||
pub main: Option<SpWallet>,
|
||||
pub recover: SpWallet,
|
||||
pub revoke: Option<SpWallet>,
|
||||
main: Option<SpWallet>,
|
||||
recover: Option<SpWallet>,
|
||||
revoke: Option<SpWallet>,
|
||||
}
|
||||
|
||||
impl UserWallets {
|
||||
pub fn new(main: Option<SpWallet>, recover: SpWallet, revoke: Option<SpWallet>) -> Self {
|
||||
pub fn new(
|
||||
main: Option<SpWallet>,
|
||||
recover: Option<SpWallet>,
|
||||
revoke: Option<SpWallet>,
|
||||
) -> Self {
|
||||
Self {
|
||||
main,
|
||||
recover,
|
||||
@ -64,8 +64,56 @@ impl UserWallets {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_revoke(&self) -> Option<&SpWallet> {
|
||||
self.revoke.as_ref()
|
||||
pub fn try_get_revoke(&self) -> Result<&SpWallet> {
|
||||
if let Some(revoke) = &self.revoke {
|
||||
Ok(revoke)
|
||||
} else {
|
||||
Err(Error::msg("No revoke wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_recover(&self) -> Result<&SpWallet> {
|
||||
if let Some(recover) = &self.recover {
|
||||
Ok(recover)
|
||||
} else {
|
||||
Err(Error::msg("No recover wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_main(&self) -> Result<&SpWallet> {
|
||||
if let Some(main) = &self.main {
|
||||
Ok(main)
|
||||
} else {
|
||||
Err(Error::msg("No main wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_mut_revoke(&mut self) -> Result<&mut SpWallet> {
|
||||
if let Some(revoke) = &mut self.revoke {
|
||||
Ok(revoke)
|
||||
} else {
|
||||
Err(Error::msg("No revoke wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_mut_recover(&mut self) -> Result<&mut SpWallet> {
|
||||
if let Some(recover) = &mut self.recover {
|
||||
Ok(recover)
|
||||
} else {
|
||||
Err(Error::msg("No recover wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_mut_main(&mut self) -> Result<&mut SpWallet> {
|
||||
if let Some(main) = &mut self.main {
|
||||
Ok(main)
|
||||
} else {
|
||||
Err(Error::msg("No main wallet available"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_not_empty(&self) -> bool {
|
||||
self.get_all_outputs().len() > 0
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_outputs(&self) -> Vec<OutputList> {
|
||||
@ -76,7 +124,9 @@ impl UserWallets {
|
||||
if let Some(revoke) = &self.revoke {
|
||||
res.push(revoke.get_outputs().clone());
|
||||
}
|
||||
res.push(self.recover.get_outputs().clone());
|
||||
if let Some(recover) = &self.recover {
|
||||
res.push(recover.get_outputs().clone());
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
@ -96,22 +146,24 @@ pub struct User {
|
||||
|
||||
impl User {
|
||||
pub fn new(user_wallets: UserWallets, user_password: String, process: String) -> Result<Self> {
|
||||
// if we are already logged in, abort
|
||||
if lock_connected_user()?.is_not_empty() {
|
||||
return Err(Error::msg("User already logged in"));
|
||||
}
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// image revoke
|
||||
// We just take the 2 revoke keys
|
||||
let mut revoke_data = Vec::with_capacity(64);
|
||||
if let Some(revoke) = user_wallets.try_get_revoke() {
|
||||
let revoke = user_wallets.try_get_revoke()?;
|
||||
revoke_data.extend_from_slice(revoke.get_client().get_scan_key().as_ref());
|
||||
revoke_data.extend_from_slice(revoke.get_client().try_get_secret_spend_key()?.as_ref());
|
||||
} else {
|
||||
return Err(Error::msg("No revoke wallet available"));
|
||||
}
|
||||
|
||||
// Take the 2 recover keys
|
||||
// split recover spend key
|
||||
let recover_spend_key = user_wallets
|
||||
.recover
|
||||
.try_get_recover()?
|
||||
.get_client()
|
||||
.try_get_secret_spend_key()?
|
||||
.clone();
|
||||
@ -189,7 +241,7 @@ impl User {
|
||||
let scan_key_encryption = Aes256Encryption::import_key(
|
||||
Purpose::ThirtyTwoBytes,
|
||||
user_wallets
|
||||
.recover
|
||||
.try_get_recover()?
|
||||
.get_client()
|
||||
.get_scan_key()
|
||||
.secret_bytes()
|
||||
@ -203,6 +255,8 @@ impl User {
|
||||
|
||||
recover_data.extend_from_slice(&cipher_scan_key);
|
||||
|
||||
let all_outputs = user_wallets.get_all_outputs();
|
||||
|
||||
Ok(User {
|
||||
pre_id: pre_id.to_string(),
|
||||
processes: vec![process],
|
||||
@ -210,10 +264,19 @@ impl User {
|
||||
recover_data,
|
||||
revoke_data: Some(revoke_data),
|
||||
shares,
|
||||
outputs: user_wallets.get_all_outputs(),
|
||||
outputs: all_outputs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn logout() -> Result<()> {
|
||||
if let Ok(mut user) = lock_connected_user() {
|
||||
*user = UserWallets::default();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg("Failed to lock CONNECTED_USER"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn login(
|
||||
pre_id: PreId,
|
||||
user_password: String,
|
||||
@ -221,6 +284,11 @@ impl User {
|
||||
shares: &[Vec<u8>],
|
||||
outputs: &[OutputList],
|
||||
) -> Result<()> {
|
||||
// if we are already logged in, abort
|
||||
if lock_connected_user()?.is_not_empty() {
|
||||
return Err(Error::msg("User already logged in"));
|
||||
}
|
||||
|
||||
let mut retrieved_spend_key = [0u8; 32];
|
||||
let mut retrieved_scan_key = [0u8; 32];
|
||||
let mut entropy1 = [0u8; 32];
|
||||
@ -244,21 +312,6 @@ impl User {
|
||||
return Err(Error::msg("pre_id and recover_data don't match"));
|
||||
}
|
||||
|
||||
// If we already have loaded a user with this pre_id, abort
|
||||
if let Some(current_users) = CONNECTED_USERS.get() {
|
||||
if current_users
|
||||
.to_owned()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&pre_id)
|
||||
{
|
||||
return Err(Error::msg(format!(
|
||||
"User with pre_id {} already logged in",
|
||||
pre_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
retrieved_spend_key[..16].copy_from_slice(&Self::recover_part1(
|
||||
&user_password,
|
||||
&entropy1,
|
||||
@ -293,26 +346,12 @@ impl User {
|
||||
|
||||
let recover_wallet = SpWallet::new(recover_client, recover_outputs)?;
|
||||
|
||||
// Adding user to CONNECTED_USERS
|
||||
if let Some(current_users) = CONNECTED_USERS.get() {
|
||||
let mut lock = current_users.to_owned().lock().unwrap();
|
||||
if lock.contains_key(&pre_id) {
|
||||
return Err(Error::msg(format!(
|
||||
"User with pre_id {} already exists",
|
||||
pre_id
|
||||
)));
|
||||
let user_wallets = UserWallets::new(None, Some(recover_wallet), None);
|
||||
|
||||
if let Ok(mut user) = lock_connected_user() {
|
||||
*user = user_wallets;
|
||||
} else {
|
||||
lock.insert(pre_id.clone(), UserWallets::new(None, recover_wallet, None));
|
||||
}
|
||||
} else {
|
||||
let mut user_map = HashMap::new();
|
||||
user_map.insert(pre_id, UserWallets::new(None, recover_wallet, None));
|
||||
let new_value = Mutex::new(user_map);
|
||||
if let Err(error) = CONNECTED_USERS.set(new_value) {
|
||||
return Err(Error::msg(
|
||||
"Failed to set the CONNECTED_USERS static variable",
|
||||
));
|
||||
}
|
||||
return Err(Error::msg("Failed to lock CONNECTED_USER"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -324,12 +363,8 @@ impl User {
|
||||
engine.write_all(&entropy);
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::ThirtyTwoBytes,
|
||||
ciphertext,
|
||||
hash.to_byte_array().to_vec(),
|
||||
None,
|
||||
)?;
|
||||
let aes_dec =
|
||||
Aes256Decryption::new(Purpose::ThirtyTwoBytes, ciphertext, hash.to_byte_array())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
@ -340,12 +375,7 @@ impl User {
|
||||
engine.write_all(&entropy);
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::Login,
|
||||
ciphertext,
|
||||
hash.to_byte_array().to_vec(),
|
||||
None,
|
||||
)?;
|
||||
let aes_dec = Aes256Decryption::new(Purpose::Login, ciphertext, hash.to_byte_array())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
@ -364,12 +394,7 @@ impl User {
|
||||
.ok_or_else(|| anyhow::Error::msg("Failed to retrieve the sharded secret"))?,
|
||||
)?;
|
||||
|
||||
let aes_dec = Aes256Decryption::new(
|
||||
Purpose::Login,
|
||||
part2_key_enc,
|
||||
hash.to_byte_array().to_vec(),
|
||||
None,
|
||||
)?;
|
||||
let aes_dec = Aes256Decryption::new(Purpose::Login, part2_key_enc, hash.to_byte_array())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
@ -459,21 +484,26 @@ mod tests {
|
||||
.unwrap();
|
||||
let user_wallets = UserWallets::new(
|
||||
Some(SpWallet::new(sp_main, None).unwrap()),
|
||||
SpWallet::new(sp_recover, None).unwrap(),
|
||||
Some(SpWallet::new(sp_recover, None).unwrap()),
|
||||
Some(SpWallet::new(sp_revoke, None).unwrap()),
|
||||
);
|
||||
|
||||
user_wallets
|
||||
}
|
||||
|
||||
// Test 1: Create User
|
||||
#[test]
|
||||
fn test_successful_creation() {
|
||||
let user_wallets = helper_create_user_wallets();
|
||||
let result = User::new(user_wallets, USER_PASSWORD.to_owned(), PROCESS.to_owned());
|
||||
|
||||
assert!(result.is_ok());
|
||||
let user = result.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logout() {
|
||||
let res = User::logout();
|
||||
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -496,9 +526,9 @@ mod tests {
|
||||
|
||||
assert!(res.is_ok());
|
||||
|
||||
let connected = CONNECTED_USERS.get().unwrap().lock().unwrap();
|
||||
let connected = lock_connected_user().unwrap();
|
||||
|
||||
let recover = &connected.get(&user.pre_id).unwrap().recover;
|
||||
let recover = connected.try_get_recover().unwrap();
|
||||
|
||||
assert!(
|
||||
format!(
|
||||
|
@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler",
|
||||
"build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler --dev",
|
||||
"start": "webpack serve",
|
||||
"build": "webpack"
|
||||
},
|
||||
|
@ -4,32 +4,10 @@ class Database {
|
||||
private dbName: string = '4nk';
|
||||
private dbVersion: number = 1;
|
||||
private storeDefinitions = {
|
||||
// SpClient: {
|
||||
// name: "sp_client",
|
||||
// options: {},
|
||||
// indices: []
|
||||
// },
|
||||
// SpOutputs: {
|
||||
// name: "sp_outputs",
|
||||
// options: {'autoIncrement': true},
|
||||
// indices: [{
|
||||
// name: 'by_wallet_fingerprint',
|
||||
// keyPath: 'wallet_fingerprint',
|
||||
// options: {
|
||||
// 'unique': false
|
||||
// }
|
||||
// }]
|
||||
// },
|
||||
AnkUser: {
|
||||
name: "user",
|
||||
options: {'keyPath': 'pre_id'},
|
||||
indices: [{
|
||||
name: 'by_process',
|
||||
keyPath: 'process',
|
||||
options: {
|
||||
'unique': false
|
||||
}
|
||||
}]
|
||||
indices: []
|
||||
},
|
||||
AnkSession: {
|
||||
name: "session",
|
||||
@ -46,6 +24,11 @@ class Database {
|
||||
'unique': true
|
||||
}
|
||||
}]
|
||||
},
|
||||
AnkMessages: {
|
||||
name: "messages",
|
||||
options: {'keyPath': 'id'},
|
||||
indices: []
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,11 +75,11 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
public getDb(): IDBDatabase {
|
||||
public async getDb(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
await this.init();
|
||||
}
|
||||
return this.db;
|
||||
return this.db!;
|
||||
}
|
||||
|
||||
public getStoreList(): {[key: string]: string} {
|
||||
@ -134,6 +117,17 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
public rmObject(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
public getFirstMatchWithIndex<T>(db: IDBDatabase, storeName: string, indexName: string, lookup: string): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readonly');
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Services from './services';
|
||||
import { WebSocketClient } from './websockets';
|
||||
|
||||
const wsurl = "ws://192.168.1.44:8090";
|
||||
const wsurl = "ws://localhost:8090";
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const services = await Services.getInstance();
|
||||
|
628
src/services.ts
628
src/services.ts
@ -1,4 +1,4 @@
|
||||
import { createUserReturn, User, Process } from '../dist/pkg/sdk_client';
|
||||
import { createUserReturn, User, Process, createTransactionReturn, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, CipherMessage, CachedMessage } from '../dist/pkg/sdk_client';
|
||||
import IndexedDB from './database'
|
||||
import { WebSocketClient } from './websockets';
|
||||
|
||||
@ -7,6 +7,7 @@ class Services {
|
||||
private sdkClient: any;
|
||||
private current_process: string | null = null;
|
||||
private websocketConnection: WebSocketClient[] = [];
|
||||
private sp_address: string | null = null;
|
||||
|
||||
// Private constructor to prevent direct instantiation from outside
|
||||
private constructor() {}
|
||||
@ -24,27 +25,9 @@ class Services {
|
||||
private async init(): Promise<void> {
|
||||
this.sdkClient = await import("../dist/pkg/sdk_client");
|
||||
this.sdkClient.setup();
|
||||
await this.updateProcesses();
|
||||
}
|
||||
|
||||
// public async getSpAddressDefaultClient(): Promise<string | null> {
|
||||
// try {
|
||||
// const indexedDB = await IndexedDB.getInstance();
|
||||
// const db = indexedDB.getDb();
|
||||
// const spClient = await indexedDB.getObject<string>(db, indexedDB.getStoreList().SpClient, "default");
|
||||
|
||||
// if (spClient) {
|
||||
// return this.sdkClient.get_receiving_address(spClient);
|
||||
// } else {
|
||||
// console.error("SP client not found");
|
||||
// return null;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Failed to retrieve object or get sp address:", error);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
public async addWebsocketConnection(url: string): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const newClient = new WebSocketClient(url, services);
|
||||
@ -57,7 +40,7 @@ class Services {
|
||||
let isNew = false;
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
let userListObject = await indexedDB.getAll<User>(db, indexedDB.getStoreList().AnkUser);
|
||||
if (userListObject.length == 0) {
|
||||
isNew = true;
|
||||
@ -70,21 +53,99 @@ class Services {
|
||||
|
||||
public async displayCreateId(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
await services.injectHtml('CREATE_ID');
|
||||
await services.createIdInjectHtml();
|
||||
services.attachSubmitListener("form4nk", (event) => services.createId(event));
|
||||
services.attachClickListener("displayrecover", services.displayRecover);
|
||||
await services.displayProcess();
|
||||
}
|
||||
|
||||
public async displaySendMessage(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
await services.injectHtml('Messaging');
|
||||
services.attachSubmitListener("form4nk", (event) => services.sendMessage(event));
|
||||
// const ourAddress = document.getElementById('our_address');
|
||||
// if (ourAddress) {
|
||||
// ourAddress.innerHTML = `<strong>Our Address:</strong> ${this.sp_address}`
|
||||
// }
|
||||
// services.attachClickListener("displaysendmessage", services.displaySendMessage);
|
||||
// await services.displayProcess();
|
||||
}
|
||||
|
||||
public async sendMessage(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const services = await Services.getInstance();
|
||||
let availableAmt: number = 0;
|
||||
|
||||
// check available amount
|
||||
try {
|
||||
availableAmt = await services.sdkClient.get_available_amount_for_user(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to get available amount');
|
||||
return;
|
||||
}
|
||||
|
||||
if (availableAmt < 2000) {
|
||||
try {
|
||||
await services.obtainTokenWithFaucet();
|
||||
} catch (error) {
|
||||
console.error('Failed to obtain faucet token:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const spAddressElement = document.getElementById("sp_address") as HTMLInputElement;
|
||||
const messageElement = document.getElementById("message") as HTMLInputElement;
|
||||
|
||||
if (!spAddressElement || !messageElement) {
|
||||
console.error("One or more elements not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientSpAddress = spAddressElement.value;
|
||||
const message = messageElement.value;
|
||||
|
||||
const msg_payload: CipherMessage = {sender: this.sp_address!, message: message, error: null};
|
||||
|
||||
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload);
|
||||
if (notificationInfo) {
|
||||
let networkMsg = notificationInfo.new_network_msg;
|
||||
console.debug(networkMsg);
|
||||
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
const flag: AnkFlag = 'Cipher';
|
||||
try {
|
||||
// send message (transaction in envelope)
|
||||
await services.updateMessages(networkMsg);
|
||||
connection?.sendMessage(flag, networkMsg.ciphertext!);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
// add peers list
|
||||
// add processes list
|
||||
}
|
||||
}
|
||||
|
||||
public async createId(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
// verify we don't already have an user
|
||||
const services = await Services.getInstance();
|
||||
try {
|
||||
let user = await services.getUserInfo();
|
||||
if (user) {
|
||||
console.error("User already exists, please recover");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const passwordElement = document.getElementById("password") as HTMLInputElement;
|
||||
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
|
||||
|
||||
if (!passwordElement || !processElement) {
|
||||
console.error("One or more elements not found");
|
||||
return;
|
||||
throw 'One or more elements not found';
|
||||
}
|
||||
|
||||
const password = passwordElement.value;
|
||||
@ -97,47 +158,46 @@ class Services {
|
||||
const birthday_signet = 50000;
|
||||
const birthday_main = 500000;
|
||||
|
||||
const services = await Services.getInstance();
|
||||
let createUserReturn: createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
|
||||
let createUserReturn: createUserReturn;
|
||||
try {
|
||||
createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let user = createUserReturn.user;
|
||||
|
||||
const shares = user.shares;
|
||||
// const shares = user.shares;
|
||||
// send the shares on the network
|
||||
const revokeData = user.revoke_data;
|
||||
if (!revokeData) {
|
||||
console.error('Failed to get revoke data from wasm');
|
||||
return;
|
||||
throw 'Failed to get revoke data from wasm';
|
||||
}
|
||||
|
||||
user.shares = [];
|
||||
// user.shares = [];
|
||||
user.revoke_data = null;
|
||||
|
||||
try {
|
||||
const indexedDb = await IndexedDB.getInstance();
|
||||
const db = indexedDb.getDb();
|
||||
const db = await indexedDb.getDb();
|
||||
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user, null);
|
||||
} catch (error) {
|
||||
console.error("Failed to write user object :", error);
|
||||
throw `Failed to write user object: ${error}`;
|
||||
}
|
||||
|
||||
let sp_address = "";
|
||||
try {
|
||||
sp_address = services.sdkClient.get_receiving_address(user.pre_id);
|
||||
console.info('Using sp_address:', sp_address);
|
||||
await services.obtainTokenWithFaucet();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await services.obtainTokenWithFaucet(sp_address);
|
||||
|
||||
await services.displayRevokeImage(new Uint8Array(revokeData));
|
||||
}
|
||||
|
||||
public async displayRecover(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
await services.injectHtml('RECOVER');
|
||||
services.attachSubmitListener("form4nk", services.recover);
|
||||
await services.recoverInjectHtml();
|
||||
services.attachSubmitListener("form4nk", (event) => services.recover(event));
|
||||
services.attachClickListener("displaycreateid", services.displayCreateId);
|
||||
services.attachClickListener("displayrevoke", services.displayRevoke);
|
||||
services.attachClickListener("submitButtonRevoke", services.revoke);
|
||||
@ -146,7 +206,6 @@ class Services {
|
||||
|
||||
public async recover(event: Event) {
|
||||
event.preventDefault();
|
||||
console.log("JS recover submit ");
|
||||
|
||||
const passwordElement = document.getElementById("password") as HTMLInputElement;
|
||||
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
|
||||
@ -158,17 +217,36 @@ class Services {
|
||||
|
||||
const password = passwordElement.value;
|
||||
const process = processElement.value;
|
||||
console.log("JS password: " + password + " process: " + process);
|
||||
// console.log("JS password: " + password + " process: " + process);
|
||||
// To comment if test
|
||||
// if (!Services.instance.isPasswordValid(password)) return;
|
||||
|
||||
// TODO
|
||||
alert("Recover submit to do ...");
|
||||
// Get user in db
|
||||
const services = await Services.getInstance();
|
||||
try {
|
||||
const user = await services.getUserInfo();
|
||||
if (user) {
|
||||
services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs);
|
||||
this.sp_address = services.sdkClient.get_recover_address();
|
||||
if (this.sp_address) {
|
||||
console.info('Using sp_address:', this.sp_address);
|
||||
await services.obtainTokenWithFaucet();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.info(this.sp_address);
|
||||
|
||||
// TODO: check blocks since last_scan and update outputs
|
||||
|
||||
await services.displaySendMessage();
|
||||
}
|
||||
|
||||
public async displayRevokeImage(revokeData: Uint8Array): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
await services.injectHtml('REVOKE_IMAGE');
|
||||
await services.revokeImageInjectHtml();
|
||||
services.attachClickListener("displayupdateanid", services.displayUpdateAnId);
|
||||
|
||||
let imageBytes = await services.getRecoverImage('assets/4nk_revoke.jpg');
|
||||
@ -176,6 +254,12 @@ class Services {
|
||||
var elem = document.getElementById("revoke") as HTMLAnchorElement;
|
||||
if (elem != null) {
|
||||
let imageWithData = services.sdkClient.add_data_to_image(imageBytes, revokeData, true);
|
||||
const blob = new Blob([imageWithData], { type: 'image/jpeg' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Set the href attribute for download
|
||||
elem.href = url;
|
||||
elem.download = 'revoke_4NK.jpg';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,21 +279,9 @@ class Services {
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
public async parseBitcoinMessage(raw: Blob): Promise<string | null> {
|
||||
try {
|
||||
const buffer = await raw.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
const msg: string = this.sdkClient.parse_bitcoin_network_msg(uint8Array);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error("Error processing the blob:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async displayRevoke(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
services.injectHtml('REVOKE');
|
||||
await services.revokeInjectHtml();
|
||||
services.attachClickListener("displayrecover", Services.instance.displayRecover);
|
||||
services.attachSubmitListener("form4nk", Services.instance.revoke);
|
||||
}
|
||||
@ -224,51 +296,19 @@ class Services {
|
||||
public async displayUpdateAnId() {
|
||||
const services = await Services.getInstance();
|
||||
|
||||
console.log("JS displayUpdateAnId process : "+services.current_process);
|
||||
let body = "";
|
||||
let style = "";
|
||||
let script = "";
|
||||
try {
|
||||
const processObject = await services.getProcessByName("UPDATE_ID");
|
||||
if (processObject) {
|
||||
body = processObject.html;
|
||||
style = processObject.style;
|
||||
script = processObject.script;
|
||||
console.log("JS displayUpdateAnId body : "+body);
|
||||
await services.updateIdInjectHtml();
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve process with Error:", error);
|
||||
}
|
||||
|
||||
services.injectUpdateAnIdHtml(body, style, script);
|
||||
services.attachSubmitListener("form4nk", services.updateAnId);
|
||||
}
|
||||
|
||||
public async parse4nkMessage(raw: string): Promise<string | null> {
|
||||
const msg: string = this.sdkClient.parse_4nk_msg(raw);
|
||||
public async parseNetworkMessage(raw: string, feeRate: number): Promise<CachedMessage> {
|
||||
const services = await Services.getInstance();
|
||||
try {
|
||||
const msg: CachedMessage = services.sdkClient.parse_network_msg(raw, feeRate);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
public injectUpdateAnIdHtml(bodyToInject: string, styleToInject: string, scriptToInject: string) {
|
||||
console.log("JS html : "+bodyToInject);
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
|
||||
if (!body) {
|
||||
console.error("No body tag");
|
||||
return;
|
||||
}
|
||||
body.innerHTML = styleToInject + bodyToInject;
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.innerHTML = scriptToInject;
|
||||
document.body.appendChild(script);
|
||||
script.onload = () => {
|
||||
console.log('Script loaded successfuly');
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.log('Error loading script');
|
||||
};
|
||||
}
|
||||
|
||||
public async updateAnId(event: Event): Promise<void> {
|
||||
@ -304,7 +344,7 @@ class Services {
|
||||
public async addProcess(process: Process): Promise<void> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
|
||||
} catch (error) {
|
||||
console.log('addProcess failed: ',error);
|
||||
@ -314,7 +354,7 @@ class Services {
|
||||
public async getAllProcess(): Promise<Process[]> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
let processListObject = await indexedDB.getAll<Process>(db, indexedDB.getStoreList().AnkProcess);
|
||||
return processListObject;
|
||||
} catch (error) {
|
||||
@ -323,13 +363,25 @@ class Services {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkTransaction(tx: string): Promise<string | null> {
|
||||
public async updateOwnedOutputsForUser(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
let latest_outputs: outputs_list;
|
||||
try {
|
||||
return services.sdkClient.check_network_transaction(tx);
|
||||
latest_outputs = services.sdkClient.get_outpoints_for_user();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let user = await services.getUserInfo();
|
||||
if (user) {
|
||||
user.outputs = latest_outputs;
|
||||
// console.warn(user);
|
||||
await services.updateUser(user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,7 +391,7 @@ class Services {
|
||||
let userProcessList: Process[] = [];
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
user = await indexedDB.getObject<User>(db, indexedDB.getStoreList().AnkUser, pre_id);
|
||||
} catch (error) {
|
||||
console.error('getAllUserProcess failed: ',error);
|
||||
@ -363,29 +415,49 @@ class Services {
|
||||
public async getProcessByName(name: string): Promise<Process | null> {
|
||||
console.log('getProcessByName name: '+name);
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
const process = await indexedDB.getFirstMatchWithIndex<Process>(db, indexedDB.getStoreList().AnkProcess, 'by_name', name);
|
||||
console.log('getProcessByName process: '+process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
public async loadProcesses(): Promise<void> {
|
||||
public async updateMessages(message: CachedMessage): Promise<void> {
|
||||
const indexedDb = await IndexedDB.getInstance();
|
||||
const db = await indexedDb.getDb();
|
||||
|
||||
try {
|
||||
await indexedDb.setObject(db, indexedDb.getStoreList().AnkMessages, message, null);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async removeMessage(id: number): Promise<void> {
|
||||
const indexedDb = await IndexedDB.getInstance();
|
||||
const db = await indexedDb.getDb();
|
||||
|
||||
try {
|
||||
await indexedDb.rmObject(db, indexedDb.getStoreList().AnkMessages, id);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateProcesses(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const processList: Process[] = services.sdkClient.get_processes();
|
||||
console.error('processList size: '+processList.length);
|
||||
|
||||
processList.forEach(async (process: Process) => {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const db = await indexedDB.getDb();
|
||||
try {
|
||||
const processStore = await indexedDB.getObject<Process>(db, indexedDB.getStoreList().AnkProcess, process.id);
|
||||
if (!processStore) {
|
||||
console.error('Adding process.id : '+process.id);
|
||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error while writing process', process.name, 'to indexedDB:', error);
|
||||
console.error('Error while writing process', process.name, 'to indexedDB:', error);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -401,6 +473,190 @@ class Services {
|
||||
element?.removeEventListener("submit", callback);
|
||||
element?.addEventListener("submit", callback);
|
||||
}
|
||||
public async revokeInjectHtml() {
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
` <div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Revoke an Id</h3>
|
||||
<div>
|
||||
<a href='#' id='displayrecover'>Recover</a>
|
||||
</div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' />
|
||||
<hr/>
|
||||
<div class='image-container'>
|
||||
<label class='image-label'>Revoke image</label>
|
||||
<img src='assets/revoke.jpeg' alt='' />
|
||||
</div>
|
||||
<hr/>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
public async revokeImageInjectHtml() {
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
`<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Revoke image</h3>
|
||||
<div><a href='#' id='displayupdateanid'>Update an Id</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='card-revoke'>
|
||||
<a href='#' download='revoke_4NK.jpg' id='revoke'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
|
||||
<path
|
||||
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class='image-container'>
|
||||
<img src='assets/4nk_revoke.jpg' alt='' />
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
public async recoverInjectHtml() {
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
const services = await Services.getInstance();
|
||||
await services.updateProcesses();
|
||||
|
||||
container.innerHTML =
|
||||
`<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Recover my Id</h3>
|
||||
<div><a href='#'>Processes</a></div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' />
|
||||
<input type='hidden' id='currentpage' value='recover' />
|
||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||
<div class='side-by-side'>
|
||||
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
|
||||
<div>
|
||||
<a href='#' id='displaycreateid'>Create an Id</a>
|
||||
</div>
|
||||
</div><hr/>
|
||||
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
|
||||
</form><br/>
|
||||
<div id='passwordalert' class='passwordalert'></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
public async createIdInjectHtml() {
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
`<div class='card'>
|
||||
<div class='side-by-side'>
|
||||
<h3>Create an Id</h3>
|
||||
<div><a href='#'>Processes</a></div>
|
||||
</div>
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='password'>Password :</label>
|
||||
<input type='password' id='password' /><hr/>
|
||||
<input type='hidden' id='currentpage' value='creatid' />
|
||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||
<div class='side-by-side'>
|
||||
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
|
||||
<div>
|
||||
<a href='#' id='displayrecover'>Recover</a>
|
||||
</div>
|
||||
</div>
|
||||
</form><br/>
|
||||
<div id='passwordalert' class='passwordalert'></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
public async updateIdInjectHtml() {
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
`<body>
|
||||
<div class='container'>
|
||||
<div>
|
||||
<h3>Update an Id</h3>
|
||||
</div>
|
||||
<hr />
|
||||
<form id='form4nk' action='#'>
|
||||
<label for='firstName'>First Name:</label>
|
||||
<input type='text' id='firstName' name='firstName' required />
|
||||
|
||||
<label for='lastName'>Last Name:</label>
|
||||
<input type='text' id='lastName' name='lastName' required />
|
||||
|
||||
<label for='Birthday'>Birthday:</label>
|
||||
<input type='date' id='Birthday' name='birthday' />
|
||||
|
||||
<label for='file'>File:</label>
|
||||
<input type='file' id='fileInput' name='file' />
|
||||
|
||||
<label>Third parties:</label>
|
||||
<div id='sp-address-block'>
|
||||
<div class='side-by-side'>
|
||||
<input
|
||||
type='text'
|
||||
name='sp-address'
|
||||
id='sp-address'
|
||||
placeholder='sp address'
|
||||
form='no-form'
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
class='circle-btn bg-secondary'
|
||||
id='add-sp-address-btn'
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='div-text-area'>
|
||||
<textarea
|
||||
name='bio'
|
||||
id=''
|
||||
cols='30'
|
||||
rows='10'
|
||||
placeholder='Bio'
|
||||
></textarea>
|
||||
</div>
|
||||
<button type='submit' class='bg-primary'>Update</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>`;
|
||||
}
|
||||
|
||||
public async injectHtml(processName: string) {
|
||||
const container = document.getElementById('containerId');
|
||||
@ -492,19 +748,161 @@ class Services {
|
||||
}
|
||||
}
|
||||
|
||||
public async obtainTokenWithFaucet(spaddress: string): Promise<string | null> {
|
||||
public async obtainTokenWithFaucet(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
if (!connection) {
|
||||
return null;
|
||||
throw 'no available relay connections';
|
||||
}
|
||||
|
||||
let cachedMsg: CachedMessage;
|
||||
try {
|
||||
connection.sendMessage('faucet'+spaddress);
|
||||
} catch (error) {
|
||||
console.error("Failed to obtain tokens with relay ", connection.getUrl());
|
||||
return null;
|
||||
const flag: AnkFlag = 'Faucet';
|
||||
cachedMsg = services.sdkClient.create_faucet_msg();
|
||||
if (cachedMsg.commitment && cachedMsg.recipient) {
|
||||
let faucetMsg: FaucetMessage = {
|
||||
sp_address: cachedMsg.recipient,
|
||||
commitment: cachedMsg.commitment,
|
||||
error: null,
|
||||
}
|
||||
connection.sendMessage(flag, JSON.stringify(faucetMsg));
|
||||
}
|
||||
} catch (error) {
|
||||
throw `Failed to obtain tokens with relay ${connection.getUrl()}: ${error}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await services.updateMessages(cachedMsg);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateUser(user: User): Promise<void> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
await indexedDB.setObject(db, indexedDB.getStoreList().AnkUser, user, null);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserInfo(): Promise<User | null> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
let user = await indexedDB.getAll<User>(db, indexedDB.getStoreList().AnkUser);
|
||||
// This should never happen
|
||||
if (user.length > 1) {
|
||||
throw "Multiple users in db";
|
||||
} else {
|
||||
let res = user.pop();
|
||||
if (res === undefined) {
|
||||
return null;
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async answer_confirmation_message(msg: CachedMessage): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
if (!connection) {
|
||||
throw new Error("No connection to relay");
|
||||
}
|
||||
let user: User;
|
||||
try {
|
||||
let possibleUser = await services.getUserInfo();
|
||||
if (!possibleUser) {
|
||||
throw new Error("No user loaded, please first create a new user or login");
|
||||
} else {
|
||||
user = possibleUser;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let notificationInfo: createTransactionReturn;
|
||||
try {
|
||||
const feeRate = 1;
|
||||
notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, feeRate);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create confirmation transaction: ${error}`);
|
||||
}
|
||||
const flag: AnkFlag = "NewTx";
|
||||
const newTxMsg: NewTxMessage = {
|
||||
'transaction': notificationInfo.transaction,
|
||||
'tweak_data': null,
|
||||
'error': null,
|
||||
}
|
||||
connection.sendMessage(flag, JSON.stringify(newTxMsg));
|
||||
await services.updateMessages(notificationInfo.new_network_msg);
|
||||
return;
|
||||
}
|
||||
|
||||
public async confirm_sender_address(msg: CachedMessage): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
if (!connection) {
|
||||
throw new Error("No connection to relay");
|
||||
}
|
||||
let user: User;
|
||||
try {
|
||||
let possibleUser = await services.getUserInfo();
|
||||
if (!possibleUser) {
|
||||
throw new Error("No user loaded, please first create a new user or login");
|
||||
} else {
|
||||
user = possibleUser;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let notificationInfo: createTransactionReturn;
|
||||
try {
|
||||
const feeRate = 1;
|
||||
notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create confirmation transaction: ${error}`);
|
||||
}
|
||||
const flag: AnkFlag = "NewTx";
|
||||
const newTxMsg: NewTxMessage = {
|
||||
'transaction': notificationInfo.transaction,
|
||||
'tweak_data': null,
|
||||
'error': null,
|
||||
}
|
||||
connection.sendMessage(flag, JSON.stringify(newTxMsg));
|
||||
await services.updateMessages(notificationInfo.new_network_msg);
|
||||
return;
|
||||
}
|
||||
|
||||
public async notify_address_for_message(sp_address: string, message: CipherMessage): Promise<createTransactionReturn> {
|
||||
const services = await Services.getInstance();
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
if (!connection) {
|
||||
throw 'No available connection';
|
||||
}
|
||||
|
||||
try {
|
||||
const feeRate = 1;
|
||||
let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate);
|
||||
const flag: AnkFlag = "NewTx";
|
||||
const newTxMsg: NewTxMessage = {
|
||||
'transaction': notificationInfo.transaction,
|
||||
'tweak_data': null,
|
||||
'error': null,
|
||||
}
|
||||
connection.sendMessage(flag, JSON.stringify(newTxMsg));
|
||||
console.info('Successfully sent notification transaction');
|
||||
return notificationInfo;
|
||||
} catch (error) {
|
||||
throw 'Failed to create notification transaction:", error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Services from "./services";
|
||||
// import * as mempool from "./mempool";
|
||||
import { AnkFlag, AnkNetworkMsg, CachedMessage } from "../dist/pkg/sdk_client";
|
||||
|
||||
class WebSocketClient {
|
||||
private ws: WebSocket;
|
||||
@ -22,31 +22,58 @@ class WebSocketClient {
|
||||
// Listen for messages
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
const msgData = event.data;
|
||||
console.log(msgData);
|
||||
|
||||
(async () => {
|
||||
if (msgData instanceof Blob) {
|
||||
// bitcoin network msg is just bytes
|
||||
let res = await services.parseBitcoinMessage(msgData);
|
||||
if (res) {
|
||||
let ours = await services.checkTransaction(res);
|
||||
if (ours) {
|
||||
console.log("Found our utxo in "+res);
|
||||
} else {
|
||||
console.log("No utxo found in tx "+res);
|
||||
}
|
||||
} else {
|
||||
console.error("Faile to parse a bitcoin network msg");
|
||||
}
|
||||
} else if (typeof(msgData) === 'string') {
|
||||
// json strings are 4nk message
|
||||
if (typeof(msgData) === 'string') {
|
||||
console.log("Received text message: "+msgData);
|
||||
let res = await services.parse4nkMessage(msgData);
|
||||
if (res) {
|
||||
try {
|
||||
const feeRate = 1;
|
||||
// By parsing the message, we can link it with existing cached message and return the updated version of the message
|
||||
let res: CachedMessage = await services.parseNetworkMessage(msgData, feeRate);
|
||||
console.debug(res);
|
||||
if (res.status === 'FaucetComplete') {
|
||||
// we received a faucet tx, there's nothing else to do
|
||||
window.alert(`New faucet output\n${res.commited_in}`);
|
||||
await services.updateMessages(res);
|
||||
await services.updateOwnedOutputsForUser();
|
||||
} else if (res.status === 'TxWaitingCipher') {
|
||||
// we received a tx but we don't have the cipher
|
||||
console.debug(`received notification in output ${res.commited_in}, waiting for cipher message`);
|
||||
await services.updateMessages(res);
|
||||
await services.updateOwnedOutputsForUser();
|
||||
} else if (res.status === 'CipherWaitingTx') {
|
||||
// we received a cipher but we don't have the key
|
||||
console.debug(`received a cipher`);
|
||||
await services.updateMessages(res);
|
||||
} else if (res.status === 'SentWaitingConfirmation') {
|
||||
// We are sender and we're waiting for the challenge that will confirm recipient got the transaction and the message
|
||||
await services.updateMessages(res);
|
||||
await services.updateOwnedOutputsForUser();
|
||||
} else if (res.status === 'MustSpendConfirmation') {
|
||||
// we received a challenge for a notification we made
|
||||
// that means we can stop rebroadcasting the tx and we must spend the challenge to confirm
|
||||
window.alert(`Spending ${res.confirmed_by} to prove our identity`);
|
||||
console.debug(`sending confirm message to ${res.recipient}`);
|
||||
await services.updateMessages(res);
|
||||
await services.answer_confirmation_message(res);
|
||||
} else if (res.status === 'ReceivedMustConfirm') {
|
||||
// we found a notification and decrypted the cipher
|
||||
window.alert(`Received message from ${res.sender}\n${res.plaintext}`);
|
||||
// we must spend the commited_in output to sender
|
||||
await services.updateMessages(res);
|
||||
await services.confirm_sender_address(res);
|
||||
} else if (res.status === 'Complete') {
|
||||
window.alert(`Received confirmation that ${res.sender} is the author of message ${res.plaintext}`)
|
||||
await services.updateMessages(res);
|
||||
await services.updateOwnedOutputsForUser();
|
||||
} else {
|
||||
console.debug('Received an unimplemented valid message');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Received an invalid message:', error);
|
||||
}
|
||||
} else {
|
||||
console.error("Received an unknown message");
|
||||
console.error('Received a non-string message');
|
||||
}
|
||||
})();
|
||||
});
|
||||
@ -63,11 +90,16 @@ class WebSocketClient {
|
||||
}
|
||||
|
||||
// Method to send messages
|
||||
public sendMessage(message: string): void {
|
||||
public sendMessage(flag: AnkFlag, message: string): void {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(message);
|
||||
const networkMessage: AnkNetworkMsg = {
|
||||
'flag': flag,
|
||||
'content': message
|
||||
}
|
||||
// console.debug("Sending message:", JSON.stringify(networkMessage));
|
||||
this.ws.send(JSON.stringify(networkMessage));
|
||||
} else {
|
||||
console.error('WebSocket is not open. ReadyState:', this.ws.readyState);
|
||||
console.warn('WebSocket is not open. ReadyState:', this.ws.readyState);
|
||||
this.messageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user