Merge branch 'demo' into dev

This commit is contained in:
Sosthene 2024-05-28 11:56:41 +02:00
commit fcbc848be6
15 changed files with 1558 additions and 1204 deletions

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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>);

View File

@ -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>;
}

View File

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

View File

@ -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>
";

View File

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

View File

@ -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!(

View File

@ -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"
},

View File

@ -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');

View File

@ -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();

View File

@ -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';
}
}
}

View File

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