WIP
This commit is contained in:
parent
26bf0c0865
commit
fcdfeeaecc
@ -8,6 +8,7 @@ name = "sdk_client"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# sp_client= { path = "../../../sp-client" }
|
||||||
sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" }
|
sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
@ -18,7 +19,8 @@ wasm-logger = "0.2.0"
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
log = "0.4.6"
|
log = "0.4.6"
|
||||||
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
|
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
|
||||||
sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" }
|
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" }
|
shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" }
|
||||||
img-parts = "0.3.0"
|
img-parts = "0.3.0"
|
||||||
|
|
||||||
|
@ -2,20 +2,22 @@ use std::any::Any;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
use std::sync::{Mutex, OnceLock, PoisonError};
|
use std::sync::{Mutex, OnceLock, PoisonError};
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use rand::Rng;
|
use rand::{Fill, Rng};
|
||||||
|
|
||||||
use anyhow::Error as AnyhowError;
|
use anyhow::Error as AnyhowError;
|
||||||
use sdk_common::crypto::AnkSharedSecret;
|
use sdk_common::crypto::{
|
||||||
|
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose,
|
||||||
|
};
|
||||||
use serde_json::Error as SerdeJsonError;
|
use serde_json::Error as SerdeJsonError;
|
||||||
use shamir::SecretData;
|
use shamir::SecretData;
|
||||||
use sp_client::bitcoin::consensus::{deserialize, serialize};
|
use sp_client::bitcoin::consensus::{deserialize, serialize};
|
||||||
use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError};
|
use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError};
|
||||||
use sp_client::bitcoin::secp256k1::ecdh::SharedSecret;
|
|
||||||
use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
|
use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
|
||||||
use sp_client::bitcoin::{OutPoint, Transaction, Txid};
|
use sp_client::bitcoin::{Amount, OutPoint, Transaction, Txid};
|
||||||
use sp_client::silentpayments::Error as SpError;
|
use sp_client::silentpayments::Error as SpError;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -25,15 +27,15 @@ use wasm_bindgen::convert::FromWasmAbi;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage};
|
use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage};
|
||||||
|
use sdk_common::silentpayments::{
|
||||||
|
check_transaction, create_transaction_for_address_with_shared_secret,
|
||||||
|
};
|
||||||
|
|
||||||
use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient};
|
use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient};
|
||||||
use sp_client::spclient::{SpWallet, SpendKey};
|
use sp_client::spclient::{SpWallet, SpendKey};
|
||||||
|
|
||||||
use crate::images;
|
|
||||||
use crate::silentpayments::{
|
|
||||||
check_transaction, create_transaction_for_address_with_shared_secret, ScannedTransaction,
|
|
||||||
};
|
|
||||||
use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER};
|
use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER};
|
||||||
|
use crate::{images, lock_scanned_transactions, Txid2Secrets};
|
||||||
|
|
||||||
use crate::process::Process;
|
use crate::process::Process;
|
||||||
|
|
||||||
@ -94,6 +96,14 @@ impl From<sp_client::bitcoin::consensus::encode::Error> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<FromUtf8Error> for ApiError {
|
||||||
|
fn from(value: FromUtf8Error) -> Self {
|
||||||
|
ApiError {
|
||||||
|
message: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<JsValue> for ApiError {
|
impl Into<JsValue> for ApiError {
|
||||||
fn into(self) -> JsValue {
|
fn into(self) -> JsValue {
|
||||||
JsValue::from_str(&self.message)
|
JsValue::from_str(&self.message)
|
||||||
@ -143,7 +153,10 @@ pub fn generate_sp_wallet(
|
|||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn get_recover_address() -> ApiResult<String> {
|
pub fn get_recover_address() -> ApiResult<String> {
|
||||||
if let Ok(my_wallets) = lock_connected_user() {
|
if let Ok(my_wallets) = lock_connected_user() {
|
||||||
Ok(my_wallets.try_get_recover()?.get_client().get_receiving_address())
|
Ok(my_wallets
|
||||||
|
.try_get_recover()?
|
||||||
|
.get_client()
|
||||||
|
.get_receiving_address())
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError {
|
Err(ApiError {
|
||||||
message: "Unknown user pre_id".to_owned(),
|
message: "Unknown user pre_id".to_owned(),
|
||||||
@ -154,7 +167,10 @@ pub fn get_recover_address() -> ApiResult<String> {
|
|||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn get_main_address() -> ApiResult<String> {
|
pub fn get_main_address() -> ApiResult<String> {
|
||||||
if let Ok(my_wallets) = lock_connected_user() {
|
if let Ok(my_wallets) = lock_connected_user() {
|
||||||
Ok(my_wallets.try_get_main()?.get_client().get_receiving_address())
|
Ok(my_wallets
|
||||||
|
.try_get_main()?
|
||||||
|
.get_client()
|
||||||
|
.get_receiving_address())
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError {
|
Err(ApiError {
|
||||||
message: "Unknown user pre_id".to_owned(),
|
message: "Unknown user pre_id".to_owned(),
|
||||||
@ -322,9 +338,28 @@ pub fn check_transaction_for_silent_payments(
|
|||||||
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
|
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
|
||||||
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
|
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
|
||||||
|
|
||||||
let txid = check_transaction(&tx, blockheight, tweak_data)?;
|
let mut connected_user = lock_connected_user()?;
|
||||||
|
if let Ok(recover) = connected_user.try_get_mut_recover() {
|
||||||
|
if let Ok(txid) = check_transaction(&tx, recover, blockheight, tweak_data) {
|
||||||
|
return Ok(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(txid)
|
if let Ok(main) = connected_user.try_get_mut_main() {
|
||||||
|
if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) {
|
||||||
|
return Ok(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(revoke) = connected_user.try_get_mut_revoke() {
|
||||||
|
if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) {
|
||||||
|
return Ok(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ApiError {
|
||||||
|
message: "No output found".to_owned(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Tsify, Serialize, Deserialize)]
|
#[derive(Tsify, Serialize, Deserialize)]
|
||||||
@ -341,14 +376,16 @@ pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> {
|
|||||||
match ank_msg.flag {
|
match ank_msg.flag {
|
||||||
AnkFlag::NewTx => {
|
AnkFlag::NewTx => {
|
||||||
let tx_message = serde_json::from_str::<NewTxMessage>(&ank_msg.content)?;
|
let tx_message = serde_json::from_str::<NewTxMessage>(&ank_msg.content)?;
|
||||||
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_message.transaction)?)?;
|
|
||||||
if tx_message.tweak_data.is_none() {
|
if tx_message.tweak_data.is_none() {
|
||||||
return Err(ApiError {
|
return Err(ApiError {
|
||||||
message: "Missing tweak_data".to_owned(),
|
message: "Missing tweak_data".to_owned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let partial_tweak = PublicKey::from_str(&tx_message.tweak_data.unwrap())?;
|
let txid = check_transaction_for_silent_payments(
|
||||||
let txid = check_transaction(&tx, 0, partial_tweak)?;
|
tx_message.transaction,
|
||||||
|
0,
|
||||||
|
tx_message.tweak_data.unwrap(),
|
||||||
|
)?;
|
||||||
return Ok(parseNetworkMsgReturn {
|
return Ok(parseNetworkMsgReturn {
|
||||||
topic: AnkFlag::NewTx.as_str().to_owned(),
|
topic: AnkFlag::NewTx.as_str().to_owned(),
|
||||||
message: txid,
|
message: txid,
|
||||||
@ -376,7 +413,27 @@ pub fn get_outpoints_for_user() -> ApiResult<outputs_list> {
|
|||||||
if connected_user.is_not_empty() {
|
if connected_user.is_not_empty() {
|
||||||
Ok(outputs_list(connected_user.get_all_outputs()))
|
Ok(outputs_list(connected_user.get_all_outputs()))
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError { message: "No user logged in".to_owned() })
|
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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,28 +460,117 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult<bool> {
|
|||||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub struct createNotificationTransactionReturn {
|
pub struct createNotificationTransactionReturn {
|
||||||
|
pub txid: String,
|
||||||
pub transaction: String,
|
pub transaction: String,
|
||||||
pub transaction2secret: ScannedTransaction,
|
pub address2secret: HashMap<String, AnkSharedSecret>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn create_notification_transaction(
|
pub fn create_notification_transaction(
|
||||||
recipient: String,
|
recipient: String,
|
||||||
message: String,
|
message: String,
|
||||||
|
fee_rate: u32,
|
||||||
) -> ApiResult<createNotificationTransactionReturn> {
|
) -> ApiResult<createNotificationTransactionReturn> {
|
||||||
let sp_address: SilentPaymentAddress = recipient.try_into()?;
|
let sp_address: SilentPaymentAddress = recipient.try_into()?;
|
||||||
|
|
||||||
let (transaction, shared_secret) =
|
let connected_user = lock_connected_user()?;
|
||||||
create_transaction_for_address_with_shared_secret(sp_address, message)?;
|
|
||||||
|
|
||||||
|
let sp_wallet: &SpWallet;
|
||||||
|
if sp_address.is_testnet() {
|
||||||
|
sp_wallet = connected_user.try_get_recover()?;
|
||||||
|
} else {
|
||||||
|
sp_wallet = connected_user.try_get_main()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (transaction, shared_secret) = create_transaction_for_address_with_shared_secret(
|
||||||
|
sp_address,
|
||||||
|
sp_wallet,
|
||||||
|
message,
|
||||||
|
Amount::from_sat(fee_rate.into()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut address2secret: Vec<(String, AnkSharedSecret)> = vec![];
|
||||||
|
address2secret.push((sp_address.into(), shared_secret));
|
||||||
|
|
||||||
// update our cache
|
// update our cache
|
||||||
|
lock_scanned_transactions()?
|
||||||
let mut transaction2secret = ScannedTransaction::new();
|
.insert(transaction.txid(), address2secret.clone());
|
||||||
transaction2secret.get_mut().insert(transaction.txid(), vec![shared_secret]);
|
|
||||||
|
|
||||||
Ok(createNotificationTransactionReturn {
|
Ok(createNotificationTransactionReturn {
|
||||||
|
txid: transaction.txid().to_string(),
|
||||||
transaction: serialize(&transaction).to_lower_hex_string(),
|
transaction: serialize(&transaction).to_lower_hex_string(),
|
||||||
transaction2secret,
|
address2secret: address2secret.into_iter().collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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(String::from_utf8(cipher)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
#![allow(warnings)]
|
#![allow(warnings)]
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use sdk_common::crypto::AnkSharedSecret;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sp_client::bitcoin::Txid;
|
||||||
|
use sp_client::silentpayments::sending::SilentPaymentAddress;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::{Mutex, MutexGuard};
|
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||||
|
use tsify::Tsify;
|
||||||
|
|
||||||
mod Prd_list;
|
mod Prd_list;
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod images;
|
mod images;
|
||||||
mod peers;
|
mod peers;
|
||||||
mod process;
|
mod process;
|
||||||
mod silentpayments;
|
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
|
pub type Txid2Secrets = HashMap<Txid, Vec<(String, AnkSharedSecret)>>;
|
||||||
|
|
||||||
|
pub static TRANSACTIONCACHE: OnceLock<Mutex<Txid2Secrets>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn lock_scanned_transactions() -> Result<MutexGuard<'static, Txid2Secrets>, Error> {
|
||||||
|
TRANSACTIONCACHE
|
||||||
|
.get_or_init(|| Mutex::new(Txid2Secrets::new()))
|
||||||
|
.lock_anyhow()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) trait MutexExt<T> {
|
pub(crate) trait MutexExt<T> {
|
||||||
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
|
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
|
||||||
}
|
}
|
||||||
|
@ -1,217 +0,0 @@
|
|||||||
// This file should move to common
|
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::iter::Once;
|
|
||||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use rand::Rng;
|
|
||||||
use sdk_common::crypto::{Aes256Encryption, Aes256Gcm, AeadCore, Purpose};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE;
|
|
||||||
use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
|
|
||||||
use sp_client::bitcoin::{block, Amount, OutPoint, Txid};
|
|
||||||
use sp_client::silentpayments::sending::SilentPaymentAddress;
|
|
||||||
use sp_client::silentpayments::utils::receiving::calculate_shared_secret;
|
|
||||||
use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient, SpWallet};
|
|
||||||
use sp_client::silentpayments::bitcoin_hashes::{sha256, Hash, HashEngine};
|
|
||||||
use sp_client::{
|
|
||||||
bitcoin::{
|
|
||||||
secp256k1::{PublicKey, Scalar, XOnlyPublicKey},
|
|
||||||
Transaction,
|
|
||||||
},
|
|
||||||
silentpayments::receiving::Label,
|
|
||||||
};
|
|
||||||
use tsify::Tsify;
|
|
||||||
|
|
||||||
use crate::user::{lock_connected_user, CONNECTED_USER};
|
|
||||||
use crate::MutexExt;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
|
||||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
||||||
pub struct ScannedTransaction(HashMap<Txid, Vec<SharedSecret>>);
|
|
||||||
|
|
||||||
impl ScannedTransaction {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(HashMap::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut(&mut self) -> &mut HashMap<Txid, Vec<SharedSecret>> {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static TRANSACTIONCACHE: OnceLock<Mutex<ScannedTransaction>> = OnceLock::new();
|
|
||||||
|
|
||||||
pub fn lock_scanned_transactions() -> Result<MutexGuard<'static, ScannedTransaction>> {
|
|
||||||
TRANSACTIONCACHE
|
|
||||||
.get_or_init(|| Mutex::new(ScannedTransaction::new()))
|
|
||||||
.lock_anyhow()
|
|
||||||
}
|
|
||||||
|
|
||||||
type FoundOutputs = HashMap<Option<Label>, HashMap<XOnlyPublicKey, Scalar>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SharedPoint([u8; 64]);
|
|
||||||
|
|
||||||
impl SharedPoint {
|
|
||||||
pub fn as_inner(&self) -> &[u8; 64] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
|
||||||
#[tsify(from_wasm_abi, into_wasm_abi)]
|
|
||||||
pub struct SharedSecret {
|
|
||||||
secret: [u8; 32],
|
|
||||||
shared_with: Option<String>, // SilentPaymentAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SharedSecret {
|
|
||||||
pub fn new(secret: [u8;32], shared_with: Option<String>) -> Result<Self> {
|
|
||||||
if let Some(ref shared) = shared_with {
|
|
||||||
if let Ok(_) = SilentPaymentAddress::try_from(shared.as_str()) {
|
|
||||||
Ok(Self {
|
|
||||||
secret,
|
|
||||||
shared_with
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(Error::msg("Invalid silent payment address"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(Self {
|
|
||||||
secret,
|
|
||||||
shared_with: None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_transaction_for_address_with_shared_secret(
|
|
||||||
sp_address: SilentPaymentAddress,
|
|
||||||
message: String,
|
|
||||||
) -> Result<(Transaction, SharedSecret)> {
|
|
||||||
let connected_user = lock_connected_user()?;
|
|
||||||
|
|
||||||
let sp_wallet = if sp_address.is_testnet() {
|
|
||||||
connected_user.try_get_recover()?
|
|
||||||
} else {
|
|
||||||
if let Ok(main) = connected_user.try_get_main() {
|
|
||||||
main
|
|
||||||
} else {
|
|
||||||
return Err(Error::msg("Can't spend on mainnet"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let available_outpoints = sp_wallet.get_outputs().to_spendable_list();
|
|
||||||
|
|
||||||
// Here we need to add more heuristics about which outpoint we spend
|
|
||||||
// For now let's keep it simple
|
|
||||||
|
|
||||||
let mut inputs: HashMap<OutPoint, OwnedOutput> = HashMap::new();
|
|
||||||
|
|
||||||
let total_available =
|
|
||||||
available_outpoints
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(Amount::from_sat(0), |acc, (outpoint, output)| {
|
|
||||||
let new_total = acc + output.amount;
|
|
||||||
inputs.insert(outpoint, output);
|
|
||||||
if new_total > Amount::from_sat(1000) {
|
|
||||||
Err(new_total)
|
|
||||||
} else {
|
|
||||||
Ok(new_total)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match total_available {
|
|
||||||
Err(total) => log::debug!("Spending {} outputs totaling {} sats", inputs.len(), total),
|
|
||||||
Ok(_) => return Err(Error::msg("Not enought fund available")),
|
|
||||||
}
|
|
||||||
|
|
||||||
let recipient = Recipient {
|
|
||||||
address: sp_address.into(),
|
|
||||||
amount: Amount::from_sat(1000),
|
|
||||||
nb_outputs: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_psbt = sp_wallet
|
|
||||||
.get_client()
|
|
||||||
.create_new_psbt(inputs, vec![recipient], Some(message.as_bytes()))?;
|
|
||||||
log::debug!("Created psbt: {}", new_psbt);
|
|
||||||
SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?;
|
|
||||||
|
|
||||||
let partial_secret = sp_wallet
|
|
||||||
.get_client()
|
|
||||||
.get_partial_secret_from_psbt(&new_psbt)?;
|
|
||||||
|
|
||||||
// This wouldn't work with many recipients in the same transaction
|
|
||||||
// each address (or more precisely each scan public key) would have its own point
|
|
||||||
let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret);
|
|
||||||
|
|
||||||
// The shared_point *must* be hashed to produce a proper ecdh secret
|
|
||||||
let mut eng = sha256::HashEngine::default();
|
|
||||||
eng.write_all(&shared_point);
|
|
||||||
let shared_secret = sha256::Hash::from_engine(eng);
|
|
||||||
|
|
||||||
// encrypt the message with the new shared_secret
|
|
||||||
let message_encryption = Aes256Encryption::import_key(
|
|
||||||
Purpose::Arbitrary,
|
|
||||||
message.into_bytes(),
|
|
||||||
shared_secret.to_byte_array(),
|
|
||||||
Aes256Gcm::generate_nonce(&mut rand::thread_rng()).into(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let cipher = message_encryption.encrypt_with_aes_key()?;
|
|
||||||
|
|
||||||
sp_client::spclient::SpClient::replace_op_return_with(&mut new_psbt, &cipher)?;
|
|
||||||
|
|
||||||
sp_wallet
|
|
||||||
.get_client()
|
|
||||||
.fill_sp_outputs(&mut new_psbt, partial_secret)?;
|
|
||||||
log::debug!("Definitive psbt: {}", new_psbt);
|
|
||||||
let mut aux_rand = [0u8; 32];
|
|
||||||
rand::thread_rng().fill(&mut aux_rand);
|
|
||||||
let mut signed = sp_wallet.get_client().sign_psbt(new_psbt, &aux_rand)?;
|
|
||||||
log::debug!("signed psbt: {}", signed);
|
|
||||||
SpClient::finalize_psbt(&mut signed)?;
|
|
||||||
|
|
||||||
let final_tx = signed.extract_tx()?;
|
|
||||||
|
|
||||||
Ok((final_tx, SharedSecret {
|
|
||||||
secret: shared_secret.to_byte_array(),
|
|
||||||
shared_with: Some(sp_address.into())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This need to go
|
|
||||||
pub fn check_transaction(
|
|
||||||
tx: &Transaction,
|
|
||||||
blockheight: u32,
|
|
||||||
tweak_data: PublicKey,
|
|
||||||
) -> Result<String> {
|
|
||||||
// first check that we haven't scanned this transaction yet
|
|
||||||
if let Some((txid, _)) = lock_scanned_transactions()?.0.get_key_value(&tx.txid()) {
|
|
||||||
let err_msg = format!("Already scanned tx {}", txid);
|
|
||||||
return Err(Error::msg(err_msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut connected_user = lock_connected_user()?;
|
|
||||||
|
|
||||||
let txid = tx.txid().to_string();
|
|
||||||
if connected_user.try_get_mut_recover()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 {
|
|
||||||
return Ok(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if connected_user.try_get_mut_main()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 {
|
|
||||||
return Ok(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if connected_user.try_get_mut_revoke()?.update_wallet_with_transaction(tx, blockheight, tweak_data)? > 0 {
|
|
||||||
return Ok(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(Error::msg("No new outputs found"));
|
|
||||||
}
|
|
@ -52,7 +52,11 @@ pub struct UserWallets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserWallets {
|
impl UserWallets {
|
||||||
pub fn new(main: Option<SpWallet>, recover: Option<SpWallet>, revoke: Option<SpWallet>) -> Self {
|
pub fn new(
|
||||||
|
main: Option<SpWallet>,
|
||||||
|
recover: Option<SpWallet>,
|
||||||
|
revoke: Option<SpWallet>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
main,
|
main,
|
||||||
recover,
|
recover,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Services from './services';
|
import Services from './services';
|
||||||
import { WebSocketClient } from './websockets';
|
import { WebSocketClient } from './websockets';
|
||||||
|
|
||||||
const wsurl = "ws://192.168.1.44:8090";
|
const wsurl = "ws://localhost:8090";
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const services = await Services.getInstance();
|
const services = await Services.getInstance();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage } from '../dist/pkg/sdk_client';
|
import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret } from '../dist/pkg/sdk_client';
|
||||||
import IndexedDB from './database'
|
import IndexedDB from './database'
|
||||||
import { WebSocketClient } from './websockets';
|
import { WebSocketClient } from './websockets';
|
||||||
|
|
||||||
@ -74,6 +74,26 @@ class Services {
|
|||||||
public async sendMessage(event: Event): Promise<void> {
|
public async sendMessage(event: Event): Promise<void> {
|
||||||
event.preventDefault();
|
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(this.sp_address!);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to obtain faucet token:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const spAddressElement = document.getElementById("sp_address") as HTMLInputElement;
|
const spAddressElement = document.getElementById("sp_address") as HTMLInputElement;
|
||||||
const messageElement = document.getElementById("message") as HTMLInputElement;
|
const messageElement = document.getElementById("message") as HTMLInputElement;
|
||||||
|
|
||||||
@ -84,11 +104,18 @@ class Services {
|
|||||||
|
|
||||||
const recipientSpAddress = spAddressElement.value;
|
const recipientSpAddress = spAddressElement.value;
|
||||||
const message = messageElement.value;
|
const message = messageElement.value;
|
||||||
const services = await Services.getInstance();
|
|
||||||
|
|
||||||
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message);
|
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message);
|
||||||
if (notificationInfo) {
|
if (notificationInfo) {
|
||||||
notificationInfo.transaction
|
console.info('Successfully sent notification transaction');
|
||||||
|
// Save the secret to db
|
||||||
|
// encrypt the message(s)
|
||||||
|
services.encryptData(message, notificationInfo.address2secret);
|
||||||
|
// encrypt the key
|
||||||
|
|
||||||
|
// add peers list
|
||||||
|
// add processes list
|
||||||
|
// send message (transaction in envelope)
|
||||||
}
|
}
|
||||||
console.log(notificationInfo);
|
console.log(notificationInfo);
|
||||||
}
|
}
|
||||||
@ -151,7 +178,7 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.sp_address = services.sdkClient.get_receiving_address(user.pre_id);
|
this.sp_address = services.sdkClient.get_recover_address();
|
||||||
if (this.sp_address) {
|
if (this.sp_address) {
|
||||||
console.info('Using sp_address:', this.sp_address);
|
console.info('Using sp_address:', this.sp_address);
|
||||||
await services.obtainTokenWithFaucet(this.sp_address);
|
await services.obtainTokenWithFaucet(this.sp_address);
|
||||||
@ -196,12 +223,14 @@ class Services {
|
|||||||
const user = await services.getUserInfo();
|
const user = await services.getUserInfo();
|
||||||
if (user) {
|
if (user) {
|
||||||
services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs);
|
services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs);
|
||||||
this.sp_address = services.sdkClient.get_receiving_address(user?.pre_id);
|
this.sp_address = services.sdkClient.get_recover_address();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info(this.sp_address);
|
||||||
|
|
||||||
// TODO: check blocks since last_scan and update outputs
|
// TODO: check blocks since last_scan and update outputs
|
||||||
|
|
||||||
await services.displaySendMessage();
|
await services.displaySendMessage();
|
||||||
@ -341,6 +370,7 @@ class Services {
|
|||||||
let user = await services.getUserInfo();
|
let user = await services.getUserInfo();
|
||||||
if (user) {
|
if (user) {
|
||||||
user.outputs = latest_outputs;
|
user.outputs = latest_outputs;
|
||||||
|
// console.warn(user);
|
||||||
await services.updateUser(user);
|
await services.updateUser(user);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -772,7 +802,8 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(user, sp_address, message);
|
const feeRate = 1000;
|
||||||
|
let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate);
|
||||||
const flag: AnkFlag = "NewTx";
|
const flag: AnkFlag = "NewTx";
|
||||||
const newTxMsg: NewTxMessage = {
|
const newTxMsg: NewTxMessage = {
|
||||||
'transaction': notificationInfo.transaction,
|
'transaction': notificationInfo.transaction,
|
||||||
@ -785,6 +816,37 @@ class Services {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async encryptData(data: string, sharedSecret: Record<string, AnkSharedSecret>): Promise<encryptWithNewKeyResult> {
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
let msg_cipher: encryptWithNewKeyResult;
|
||||||
|
try {
|
||||||
|
msg_cipher = services.sdkClient.encrypt_with_new_key(data);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = new Map<string, string>();
|
||||||
|
for (const [recipient, secret] of Object.entries(sharedSecret)) {
|
||||||
|
try {
|
||||||
|
const key = secret.secret;
|
||||||
|
const encryptedKey = await services.sdkClient.encrypt_with_key(msg_cipher.key, key);
|
||||||
|
res.set(recipient, encryptedKey);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async decryptData(cipher: string, key: string): Promise<string> {
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
try {
|
||||||
|
let res = services.sdkClient.try_decrypt_with_key(cipher, key);
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Services;
|
export default Services;
|
||||||
|
@ -22,7 +22,6 @@ class WebSocketClient {
|
|||||||
// Listen for messages
|
// Listen for messages
|
||||||
this.ws.addEventListener('message', (event) => {
|
this.ws.addEventListener('message', (event) => {
|
||||||
const msgData = event.data;
|
const msgData = event.data;
|
||||||
console.log(msgData);
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (typeof(msgData) === 'string') {
|
if (typeof(msgData) === 'string') {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user