Merge branch 'dev' into 'main'
sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" } See merge request 4nk/sdk_client!1
This commit is contained in:
commit
9d79891ce6
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
target/
|
||||
pkg/
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode
|
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/sp_client"
|
||||
]
|
27
crates/sp_client/Cargo.toml
Normal file
27
crates/sp_client/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "sdk_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "sdk_client"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
wasm-bindgen = "0.2.91"
|
||||
getrandom = { version="0.2.12", features = ["js"] }
|
||||
wasm-logger = "0.2.0"
|
||||
rand = "0.8.5"
|
||||
log = "0.4.6"
|
||||
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
|
||||
# sdk_common = { path = "../../../sdk_common" }
|
||||
# sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" }
|
||||
sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" }
|
||||
shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" }
|
||||
img-parts = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
1033
crates/sp_client/src/api.rs
Normal file
1033
crates/sp_client/src/api.rs
Normal file
File diff suppressed because it is too large
Load Diff
38
crates/sp_client/src/images.rs
Normal file
38
crates/sp_client/src/images.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use anyhow::{Error, Result};
|
||||
use img_parts::{jpeg::Jpeg, Bytes, ImageEXIF};
|
||||
use sdk_common::sp_client::bitcoin::secp256k1::SecretKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackUpImage(Vec<u8>);
|
||||
|
||||
impl BackUpImage {
|
||||
pub fn new_recover(image: Vec<u8>, data: &[u8]) -> Result<Self> {
|
||||
// TODO: sanity check on data
|
||||
let img = write_exif(image, data)?;
|
||||
Ok(Self(img))
|
||||
}
|
||||
|
||||
pub fn new_revoke(image: Vec<u8>, data: &[u8]) -> Result<Self> {
|
||||
// TODO: sanity check on data
|
||||
let img = write_exif(image, data)?;
|
||||
Ok(Self(img))
|
||||
}
|
||||
|
||||
pub fn to_inner(&self) -> Vec<u8> {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn as_inner(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn write_exif(image: Vec<u8>, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut jpeg = Jpeg::from_bytes(Bytes::from(image))?;
|
||||
let data_bytes = Bytes::from(data.to_owned());
|
||||
jpeg.set_exif(Some(data_bytes));
|
||||
let output_image_bytes = jpeg.encoder().bytes();
|
||||
let output_image = output_image_bytes.to_vec();
|
||||
Ok(output_image)
|
||||
}
|
34
crates/sp_client/src/lib.rs
Normal file
34
crates/sp_client/src/lib.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![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, OnceLock};
|
||||
use tsify::Tsify;
|
||||
|
||||
pub mod api;
|
||||
mod images;
|
||||
mod peers;
|
||||
mod process;
|
||||
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>;
|
||||
}
|
||||
|
||||
impl<T: Debug> MutexExt<T> for Mutex<T> {
|
||||
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error> {
|
||||
self.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock: {}", e)))
|
||||
}
|
||||
}
|
9
crates/sp_client/src/peers.rs
Normal file
9
crates/sp_client/src/peers.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Peer {
|
||||
pub addr: SocketAddr,
|
||||
pub processes: Vec<String>,
|
||||
}
|
405
crates/sp_client/src/process.rs
Normal file
405
crates/sp_client/src/process.rs
Normal file
@ -0,0 +1,405 @@
|
||||
use std::fmt::DebugStruct;
|
||||
|
||||
use sdk_common::sp_client::silentpayments::sending::SilentPaymentAddress;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use tsify::Tsify;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub const HTML_KOTPART: &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='#'>
|
||||
<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>
|
||||
";
|
||||
|
||||
pub const HTML_STORAGE: &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='#'>
|
||||
<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>
|
||||
";
|
||||
|
||||
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/>
|
||||
<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 CSS: &str = "
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
.card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* flex-wrap: wrap; */
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
margin: 10px 0;
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
select {
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.side-by-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.side-by-side>* {
|
||||
display: inline-block;
|
||||
}
|
||||
button.recover {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.recover:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
a.btn {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #78a6de;
|
||||
}
|
||||
.bg-secondary {
|
||||
background-color: #2b81ed;
|
||||
}
|
||||
.bg-primary {
|
||||
background-color: #1A61ED;
|
||||
}
|
||||
.bg-primary:hover {
|
||||
background-color: #457be8;
|
||||
}
|
||||
.card-revoke {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-revoke a {
|
||||
max-width: 50px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-revoke button {
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #78a6de;
|
||||
}
|
||||
.card-revoke svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
fill: #333;
|
||||
}
|
||||
.image-label {
|
||||
display: block;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.image-container {
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-container img {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
}
|
||||
.passwordalert {
|
||||
color: #FF0000;
|
||||
}
|
||||
";
|
||||
|
||||
pub const CSSUPDATE: &str = "
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1fr, 2fr);
|
||||
gap: 10px;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #1a61ed;
|
||||
}
|
||||
|
||||
.bg-primary:hover {
|
||||
background-color: #457be8;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: #2b81ed;
|
||||
}
|
||||
|
||||
.bg-secondary:hover {
|
||||
background-color: #5f9bff;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
grid-column: span 2;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.div-text-area {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.side-by-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.circle-btn {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#fileInput {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding-left: 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
";
|
||||
|
||||
pub const JSUPDATE: &str = "
|
||||
var addSpAddressBtn = document.getElementById('add-sp-address-btn');
|
||||
var removeSpAddressBtn = document.querySelectorAll('.minus-sp-address-btn');
|
||||
|
||||
addSpAddressBtn.addEventListener('click', function (event) {
|
||||
addDynamicField(this);
|
||||
});
|
||||
|
||||
function addDynamicField(element) {
|
||||
var addSpAddressBlock = document.getElementById('sp-address-block');
|
||||
var spAddress = addSpAddressBlock.querySelector('#sp-address').value;
|
||||
addSpAddressBlock.querySelector('#sp-address').value = '';
|
||||
spAddress = spAddress.trim();
|
||||
if (spAddress != '') {
|
||||
var sideBySideDiv = document.createElement('div');
|
||||
sideBySideDiv.className = 'side-by-side';
|
||||
|
||||
var inputElement = document.createElement('input');
|
||||
inputElement.type = 'text';
|
||||
inputElement.name = 'spAddresses[]';
|
||||
inputElement.setAttribute('form', 'no-form');
|
||||
inputElement.value = spAddress;
|
||||
inputElement.disabled = true;
|
||||
|
||||
var buttonElement = document.createElement('button');
|
||||
buttonElement.type = 'button';
|
||||
buttonElement.className =
|
||||
'circle-btn bg-secondary minus-sp-address-btn';
|
||||
buttonElement.innerHTML = '-';
|
||||
|
||||
buttonElement.addEventListener('click', function (event) {
|
||||
removeDynamicField(this.parentElement);
|
||||
});
|
||||
|
||||
sideBySideDiv.appendChild(inputElement);
|
||||
sideBySideDiv.appendChild(buttonElement);
|
||||
|
||||
addSpAddressBlock.appendChild(sideBySideDiv);
|
||||
}
|
||||
function removeDynamicField(element) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Tsify)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
pub struct Process {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub members: Vec<String>,
|
||||
pub html: String,
|
||||
pub style: String,
|
||||
pub script: String,
|
||||
// Add conditions : process, member, peer, artefact
|
||||
}
|
544
crates/sp_client/src/user.rs
Normal file
544
crates/sp_client/src/user.rs
Normal file
@ -0,0 +1,544 @@
|
||||
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 tsify::Tsify;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use shamir::SecretData;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
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::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;
|
||||
|
||||
pub static CONNECTED_USER: OnceLock<Mutex<UserWallets>> = OnceLock::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, Default)]
|
||||
pub struct UserWallets {
|
||||
main: Option<SpWallet>,
|
||||
recover: Option<SpWallet>,
|
||||
revoke: Option<SpWallet>,
|
||||
}
|
||||
|
||||
impl UserWallets {
|
||||
pub fn new(
|
||||
main: Option<SpWallet>,
|
||||
recover: Option<SpWallet>,
|
||||
revoke: Option<SpWallet>,
|
||||
) -> Self {
|
||||
Self {
|
||||
main,
|
||||
recover,
|
||||
revoke,
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut res = Vec::<OutputList>::new();
|
||||
if let Some(main) = &self.main {
|
||||
res.push(main.get_outputs().clone());
|
||||
}
|
||||
if let Some(revoke) = &self.revoke {
|
||||
res.push(revoke.get_outputs().clone());
|
||||
}
|
||||
if let Some(recover) = &self.recover {
|
||||
res.push(recover.get_outputs().clone());
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Tsify)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
pub struct User {
|
||||
pub pre_id: PreId,
|
||||
pub processes: Vec<String>,
|
||||
pub peers: Vec<Peer>,
|
||||
recover_data: Vec<u8>,
|
||||
revoke_data: Option<Vec<u8>>,
|
||||
shares: Vec<Vec<u8>>,
|
||||
outputs: Vec<OutputList>,
|
||||
}
|
||||
|
||||
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);
|
||||
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());
|
||||
|
||||
// Take the 2 recover keys
|
||||
// split recover spend key
|
||||
let recover_spend_key = user_wallets
|
||||
.try_get_recover()?
|
||||
.get_client()
|
||||
.try_get_secret_spend_key()?
|
||||
.clone();
|
||||
let (part1_key, part2_key) = recover_spend_key.as_ref().split_at(SECRET_KEY_SIZE / 2);
|
||||
let mut recover_data = Vec::<u8>::with_capacity(180); // 32 * 3 + (12+16)*3
|
||||
|
||||
// generate 3 tokens of 32B entropy
|
||||
let mut entropy_1: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
||||
let mut entropy_2: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
||||
let mut entropy_3: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
||||
|
||||
recover_data.extend_from_slice(&entropy_1);
|
||||
recover_data.extend_from_slice(&entropy_2);
|
||||
recover_data.extend_from_slice(&entropy_3);
|
||||
|
||||
// hash the concatenation
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&user_password.as_bytes());
|
||||
engine.write_all(&entropy_1);
|
||||
let hash1 = sha256::Hash::from_engine(engine);
|
||||
|
||||
// take it as a AES key
|
||||
let part1_encryption = Aes256Encryption::import_key(
|
||||
Purpose::Login,
|
||||
part1_key.to_vec(),
|
||||
hash1.to_byte_array(),
|
||||
Aes256Gcm::generate_nonce(&mut rng).into(),
|
||||
)?;
|
||||
|
||||
// encrypt the part1 of the key
|
||||
let cipher_recover_part1 = part1_encryption.encrypt_with_aes_key()?;
|
||||
|
||||
recover_data.extend_from_slice(&cipher_recover_part1);
|
||||
|
||||
//Pre ID
|
||||
let pre_id: PreId = Self::compute_pre_id(&user_password, &cipher_recover_part1);
|
||||
|
||||
// encrypt the part 2 of the key
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&user_password.as_bytes());
|
||||
engine.write_all(&entropy_2);
|
||||
let hash2 = sha256::Hash::from_engine(engine);
|
||||
|
||||
// take it as a AES key
|
||||
let part2_encryption = Aes256Encryption::import_key(
|
||||
Purpose::Login,
|
||||
part2_key.to_vec(),
|
||||
hash2.to_byte_array(),
|
||||
Aes256Gcm::generate_nonce(&mut rng).into(),
|
||||
)?;
|
||||
|
||||
// encrypt the part2 of the key
|
||||
let cipher_recover_part2 = part2_encryption.encrypt_with_aes_key()?;
|
||||
|
||||
//create shardings
|
||||
let threshold = (MANAGERS_NUMBER as f32 * QUORUM_SHARD).floor();
|
||||
debug_assert!(threshold > 0.0 && threshold <= u8::MAX as f32);
|
||||
let sharding = shamir::SecretData::with_secret(
|
||||
&cipher_recover_part2.to_lower_hex_string(),
|
||||
threshold as u8,
|
||||
);
|
||||
|
||||
let shares: Vec<Vec<u8>> = (1..MANAGERS_NUMBER)
|
||||
.map(|x| {
|
||||
sharding.get_share(x).unwrap() // Let's trust it for now
|
||||
})
|
||||
.collect();
|
||||
|
||||
//scan key:
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&user_password.as_bytes());
|
||||
engine.write_all(&entropy_3);
|
||||
let hash3 = sha256::Hash::from_engine(engine);
|
||||
|
||||
let scan_key_encryption = Aes256Encryption::import_key(
|
||||
Purpose::ThirtyTwoBytes,
|
||||
user_wallets
|
||||
.try_get_recover()?
|
||||
.get_client()
|
||||
.get_scan_key()
|
||||
.secret_bytes()
|
||||
.to_vec(),
|
||||
hash3.to_byte_array(),
|
||||
Aes256Gcm::generate_nonce(&mut rng).into(),
|
||||
)?;
|
||||
|
||||
// encrypt the scan key
|
||||
let cipher_scan_key = scan_key_encryption.encrypt_with_aes_key()?;
|
||||
|
||||
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],
|
||||
peers: vec![],
|
||||
recover_data,
|
||||
revoke_data: Some(revoke_data),
|
||||
shares,
|
||||
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,
|
||||
recover_data: &[u8],
|
||||
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];
|
||||
let mut entropy2 = [0u8; 32];
|
||||
let mut entropy3 = [0u8; 32];
|
||||
let mut cipher_scan_key = [0u8; 60]; // cipher length == plain.len() + 16 + nonce.len()
|
||||
let mut part1_ciphertext = [0u8; 44];
|
||||
|
||||
let mut reader = Cursor::new(recover_data);
|
||||
reader.read_exact(&mut entropy1)?;
|
||||
reader.read_exact(&mut entropy2)?;
|
||||
reader.read_exact(&mut entropy3)?;
|
||||
reader.read_exact(&mut part1_ciphertext)?;
|
||||
reader.read_exact(&mut cipher_scan_key)?;
|
||||
|
||||
// We can retrieve the pre_id and check that it matches
|
||||
let retrieved_pre_id = Self::compute_pre_id(&user_password, &part1_ciphertext);
|
||||
|
||||
// If pre_id is not the same, password is probably false, or the client is feeding us garbage
|
||||
if retrieved_pre_id != pre_id {
|
||||
return Err(Error::msg("pre_id and recover_data don't match"));
|
||||
}
|
||||
|
||||
retrieved_spend_key[..16].copy_from_slice(&Self::recover_part1(
|
||||
&user_password,
|
||||
&entropy1,
|
||||
part1_ciphertext.to_vec(),
|
||||
)?);
|
||||
|
||||
retrieved_spend_key[16..].copy_from_slice(&Self::recover_part2(
|
||||
&user_password,
|
||||
&entropy2,
|
||||
shares,
|
||||
)?);
|
||||
|
||||
retrieved_scan_key.copy_from_slice(&Self::recover_key_slice(
|
||||
&user_password,
|
||||
&entropy3,
|
||||
cipher_scan_key.to_vec(),
|
||||
)?);
|
||||
|
||||
// we can create the recover sp_client
|
||||
let recover_client = SpClient::new(
|
||||
"".to_owned(),
|
||||
SecretKey::from_slice(&retrieved_scan_key)?,
|
||||
SpendKey::Secret(SecretKey::from_slice(&retrieved_spend_key)?),
|
||||
None,
|
||||
true,
|
||||
)?;
|
||||
|
||||
let recover_outputs = outputs
|
||||
.iter()
|
||||
.find(|o| o.check_fingerprint(&recover_client))
|
||||
.cloned();
|
||||
|
||||
let recover_wallet = SpWallet::new(recover_client, recover_outputs)?;
|
||||
|
||||
let user_wallets = UserWallets::new(None, Some(recover_wallet), None);
|
||||
|
||||
if let Ok(mut user) = lock_connected_user() {
|
||||
*user = user_wallets;
|
||||
} else {
|
||||
return Err(Error::msg("Failed to lock CONNECTED_USER"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recover_key_slice(password: &str, entropy: &[u8], ciphertext: Vec<u8>) -> Result<Vec<u8>> {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&password.as_bytes());
|
||||
engine.write_all(&entropy);
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
|
||||
let aes_dec =
|
||||
Aes256Decryption::new(Purpose::ThirtyTwoBytes, ciphertext, hash.to_byte_array())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
|
||||
fn recover_part1(password: &str, entropy: &[u8], ciphertext: Vec<u8>) -> Result<Vec<u8>> {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&password.as_bytes());
|
||||
engine.write_all(&entropy);
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
|
||||
let aes_dec = Aes256Decryption::new(Purpose::Login, ciphertext, hash.to_byte_array())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
|
||||
fn recover_part2(password: &str, entropy: &[u8], shares: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&password.as_bytes());
|
||||
engine.write_all(&entropy);
|
||||
let hash = sha256::Hash::from_engine(engine);
|
||||
|
||||
let threshold = (MANAGERS_NUMBER as f32 * QUORUM_SHARD).floor();
|
||||
debug_assert!(threshold > 0.0 && threshold <= u8::MAX as f32);
|
||||
|
||||
let part2_key_enc = Vec::from_hex(
|
||||
&SecretData::recover_secret(threshold as u8, shares.to_vec())
|
||||
.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())?;
|
||||
|
||||
aes_dec.decrypt_with_key()
|
||||
}
|
||||
|
||||
fn compute_pre_id(user_password: &str, cipher_recover_part1: &[u8]) -> PreId {
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
engine.write_all(&user_password.as_bytes());
|
||||
engine.write_all(&cipher_recover_part1);
|
||||
let pre_id = sha256::Hash::from_engine(engine);
|
||||
|
||||
pre_id.to_string()
|
||||
}
|
||||
|
||||
//not used
|
||||
// pub fn pbkdf2(password: &str, data: &str) -> String {
|
||||
// let data_salt = data.trim_end_matches('=');
|
||||
// let salt = SaltString::from_b64(data_salt)
|
||||
// .map(|s| s)
|
||||
// .unwrap_or_else(|_| panic!("Failed to parse salt value from base64 string"));
|
||||
|
||||
// let mut password_hash = String::new();
|
||||
// if let Ok(pwd) = Scrypt.hash_password(password.as_bytes(), &salt) {
|
||||
// password_hash.push_str(&pwd.to_string());
|
||||
// }
|
||||
// sha_256(&password_hash)
|
||||
// }
|
||||
|
||||
// // Test sharing JS side
|
||||
// pub fn get_shares(&self) -> Vec<String> {
|
||||
// self.sharding.shares_format_str.clone()
|
||||
// }
|
||||
|
||||
// //Test sharing Js side
|
||||
// pub fn get_secret(&self, shardings: Vec<String>) -> String {
|
||||
// let mut shares_vec = Vec::new();
|
||||
|
||||
// for s in shardings.iter() {
|
||||
// let bytes_vec: Vec<u8> = s
|
||||
// .trim_matches(|c| c == '[' || c == ']')
|
||||
// .split(',')
|
||||
// .filter_map(|s| s.trim().parse().ok())
|
||||
// .collect();
|
||||
// shares_vec.push(bytes_vec);
|
||||
// }
|
||||
// self.sharding.recover_secrete(shares_vec.clone())
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*; // Import everything from the outer module
|
||||
|
||||
const RECOVER_SPEND: &str = "394ef7757f5bc8cd692337c62abf6fa0ce9932fd4ec6676daddfbe3c1b3b9d11";
|
||||
const RECOVER_SCAN: &str = "3aa8cc570d17ec3a4dc4136e50151cc6de26052d968abfe02a5fea724ce38205";
|
||||
const REVOKE_SPEND: &str = "821c1a84fa9ee718c02005505fb8315bd479c7b9a878b1eff45929c48dfcaf28";
|
||||
const REVOKE_SCAN: &str = "a0f36cbc380624fa7eef022f39cab2716333451649dd8eb78e86d2e76bdb3f47";
|
||||
const MAIN_SPEND: &str = "b9098a6598ac55d8dd0e6b7aab0d1f63eb8792d06143f3c0fb6f5b80476a1c0d";
|
||||
const MAIN_SCAN: &str = "79dda4031663ac2cb250c46d896dc92b3c027a48a761b2342fabf1e441ea2857";
|
||||
const USER_PASSWORD: &str = "correct horse battery staple";
|
||||
const PROCESS: &str = "example";
|
||||
|
||||
fn helper_create_user_wallets() -> UserWallets {
|
||||
let label = "default".to_owned();
|
||||
let sp_main = SpClient::new(
|
||||
label.clone(),
|
||||
SecretKey::from_str(MAIN_SCAN).unwrap(),
|
||||
SpendKey::Secret(SecretKey::from_str(MAIN_SPEND).unwrap()),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let sp_recover = SpClient::new(
|
||||
label.clone(),
|
||||
SecretKey::from_str(RECOVER_SCAN).unwrap(),
|
||||
SpendKey::Secret(SecretKey::from_str(RECOVER_SPEND).unwrap()),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let sp_revoke = SpClient::new(
|
||||
label.clone(),
|
||||
SecretKey::from_str(REVOKE_SCAN).unwrap(),
|
||||
SpendKey::Secret(SecretKey::from_str(REVOKE_SPEND).unwrap()),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let user_wallets = UserWallets::new(
|
||||
Some(SpWallet::new(sp_main, None).unwrap()),
|
||||
Some(SpWallet::new(sp_recover, None).unwrap()),
|
||||
Some(SpWallet::new(sp_revoke, None).unwrap()),
|
||||
);
|
||||
|
||||
user_wallets
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logout() {
|
||||
let res = User::logout();
|
||||
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login() {
|
||||
let user_wallets = helper_create_user_wallets();
|
||||
let user = User::new(
|
||||
user_wallets.clone(),
|
||||
USER_PASSWORD.to_owned(),
|
||||
PROCESS.to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = User::login(
|
||||
user.pre_id.clone(),
|
||||
USER_PASSWORD.to_owned(),
|
||||
&user.recover_data,
|
||||
&user.shares,
|
||||
&user_wallets.get_all_outputs(),
|
||||
);
|
||||
|
||||
assert!(res.is_ok());
|
||||
|
||||
let connected = lock_connected_user().unwrap();
|
||||
|
||||
let recover = connected.try_get_recover().unwrap();
|
||||
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
recover
|
||||
.get_client()
|
||||
.try_get_secret_spend_key()
|
||||
.unwrap()
|
||||
.display_secret()
|
||||
) == RECOVER_SPEND
|
||||
)
|
||||
}
|
||||
}
|
4765
package-lock.json
generated
Normal file
4765
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "sdk_client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"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 --dev",
|
||||
"start": "webpack serve",
|
||||
"build": "webpack"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.2"
|
||||
}
|
||||
}
|
BIN
src/assets/4nk_image.png
Normal file
BIN
src/assets/4nk_image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
BIN
src/assets/4nk_revoke.jpg
Normal file
BIN
src/assets/4nk_revoke.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
178
src/database.ts
Normal file
178
src/database.ts
Normal file
@ -0,0 +1,178 @@
|
||||
class Database {
|
||||
private static instance: Database;
|
||||
private db: IDBDatabase | null = null;
|
||||
private dbName: string = '4nk';
|
||||
private dbVersion: number = 1;
|
||||
private storeDefinitions = {
|
||||
AnkUser: {
|
||||
name: "user",
|
||||
options: {'keyPath': 'pre_id'},
|
||||
indices: []
|
||||
},
|
||||
AnkSession: {
|
||||
name: "session",
|
||||
options: {},
|
||||
indices: []
|
||||
},
|
||||
AnkProcess: {
|
||||
name: "process",
|
||||
options: {'keyPath': 'id'},
|
||||
indices: [{
|
||||
name: 'by_name',
|
||||
keyPath: 'name',
|
||||
options: {
|
||||
'unique': true
|
||||
}
|
||||
}]
|
||||
},
|
||||
AnkMessages: {
|
||||
name: "messages",
|
||||
options: {'keyPath': 'id'},
|
||||
indices: []
|
||||
}
|
||||
}
|
||||
|
||||
// Private constructor to prevent direct instantiation from outside
|
||||
private constructor() {}
|
||||
|
||||
// Method to access the singleton instance of Database
|
||||
public static async getInstance(): Promise<Database> {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
await Database.instance.init();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
// Initialize the database
|
||||
private async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
|
||||
Object.values(this.storeDefinitions).forEach(({name, options, indices}) => {
|
||||
if (!db.objectStoreNames.contains(name)) {
|
||||
let store = db.createObjectStore(name, options);
|
||||
|
||||
indices.forEach(({name, keyPath, options}) => {
|
||||
store.createIndex(name, keyPath, options);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Database error:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getDb(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
await this.init();
|
||||
}
|
||||
return this.db!;
|
||||
}
|
||||
|
||||
public getStoreList(): {[key: string]: string} {
|
||||
const objectList: {[key: string]: string} = {};
|
||||
Object.keys(this.storeDefinitions).forEach(key => {
|
||||
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
|
||||
});
|
||||
return objectList;
|
||||
}
|
||||
|
||||
public writeObject(db: IDBDatabase, storeName: string, obj: any, key: IDBValidKey | null): Promise<IDBRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
let request: IDBRequest<any>;
|
||||
if (key) {
|
||||
request = store.add(obj, key);
|
||||
} else {
|
||||
request = store.add(obj);
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
public getObject<T>(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const index = store.index(indexName);
|
||||
const request = index.openCursor(IDBKeyRange.only(lookup));
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const cursor = request.result;
|
||||
if (cursor) {
|
||||
resolve(cursor.value);
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setObject(db: IDBDatabase, storeName: string, obj: any, key: string | null): Promise<IDBRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
let request: IDBRequest<any>;
|
||||
if (key) {
|
||||
request = store.put(obj, key);
|
||||
} else {
|
||||
request = store.put(obj);
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
public getAll<T>(db: IDBDatabase, storeName: string): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
17
src/index.html
Normal file
17
src/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="author" content="4NK">
|
||||
<meta name="description" content="4NK Web5 Platform">
|
||||
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style/4nk.css">
|
||||
<title>4NK Application</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="containerId" class="container">
|
||||
<!-- 4NK Web5 Solution -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
20
src/index.ts
Normal file
20
src/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import Services from './services';
|
||||
import { WebSocketClient } from './websockets';
|
||||
|
||||
const wsurl = "ws://localhost:8090";
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const services = await Services.getInstance();
|
||||
await services.addWebsocketConnection(wsurl);
|
||||
|
||||
if ((await services.isNewUser())) {
|
||||
await services.displayCreateId();
|
||||
}
|
||||
else {
|
||||
await services.displayRecover()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
909
src/services.ts
Normal file
909
src/services.ts
Normal file
@ -0,0 +1,909 @@
|
||||
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';
|
||||
|
||||
class Services {
|
||||
private static instance: 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() {}
|
||||
|
||||
// Method to access the singleton instance of Services
|
||||
public static async getInstance(): Promise<Services> {
|
||||
if (!Services.instance) {
|
||||
Services.instance = new Services();
|
||||
await Services.instance.init();
|
||||
}
|
||||
return Services.instance;
|
||||
}
|
||||
|
||||
// The init method is now part of the instance, and should only be called once
|
||||
private async init(): Promise<void> {
|
||||
this.sdkClient = await import("../dist/pkg/sdk_client");
|
||||
this.sdkClient.setup();
|
||||
await this.updateProcesses();
|
||||
}
|
||||
|
||||
public async addWebsocketConnection(url: string): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const newClient = new WebSocketClient(url, services);
|
||||
if (!services.websocketConnection.includes(newClient)) {
|
||||
services.websocketConnection.push(newClient);
|
||||
}
|
||||
}
|
||||
|
||||
public async isNewUser(): Promise<boolean> {
|
||||
let isNew = false;
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
let userListObject = await indexedDB.getAll<User>(db, indexedDB.getStoreList().AnkUser);
|
||||
if (userListObject.length == 0) {
|
||||
isNew = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve isNewUser :", error);
|
||||
}
|
||||
return isNew;
|
||||
}
|
||||
|
||||
public async displayCreateId(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
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) {
|
||||
throw 'One or more elements not found';
|
||||
}
|
||||
|
||||
const password = passwordElement.value;
|
||||
this.current_process = processElement.value;
|
||||
// console.log("JS password: " + password + " process: " + this.current_process);
|
||||
// To comment if test
|
||||
// if (!Services.instance.isPasswordValid(password)) return;
|
||||
|
||||
const label = null;
|
||||
const birthday_signet = 50000;
|
||||
const birthday_main = 500000;
|
||||
|
||||
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;
|
||||
// send the shares on the network
|
||||
const revokeData = user.revoke_data;
|
||||
if (!revokeData) {
|
||||
throw 'Failed to get revoke data from wasm';
|
||||
}
|
||||
|
||||
// user.shares = [];
|
||||
user.revoke_data = null;
|
||||
|
||||
try {
|
||||
const indexedDb = await IndexedDB.getInstance();
|
||||
const db = await indexedDb.getDb();
|
||||
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user, null);
|
||||
} catch (error) {
|
||||
throw `Failed to write user object: ${error}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await services.obtainTokenWithFaucet();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await services.displayRevokeImage(new Uint8Array(revokeData));
|
||||
}
|
||||
|
||||
public async displayRecover(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
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);
|
||||
await services.displayProcess();
|
||||
}
|
||||
|
||||
public async recover(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const password = passwordElement.value;
|
||||
const process = processElement.value;
|
||||
// console.log("JS password: " + password + " process: " + process);
|
||||
// To comment if test
|
||||
// if (!Services.instance.isPasswordValid(password)) return;
|
||||
|
||||
// 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.revokeImageInjectHtml();
|
||||
services.attachClickListener("displayupdateanid", services.displayUpdateAnId);
|
||||
|
||||
let imageBytes = await services.getRecoverImage('assets/4nk_revoke.jpg');
|
||||
if (imageBytes != null) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getRecoverImage(imageUrl:string): Promise<Uint8Array|null> {
|
||||
let imageBytes = null;
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
imageBytes = new Uint8Array(arrayBuffer);
|
||||
} catch (error) {
|
||||
console.error("Failed to get image : "+imageUrl, error);
|
||||
}
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
public async displayRevoke(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
await services.revokeInjectHtml();
|
||||
services.attachClickListener("displayrecover", Services.instance.displayRecover);
|
||||
services.attachSubmitListener("form4nk", Services.instance.revoke);
|
||||
}
|
||||
|
||||
public async revoke(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
console.log("JS revoke click ");
|
||||
// TODO
|
||||
alert("revoke click to do ...");
|
||||
}
|
||||
|
||||
public async displayUpdateAnId() {
|
||||
const services = await Services.getInstance();
|
||||
|
||||
await services.updateIdInjectHtml();
|
||||
|
||||
services.attachSubmitListener("form4nk", services.updateAnId);
|
||||
}
|
||||
|
||||
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 async updateAnId(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
// TODO get values
|
||||
const firstNameElement = 'firstName';
|
||||
const lastNameElement = 'lastName';
|
||||
const firstName = document.getElementById(firstNameElement) as HTMLInputElement;
|
||||
const lastName = document.getElementById(lastNameElement) as HTMLInputElement;
|
||||
|
||||
console.log("JS updateAnId submit ");
|
||||
// TODO
|
||||
alert("updateAnId submit to do ... Name : "+firstName.value + " "+lastName.value);
|
||||
|
||||
// TODO Mock add user member to process
|
||||
}
|
||||
|
||||
public async displayProcess(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const processList = await services.getAllProcess();
|
||||
const selectProcess = document.getElementById("selectProcess");
|
||||
if (selectProcess) {
|
||||
processList.forEach((process) => {
|
||||
let child = new Option(process.name, process.name);
|
||||
if (!selectProcess.contains(child)) {
|
||||
selectProcess.appendChild(child);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async addProcess(process: Process): Promise<void> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
|
||||
} catch (error) {
|
||||
console.log('addProcess failed: ',error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllProcess(): Promise<Process[]> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
let processListObject = await indexedDB.getAll<Process>(db, indexedDB.getStoreList().AnkProcess);
|
||||
return processListObject;
|
||||
} catch (error) {
|
||||
console.log('getAllProcess failed: ',error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async updateOwnedOutputsForUser(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
let latest_outputs: outputs_list;
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllProcessForUser(pre_id: string): Promise<Process[]> {
|
||||
const services = await Services.getInstance();
|
||||
let user: User;
|
||||
let userProcessList: Process[] = [];
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
user = await indexedDB.getObject<User>(db, indexedDB.getStoreList().AnkUser, pre_id);
|
||||
} catch (error) {
|
||||
console.error('getAllUserProcess failed: ',error);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const processListObject = await services.getAllProcess();
|
||||
processListObject.forEach(async (processObject) => {
|
||||
if (processObject.members.includes(user.pre_id)) {
|
||||
userProcessList.push(processObject);
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('getAllUserProcess failed: ',error);
|
||||
return [];
|
||||
}
|
||||
return userProcessList;
|
||||
}
|
||||
|
||||
public async getProcessByName(name: string): Promise<Process | null> {
|
||||
console.log('getProcessByName name: '+name);
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
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 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();
|
||||
|
||||
processList.forEach(async (process: Process) => {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = await indexedDB.getDb();
|
||||
try {
|
||||
const processStore = await indexedDB.getObject<Process>(db, indexedDB.getStoreList().AnkProcess, process.id);
|
||||
if (!processStore) {
|
||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while writing process', process.name, 'to indexedDB:', error);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public attachClickListener(elementId: string, callback: (event: Event) => void): void {
|
||||
const element = document.getElementById(elementId);
|
||||
element?.removeEventListener("click", callback);
|
||||
element?.addEventListener("click", callback);
|
||||
}
|
||||
|
||||
public attachSubmitListener(elementId: string, callback: (event: Event) => void): void {
|
||||
const element = document.getElementById(elementId);
|
||||
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');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
return;
|
||||
}
|
||||
|
||||
const services = await Services.getInstance();
|
||||
|
||||
// do we have all processes in db?
|
||||
const knownProcesses = await services.getAllProcess();
|
||||
const processesFromNetwork: Process[] = services.sdkClient.get_processes();
|
||||
|
||||
const processToAdd = processesFromNetwork.filter(processFromNetwork => !knownProcesses.some(knownProcess => knownProcess.id === processFromNetwork.id));
|
||||
|
||||
processToAdd.forEach(async p => {
|
||||
await services.addProcess(p);
|
||||
})
|
||||
|
||||
// get the process we need
|
||||
const process = await services.getProcessByName(processName);
|
||||
if (process) {
|
||||
container.innerHTML = process.html;
|
||||
} else {
|
||||
console.error("No process ", processName);
|
||||
}
|
||||
}
|
||||
|
||||
// public async getCurrentProcess(): Promise<string> {
|
||||
// let currentProcess = "";
|
||||
// try {
|
||||
// const indexedDB = await IndexedDB.getInstance();
|
||||
// const db = indexedDB.getDb();
|
||||
// currentProcess = await indexedDB.getObject<string>(db, indexedDB.getStoreList().AnkSession, Services.CURRENT_PROCESS);
|
||||
// } catch (error) {
|
||||
// console.error("Failed to retrieve currentprocess object :", error);
|
||||
// }
|
||||
// return currentProcess;
|
||||
// }
|
||||
|
||||
public isPasswordValid(password: string) {
|
||||
var alertElem = document.getElementById("passwordalert");
|
||||
var success = true;
|
||||
var strength = 0;
|
||||
if (password.match(/[a-z]+/)) {
|
||||
var strength = 0;
|
||||
strength += 1;
|
||||
}
|
||||
if (password.match(/[A-Z]+/)) {
|
||||
strength += 1;
|
||||
}
|
||||
if (password.match(/[0-9]+/)) {
|
||||
strength += 1;
|
||||
}
|
||||
if (password.match(/[$@#&!]+/)) {
|
||||
strength += 1;
|
||||
}
|
||||
if (alertElem !== null) {
|
||||
// TODO Passer à 18
|
||||
if (password.length < 4) {
|
||||
alertElem.innerHTML = "Password size is < 4";
|
||||
success = false;
|
||||
} else {
|
||||
if (password.length > 30) {
|
||||
alertElem.innerHTML = "Password size is > 30";
|
||||
success = false;
|
||||
} else {
|
||||
if (strength < 4) {
|
||||
alertElem.innerHTML = "Password need [a-z] [A-Z] [0-9]+ [$@#&!]+";
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private async pickWebsocketConnectionRandom(): Promise<WebSocketClient | null> {
|
||||
const services = await Services.getInstance();
|
||||
const websockets = services.websocketConnection;
|
||||
if (websockets.length === 0) {
|
||||
console.error("No websocket connection available at the moment");
|
||||
return null;
|
||||
} else {
|
||||
const random = Math.floor(Math.random() * websockets.length);
|
||||
return websockets[random];
|
||||
}
|
||||
}
|
||||
|
||||
public async obtainTokenWithFaucet(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const connection = await services.pickWebsocketConnectionRandom();
|
||||
if (!connection) {
|
||||
throw 'no available relay connections';
|
||||
}
|
||||
|
||||
let cachedMsg: CachedMessage;
|
||||
try {
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Services;
|
170
src/style/4nk.css
Normal file
170
src/style/4nk.css
Normal file
@ -0,0 +1,170 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
.card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* flex-wrap: wrap; */
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
margin: 10px 0;
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
select {
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.side-by-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.side-by-side>* {
|
||||
display: inline-block;
|
||||
}
|
||||
button.recover {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.recover:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
a.btn {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 17px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #78a6de;
|
||||
}
|
||||
.bg-secondary {
|
||||
background-color: #2b81ed;
|
||||
}
|
||||
.bg-primary {
|
||||
background-color: #1A61ED;
|
||||
}
|
||||
.bg-primary:hover {
|
||||
background-color: #457be8;
|
||||
}
|
||||
.card-revoke {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-revoke a {
|
||||
max-width: 50px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-revoke button {
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #78a6de;
|
||||
}
|
||||
.card-revoke svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
fill: #333;
|
||||
}
|
||||
.image-label {
|
||||
display: block;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.image-container {
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-container img {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
}
|
||||
.passwordalert {
|
||||
color: #FF0000;
|
||||
}
|
117
src/websockets.ts
Normal file
117
src/websockets.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import Services from "./services";
|
||||
import { AnkFlag, AnkNetworkMsg, CachedMessage } from "../dist/pkg/sdk_client";
|
||||
|
||||
class WebSocketClient {
|
||||
private ws: WebSocket;
|
||||
private messageQueue: string[] = [];
|
||||
|
||||
constructor(url: string, private services: Services) {
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.addEventListener('open', (event) => {
|
||||
console.log('WebSocket connection established');
|
||||
// Once the connection is open, send all messages in the queue
|
||||
while (this.messageQueue.length > 0) {
|
||||
const message = this.messageQueue.shift();
|
||||
if (message) {
|
||||
this.ws.send(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for messages
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
const msgData = event.data;
|
||||
|
||||
(async () => {
|
||||
if (typeof(msgData) === 'string') {
|
||||
console.log("Received text message: "+msgData);
|
||||
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 a non-string message');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
// Listen for possible errors
|
||||
this.ws.addEventListener('error', (event) => {
|
||||
console.error('WebSocket error:', event);
|
||||
});
|
||||
|
||||
// Listen for when the connection is closed
|
||||
this.ws.addEventListener('close', (event) => {
|
||||
console.log('WebSocket is closed now.');
|
||||
});
|
||||
}
|
||||
|
||||
// Method to send messages
|
||||
public sendMessage(flag: AnkFlag, message: string): void {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
const networkMessage: AnkNetworkMsg = {
|
||||
'flag': flag,
|
||||
'content': message
|
||||
}
|
||||
// console.debug("Sending message:", JSON.stringify(networkMessage));
|
||||
this.ws.send(JSON.stringify(networkMessage));
|
||||
} else {
|
||||
console.warn('WebSocket is not open. ReadyState:', this.ws.readyState);
|
||||
this.messageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
public getUrl(): string {
|
||||
return this.ws.url;
|
||||
}
|
||||
|
||||
// Method to close the WebSocket connection
|
||||
public close(): void {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
export { WebSocketClient };
|
110
tsconfig.json
Normal file
110
tsconfig.json
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ESNext", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
46
webpack.config.js
Normal file
46
webpack.config.js
Normal file
@ -0,0 +1,46 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: './src/index.ts',
|
||||
devtool: 'inline-source-map',
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: 'webassembly/async',
|
||||
}
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html'
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/assets', to: './assets' },
|
||||
{ from: 'src/style', to: './style' }
|
||||
],
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
static: './dist',
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user