Compare commits

..

No commits in common. "main" and "master" have entirely different histories.
main ... master

33 changed files with 4257 additions and 8336 deletions

10
.gitignore vendored
View File

@ -1,6 +1,4 @@
target/
pkg/
Cargo.lock
node_modules/
dist/
.vscode
node_modules
pkg
dist
data

View File

@ -1,5 +0,0 @@
[workspace]
resolver = "2"
members = [
"crates/sp_client"
]

View File

@ -1,93 +0,0 @@
# sdk_client
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://git.4nkweb.com/4nk/sdk_client.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://git.4nkweb.com/4nk/sdk_client/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

171
SERVER_README.md Normal file
View File

@ -0,0 +1,171 @@
# 4NK Protocol Server
A high-availability server that automatically handles protocol operations for the 4NK network.
## Features
- **High Availability**: Runs continuously on a server
- **Automatic Operations**: Handles UPDATE_PROCESS, NOTIFY_UPDATE, and VALIDATE_STATE operations
- **Protocol Compatible**: Uses the same message format as the browser client
- **WebSocket Interface**: Real-time communication with clients
## Quick Start
### 1. Install Dependencies
```bash
npm install
```
### 2. Build the Server
```bash
npm run build:server
```
### 3. Start the Server
```bash
npm run start:server
```
### 4. Development Mode
```bash
npm run dev:server
```
## Configuration
Create a `.env` file in the root directory:
```env
PORT=8080
JWT_SECRET_KEY=your-secret-key-here
DATABASE_PATH=./data/server.db
RELAY_URLS=ws://localhost:8090,ws://relay2.example.com:8090
LOG_LEVEL=info
```
## API Usage
Connect to the WebSocket server and send messages in the same format as the browser client:
### Example: Update Process
```javascript
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'UPDATE_PROCESS',
processId: 'your-process-id',
newData: { field: 'value' },
privateFields: [],
roles: {},
accessToken: 'your-access-token',
messageId: 'unique-message-id'
}));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
console.log('Response:', response);
};
```
### Example: Notify Update
```javascript
ws.send(JSON.stringify({
type: 'NOTIFY_UPDATE',
processId: 'your-process-id',
stateId: 'your-state-id',
accessToken: 'your-access-token',
messageId: 'unique-message-id'
}));
```
### Example: Validate State
```javascript
ws.send(JSON.stringify({
type: 'VALIDATE_STATE',
processId: 'your-process-id',
stateId: 'your-state-id',
accessToken: 'your-access-token',
messageId: 'unique-message-id'
}));
```
## Deployment
### Systemd Service (Linux)
```bash
# Create service file
sudo tee /etc/systemd/system/4nk-server.service > /dev/null <<EOF
[Unit]
Description=4NK Protocol Server
After=network.target
[Service]
Type=simple
User=4nk
WorkingDirectory=/opt/4nk-server
ExecStart=/usr/bin/node server-dist/server/server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=JWT_SECRET_KEY=your-secret-key
[Install]
WantedBy=multi-user.target
EOF
# Enable and start service
sudo systemctl enable 4nk-server.service
sudo systemctl start 4nk-server.service
```
### Docker
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY server-dist ./server-dist
EXPOSE 8080
CMD ["node", "server-dist/server/server.js"]
```
## Monitoring
The server logs all operations with timestamps and client IDs:
```
🚀 Initializing 4NK Protocol Server...
📡 Connecting to protocol relays...
✅ Server running on port 8080
📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE
🔗 Client connected: client_1234567890_abc123 from 192.168.1.100
📨 Received message from client_1234567890_abc123: UPDATE_PROCESS
```
## Security
- All operations require valid JWT tokens
- Client IDs are tracked for audit logs
- Graceful shutdown handling
- Error handling and logging
## Troubleshooting
### Common Issues
1. **Port already in use**: Change the PORT environment variable
2. **JWT validation fails**: Check JWT_SECRET_KEY configuration
3. **Relay connection fails**: Verify RELAY_URLS configuration
4. **Permission denied**: Check file permissions for database directory
### Logs
Check server logs for detailed error information:
```bash
# If using systemd
sudo journalctl -u 4nk-server.service -f
# If running directly
npm run start:server
```

View File

@ -1,27 +0,0 @@
[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"

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
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)
}

View File

@ -1,34 +0,0 @@
#![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)))
}
}

View File

@ -1,9 +0,0 @@
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Peer {
pub addr: SocketAddr,
pub processes: Vec<String>,
}

View File

@ -1,405 +0,0 @@
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
}

View File

@ -1,544 +0,0 @@
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
)
}
}

4819
package-lock.json generated

File diff suppressed because it is too large Load Diff

24
package.json Normal file → Executable file
View File

@ -1,24 +1,26 @@
{
"name": "sdk_client",
"name": "sdk_signer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "dist/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"
"build_wasm": "wasm-pack build --out-dir ../sdk_signer/pkg ../sdk_client --target nodejs --dev",
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"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"
"ts-node": "^10.9.2"
},
"dependencies": {
"ws": "^8.14.2",
"@types/ws": "^8.5.10",
"dotenv": "^16.3.1",
"level": "^10.0.0"
}
}

1703
src/.service.bak.ts Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

19
src/config.ts Normal file
View File

@ -0,0 +1,19 @@
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
export const config = {
port: parseInt(process.env.PORT || '8080'),
apiKey: process.env.API_KEY || 'your-api-key-change-this',
databasePath: process.env.DATABASE_PATH || './data/server.db',
relayUrls: process.env.RELAY_URLS?.split(',') || ['ws://localhost:8090'],
autoRestart: process.env.AUTO_RESTART === 'true',
maxRestarts: parseInt(process.env.MAX_RESTARTS || '10'),
logLevel: process.env.LOG_LEVEL || 'info'
};
// Validate required environment variables
if (!config.apiKey || config.apiKey === 'your-api-key-change-this') {
console.warn('⚠️ Warning: Using default API key. Set API_KEY environment variable for production.');
}

276
src/database.service.ts Normal file
View File

@ -0,0 +1,276 @@
// LevelDB-based persistent key-value database service
// High performance alternative to JSON files
import { Level } from 'level';
import * as path from 'path';
interface DatabaseObject {
storeName: string;
object: any;
key: string | null;
}
interface BatchWriteOperation {
storeName: string;
objects: Array<{ key: string; object: any }>;
}
export default class Database {
private static instance: Database;
private db!: Level;
private dataDir: string;
private initialized: boolean = false;
private constructor() {
this.dataDir = path.join(process.cwd(), 'data');
console.log('🔧 Database service initialized (LevelDB)');
}
public static async getInstance(): Promise<Database> {
if (!Database.instance) {
Database.instance = new Database();
await Database.instance.init();
}
return Database.instance;
}
private async init(): Promise<void> {
if (this.initialized) return;
try {
// Initialize LevelDB with performance optimizations
this.db = new Level(this.dataDir, {
valueEncoding: 'json',
maxFileSize: 2 * 1024 * 1024, // 2MB
blockSize: 4096,
cacheSize: 8 * 1024 * 1024 // 8MB cache
});
this.initialized = true;
console.log('✅ LevelDB database initialized with persistent storage');
} catch (error) {
console.error('❌ Failed to initialize LevelDB database:', error);
throw error;
}
}
// Key encoding: "storeName:key"
private getKey(storeName: string, key: string): string {
return `${storeName}:${key}`;
}
private parseKey(fullKey: string): { storeName: string; key: string } | null {
const parts = fullKey.split(':', 2);
if (parts.length !== 2) return null;
return { storeName: parts[0], key: parts[1] };
}
/**
* Get a single object from a store
* O(log n) operation - only reads specific key
*/
public async getObject(storeName: string, key: string): Promise<any | null> {
try {
const fullKey = this.getKey(storeName, key);
return await this.db.get(fullKey);
} catch (error) {
if ((error as any).code === 'LEVEL_NOT_FOUND') {
return null;
}
throw error;
}
}
/**
* Add or update an object in a store
* O(log n) operation - only writes specific key-value pair
*/
public async addObject(operation: DatabaseObject): Promise<void> {
const { storeName, object, key } = operation;
if (key) {
const fullKey = this.getKey(storeName, key);
await this.db.put(fullKey, object);
} else {
// Auto-generate key if none provided
const autoKey = Date.now().toString() + Math.random().toString(36).substr(2, 9);
const fullKey = this.getKey(storeName, autoKey);
await this.db.put(fullKey, object);
}
}
/**
* Delete an object from a store
* O(log n) operation - only deletes specific key
*/
public async deleteObject(storeName: string, key: string): Promise<void> {
try {
const fullKey = this.getKey(storeName, key);
await this.db.del(fullKey);
} catch (error) {
if ((error as any).code === 'LEVEL_NOT_FOUND') {
// Key doesn't exist, that's fine
return;
}
throw error;
}
}
/**
* Get all objects from a store as a record
* Efficient range query - only reads keys in the store
*/
public async dumpStore(storeName: string): Promise<Record<string, any>> {
const result: Record<string, any> = {};
const prefix = `${storeName}:`;
try {
// Use LevelDB's range queries for efficient store dumping
for await (const [key, value] of this.db.iterator({
gte: prefix,
lt: prefix + '\xff' // '\xff' is higher than any valid character
})) {
const parsed = this.parseKey(key);
if (parsed && parsed.storeName === storeName) {
result[parsed.key] = value;
}
}
} catch (error) {
console.error(`Failed to dump store ${storeName}:`, error);
}
return result;
}
/**
* Clear all objects from a store
* Efficient store clearing using range deletes
*/
public async clearStore(storeName: string): Promise<void> {
const prefix = `${storeName}:`;
const batch = this.db.batch();
// Collect all keys in the store
for await (const [key] of this.db.iterator({
gte: prefix,
lt: prefix + '\xff'
})) {
batch.del(key);
}
await batch.write();
}
/**
* Batch write operations
* Atomic batch operations
*/
public async batchWriting(operation: BatchWriteOperation): Promise<void> {
const { storeName, objects } = operation;
// Use LevelDB's batch operations for atomic writes
const batch = this.db.batch();
for (const { key, object } of objects) {
const fullKey = this.getKey(storeName, key);
batch.put(fullKey, object);
}
await batch.write();
}
/**
* Get all store names (for debugging)
*/
public async getStoreNames(): Promise<string[]> {
const storeNames = new Set<string>();
for await (const [key] of this.db.iterator()) {
const parsed = this.parseKey(key);
if (parsed) {
storeNames.add(parsed.storeName);
}
}
return Array.from(storeNames);
}
/**
* Get store size (for debugging)
*/
public async getStoreSize(storeName: string): Promise<number> {
let count = 0;
const prefix = `${storeName}:`;
for await (const [key] of this.db.iterator({
gte: prefix,
lt: prefix + '\xff'
})) {
count++;
}
return count;
}
/**
* Clear all data (for testing/reset)
*/
public async clearAll(): Promise<void> {
// Close and reopen database to clear all data
await this.db.close();
// Remove the data directory
const fs = require('fs');
if (fs.existsSync(this.dataDir)) {
fs.rmSync(this.dataDir, { recursive: true, force: true });
}
// Reinitialize
this.initialized = false;
await this.init();
}
/**
* Get data directory path (for debugging)
*/
public getDataDirectory(): string {
return this.dataDir;
}
/**
* Get database statistics (LevelDB specific)
*/
public async getStats(): Promise<any> {
let totalKeys = 0;
let totalStores = 0;
const storeSizes: Record<string, number> = {};
for await (const [key] of this.db.iterator()) {
totalKeys++;
const parsed = this.parseKey(key);
if (parsed) {
if (!storeSizes[parsed.storeName]) {
storeSizes[parsed.storeName] = 0;
totalStores++;
}
storeSizes[parsed.storeName]++;
}
}
return {
totalKeys,
totalStores,
storeSizes,
dataDirectory: this.dataDir
};
}
/**
* Close database connection (should be called on app shutdown)
*/
public async close(): Promise<void> {
if (this.db) {
await this.db.close();
}
}
}

View File

@ -1,178 +0,0 @@
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;

View File

@ -1,17 +0,0 @@
<!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>

View File

@ -1,20 +1,9 @@
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);
}
});
// Main entry point for the SDK Signer server
export { Service } from './service';
export { config } from './config';
export { MessageType, AnkFlag } from './models';
export { isValid32ByteHex } from './utils';
export { RelayManager } from './relay-manager';
// Re-export the main server class
export { Server } from './simple-server';

60
src/models.ts Normal file
View File

@ -0,0 +1,60 @@
// Server-specific model definitions
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
ERROR = 'ERROR',
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
RENEW_TOKEN = 'RENEW_TOKEN',
// Get various information
GET_PAIRING_ID = 'GET_PAIRING_ID',
GET_PROCESSES = 'GET_PROCESSES',
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
RETRIEVE_DATA = 'RETRIEVE_DATA',
DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes
CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
VALIDATE_STATE = 'VALIDATE_STATE',
STATE_VALIDATED = 'STATE_VALIDATED',
// Hash and merkle proof
HASH_VALUE = 'HASH_VALUE',
VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}
// Re-export AnkFlag from WASM for relay message typing
export { AnkFlag } from '../pkg/sdk_client';
// Message priority levels
export enum MessagePriority {
LOW = 0,
NORMAL = 1,
HIGH = 2,
CRITICAL = 3,
}
// Message delivery status
export enum DeliveryStatus {
PENDING = 'PENDING',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
FAILED = 'FAILED',
RETRY = 'RETRY',
}

96
src/relay-example.ts Normal file
View File

@ -0,0 +1,96 @@
// Example usage of the RelayManager for outbound relay connections
import { RelayManager } from './relay-manager';
import { AnkFlag } from './models';
async function exampleRelayUsage() {
const relayManager = RelayManager.getInstance();
// 1. Connect to relays
console.log('🔗 Connecting to relays...');
// Connect to a faucet relay
await relayManager.connectToRelay(
'faucet-relay-1',
'wss://faucet-relay.example.com/ws',
'sp1qfaucetrelayaddress...'
);
// Connect to a validator relay
await relayManager.connectToRelay(
'validator-relay-1',
'wss://validator-relay.example.com/ws',
'sp1qvalidatorrelayaddress...'
);
// 2. Send messages using protocol-specific methods
console.log('📤 Sending protocol messages...');
// Send faucet message
relayManager.sendFaucetMessage('faucet_request_message');
// Send new transaction
relayManager.sendNewTxMessage('new_transaction_data');
// Send commit message
relayManager.sendCommitMessage('commit_data');
// Send cipher messages
relayManager.sendCipherMessages(['cipher1', 'cipher2']);
// 3. Send messages using generic method with AnkFlag
console.log('📤 Sending generic messages...');
relayManager.sendMessage('NewTx' as AnkFlag, 'transaction_data', 'validator-relay-1');
relayManager.sendMessage('Faucet' as AnkFlag, 'faucet_data', 'faucet-relay-1');
relayManager.sendMessage('Cipher' as AnkFlag, 'cipher_data'); // Broadcast to all
// 4. Check relay status
const stats = relayManager.getStats();
console.log('📊 Relay stats:', stats);
// 5. Get connected relays
const connectedRelays = relayManager.getConnectedRelays();
console.log('🔗 Connected relays:', connectedRelays.map(r => r.id));
// 6. Send to specific relay
const success = relayManager.sendToRelay('faucet-relay-1', 'Faucet' as AnkFlag, 'specific_message');
console.log('✅ Message sent to specific relay:', success);
// 7. Broadcast to all relays
const sentCount = relayManager.broadcastToAllRelays('Sync' as AnkFlag, 'sync_data');
console.log(`📡 Broadcasted to ${sentCount} relays`);
}
// Example of how to add relay addresses to the service
async function addRelayAddresses() {
// This would typically be done in the service configuration
// For now, we'll show the structure
const relayAddresses = {
'wss://faucet-relay.example.com/ws': 'sp1qfaucetrelayaddress...',
'wss://validator-relay.example.com/ws': 'sp1qvalidatorrelayaddress...',
'wss://network-relay.example.com/ws': 'sp1qnetworkrelayaddress...'
};
console.log('📝 Relay addresses configured:', Object.keys(relayAddresses));
}
// Example of storage API usage (separate from relay system)
async function storageExample() {
// Storage is handled via REST API, not WebSocket
console.log('💾 Storage operations would use REST API endpoints:');
console.log(' - POST /storage/push');
console.log(' - GET /storage/retrieve/{hash}');
console.log(' - DELETE /storage/delete/{hash}');
console.log(' - GET /storage/list');
}
// Run examples
if (require.main === module) {
exampleRelayUsage()
.then(() => addRelayAddresses())
.then(() => storageExample())
.catch(console.error);
}
export { exampleRelayUsage, addRelayAddresses, storageExample };

396
src/relay-manager.ts Normal file
View File

@ -0,0 +1,396 @@
import WebSocket from 'ws';
import { AnkFlag } from '../pkg/sdk_client';
interface RelayConnection {
id: string;
ws: WebSocket;
url: string;
spAddress: string;
isConnected: boolean;
lastHeartbeat: number;
reconnectAttempts: number;
maxReconnectAttempts: number;
}
interface QueuedMessage {
id: string;
flag: AnkFlag;
payload: any;
targetRelayId?: string;
timestamp: number;
expiresAt?: number;
retryCount: number;
maxRetries: number;
}
export class RelayManager {
private static instance: RelayManager;
private relays: Map<string, RelayConnection> = new Map();
private messageQueue: QueuedMessage[] = [];
private processingQueue = false;
private heartbeatInterval: NodeJS.Timeout | null = null;
private queueProcessingInterval: NodeJS.Timeout | null = null;
private reconnectInterval: NodeJS.Timeout | null = null;
private constructor() {
this.startHeartbeat();
this.startQueueProcessing();
this.startReconnectMonitoring();
}
static getInstance(): RelayManager {
if (!RelayManager.instance) {
RelayManager.instance = new RelayManager();
}
return RelayManager.instance;
}
// Relay Management - Outbound Connections
public async connectToRelay(relayId: string, wsUrl: string, spAddress: string): Promise<boolean> {
try {
console.log(`🔗 Connecting to relay ${relayId} at ${wsUrl}`);
const ws = new WebSocket(wsUrl);
const relay: RelayConnection = {
id: relayId,
ws,
url: wsUrl,
spAddress,
isConnected: false,
lastHeartbeat: Date.now(),
reconnectAttempts: 0,
maxReconnectAttempts: 5
};
// Set up WebSocket event handlers
ws.on('open', () => {
console.log(`✅ Connected to relay ${relayId}`);
relay.isConnected = true;
relay.reconnectAttempts = 0;
relay.lastHeartbeat = Date.now();
});
ws.on('message', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString());
this.handleRelayMessage(relayId, message);
} catch (error) {
console.error(`❌ Error parsing message from relay ${relayId}:`, error);
}
});
ws.on('close', () => {
console.log(`🔌 Disconnected from relay ${relayId}`);
relay.isConnected = false;
this.scheduleReconnect(relayId);
});
ws.on('error', (error) => {
console.error(`❌ WebSocket error for relay ${relayId}:`, error);
relay.isConnected = false;
});
this.relays.set(relayId, relay);
// Wait for connection to establish
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
}, 5000);
ws.once('open', () => {
clearTimeout(timeout);
resolve(true);
});
});
} catch (error) {
console.error(`❌ Failed to connect to relay ${relayId}:`, error);
return false;
}
}
public disconnectFromRelay(relayId: string): void {
const relay = this.relays.get(relayId);
if (relay) {
relay.isConnected = false;
relay.ws.close();
this.relays.delete(relayId);
console.log(`🔌 Disconnected from relay ${relayId}`);
}
}
public getRelayById(relayId: string): RelayConnection | undefined {
return this.relays.get(relayId);
}
public getConnectedRelays(): RelayConnection[] {
return Array.from(this.relays.values()).filter(relay => relay.isConnected);
}
// Message Sending Methods using AnkFlag
public sendMessage(flag: AnkFlag, payload: any, targetRelayId?: string): void {
const msg: QueuedMessage = {
id: this.generateMessageId(),
flag,
payload,
targetRelayId,
timestamp: Date.now(),
expiresAt: Date.now() + 30000, // 30 seconds
retryCount: 0,
maxRetries: 3
};
this.queueMessage(msg);
}
public sendToRelay(relayId: string, flag: AnkFlag, payload: any): boolean {
const relay = this.relays.get(relayId);
if (!relay || !relay.isConnected) {
console.warn(`⚠️ Cannot send to relay ${relayId}: not connected`);
return false;
}
try {
const message = {
flag,
payload,
messageId: this.generateMessageId()
};
relay.ws.send(JSON.stringify(message));
return true;
} catch (error) {
console.error(`❌ Failed to send message to relay ${relayId}:`, error);
return false;
}
}
public broadcastToAllRelays(flag: AnkFlag, payload: any): number {
const connectedRelays = this.getConnectedRelays();
let sentCount = 0;
for (const relay of connectedRelays) {
if (this.sendToRelay(relay.id, flag, payload)) {
sentCount++;
}
}
console.log(`📡 Broadcasted to ${sentCount}/${connectedRelays.length} relays`);
return sentCount;
}
// Protocol-Specific Message Methods
public sendNewTxMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for new transaction
this.sendMessage("NewTx" as AnkFlag, message, targetRelayId);
}
public sendCommitMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for commit
this.sendMessage("Commit" as AnkFlag, message, targetRelayId);
}
public sendCipherMessages(ciphers: string[], targetRelayId?: string): void {
for (const cipher of ciphers) {
// Use appropriate AnkFlag for cipher
this.sendMessage("Cipher" as AnkFlag, cipher, targetRelayId);
}
}
public sendFaucetMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for faucet
this.sendMessage("Faucet" as AnkFlag, message, targetRelayId);
}
// Message Queue Management
private queueMessage(message: QueuedMessage): void {
this.messageQueue.push(message);
console.log(`📬 Queued message ${message.id} with flag ${message.flag}`);
}
private async processQueue(): Promise<void> {
if (this.processingQueue || this.messageQueue.length === 0) {
return;
}
this.processingQueue = true;
try {
const message = this.messageQueue.shift();
if (!message) {
return;
}
// Check if message has expired
if (message.expiresAt && Date.now() > message.expiresAt) {
console.warn(`⏰ Message ${message.id} expired`);
return;
}
await this.deliverMessage(message);
} finally {
this.processingQueue = false;
}
}
private async deliverMessage(message: QueuedMessage): Promise<void> {
try {
let delivered = false;
if (message.targetRelayId) {
// Send to specific relay
delivered = this.sendToRelay(message.targetRelayId, message.flag, message.payload);
} else {
// Broadcast to all connected relays
const sentCount = this.broadcastToAllRelays(message.flag, message.payload);
delivered = sentCount > 0;
}
if (!delivered) {
throw new Error('No suitable relay available');
}
console.log(`✅ Message ${message.id} delivered`);
} catch (error) {
console.error(`❌ Failed to deliver message ${message.id}:`, error);
message.retryCount++;
if (message.retryCount < message.maxRetries) {
// Re-queue with exponential backoff
setTimeout(() => {
this.queueMessage(message);
}, Math.pow(2, message.retryCount) * 1000);
} else {
console.error(`💀 Message ${message.id} failed after ${message.maxRetries} retries`);
}
}
}
// Relay Message Handling
private handleRelayMessage(relayId: string, message: any): void {
console.log(`📨 Received message from relay ${relayId}:`, message);
// Handle different types of relay responses
if (message.flag) {
// Handle protocol-specific responses
this.handleProtocolMessage(relayId, message);
} else if (message.type === 'heartbeat') {
// Update heartbeat
const relay = this.relays.get(relayId);
if (relay) {
relay.lastHeartbeat = Date.now();
}
}
}
private handleProtocolMessage(relayId: string, message: any): void {
// Handle different AnkFlag responses
switch (message.flag) {
case "NewTx":
console.log(`📨 NewTx response from relay ${relayId}`);
break;
case "Commit":
console.log(`📨 Commit response from relay ${relayId}`);
break;
case "Cipher":
console.log(`📨 Cipher response from relay ${relayId}`);
break;
case "Faucet":
console.log(`📨 Faucet response from relay ${relayId}`);
break;
default:
console.log(`📨 Unknown flag response from relay ${relayId}:`, message.flag);
}
}
// Reconnection Logic
private scheduleReconnect(relayId: string): void {
const relay = this.relays.get(relayId);
if (!relay || relay.reconnectAttempts >= relay.maxReconnectAttempts) {
console.log(`💀 Max reconnection attempts reached for relay ${relayId}`);
this.relays.delete(relayId);
return;
}
const delay = Math.pow(2, relay.reconnectAttempts) * 1000; // Exponential backoff
console.log(`🔄 Scheduling reconnect to relay ${relayId} in ${delay}ms (attempt ${relay.reconnectAttempts + 1})`);
setTimeout(async () => {
relay.reconnectAttempts++;
await this.connectToRelay(relayId, relay.url, relay.spAddress);
}, delay);
}
private startReconnectMonitoring(): void {
this.reconnectInterval = setInterval(() => {
// Check for disconnected relays and attempt reconnection
for (const [relayId, relay] of this.relays) {
if (!relay.isConnected && relay.reconnectAttempts < relay.maxReconnectAttempts) {
this.scheduleReconnect(relayId);
}
}
}, 10000); // Check every 10 seconds
}
// Heartbeat Management
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
const now = Date.now();
const heartbeatMessage = {
type: 'heartbeat',
timestamp: now
};
for (const [relayId, relay] of this.relays) {
if (relay.isConnected) {
try {
relay.ws.send(JSON.stringify(heartbeatMessage));
relay.lastHeartbeat = now;
} catch (error) {
console.error(`❌ Heartbeat failed for relay ${relayId}:`, error);
relay.isConnected = false;
}
}
}
}, 30000); // 30 seconds
}
private startQueueProcessing(): void {
this.queueProcessingInterval = setInterval(() => {
this.processQueue();
}, 100); // Process queue every 100ms
}
// Utility Methods
private generateMessageId(): string {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
public getStats(): any {
return {
totalRelays: this.relays.size,
connectedRelays: this.getConnectedRelays().length,
queuedMessages: this.messageQueue.length
};
}
public shutdown(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
if (this.queueProcessingInterval) {
clearInterval(this.queueProcessingInterval);
}
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
for (const [relayId] of this.relays) {
this.disconnectFromRelay(relayId);
}
console.log('🛑 Relay manager shutdown complete');
}
}

819
src/service.ts Normal file
View File

@ -0,0 +1,819 @@
// Simple server service with core protocol methods using WASM SDK
import Database from './database.service';
import * as wasm from '../pkg/sdk_client';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../pkg/sdk_client';
import { RelayManager } from './relay-manager';
const DEFAULTAMOUNT = 1000n;
export class Service {
private static instance: Service;
private processes: Map<string, any> = new Map();
private membersList: any = {};
private relayManager: RelayManager;
private storages: string[] = []; // storage urls
private constructor() {
console.log('🔧 Service initialized');
this.relayManager = RelayManager.getInstance();
this.initWasm();
}
private initWasm() {
try {
console.log('🔧 Initializing WASM SDK...');
wasm.setup();
console.log('✅ WASM SDK initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize WASM SDK:', error);
throw error;
}
}
static getInstance(): Service {
if (!Service.instance) {
Service.instance = new Service();
}
return Service.instance;
}
public async connectToRelays(): Promise<void> {
const relays = this.getAllRelays();
console.log(`🔗 Connecting to ${relays.length} relays...`);
for (const relay of relays) {
try {
const success = await this.relayManager.connectToRelay(
relay.spAddress, // Use spAddress as relay ID
relay.wsurl,
relay.spAddress
);
if (success) {
console.log(`✅ Connected to relay: ${relay.spAddress}`);
} else {
console.warn(`⚠️ Failed to connect to relay: ${relay.spAddress}`);
}
} catch (error) {
console.error(`❌ Error connecting to relay ${relay.spAddress}:`, error);
}
}
}
/**
* Get all connected relays from RelayManager.
* @returns An array of objects containing relay information.
*/
public getAllRelays(): { wsurl: string; spAddress: string }[] {
const connectedRelays = this.relayManager.getConnectedRelays();
return connectedRelays.map(relay => ({
wsurl: relay.url,
spAddress: relay.spAddress,
}));
}
/**
* Add a relay address to the RelayManager.
* @param relayId - Unique identifier for the relay
* @param wsUrl - WebSocket URL of the relay
* @param spAddress - Silent Payment address of the relay
*/
public async addRelayAddress(relayId: string, wsUrl: string, spAddress: string): Promise<boolean> {
try {
const success = await this.relayManager.connectToRelay(relayId, wsUrl, spAddress);
if (success) {
console.log(`✅ Added relay: ${relayId} at ${wsUrl}`);
} else {
console.warn(`⚠️ Failed to add relay: ${relayId}`);
}
return success;
} catch (error) {
console.error(`❌ Error adding relay ${relayId}:`, error);
return false;
}
}
/**
* Remove a relay from the RelayManager.
* @param relayId - Unique identifier for the relay to remove
*/
public removeRelayAddress(relayId: string): void {
this.relayManager.disconnectFromRelay(relayId);
console.log(`🔌 Removed relay: ${relayId}`);
}
/**
* Get relay statistics from RelayManager.
* @returns Statistics about connected relays
*/
public getRelayStats(): any {
return this.relayManager.getStats();
}
public async getSecretForAddress(address: string): Promise<string | null> {
const db = await Database.getInstance();
return await db.getObject('shared_secrets', address);
}
private async getTokensFromFaucet(): Promise<void> {
try {
await this.ensureSufficientAmount();
} catch (e) {
console.error('Failed to get tokens from relay, check connection');
return;
}
}
public getAllMembers(): Record<string, Member> {
return this.membersList;
}
public getAddressesForMemberId(memberId: string): string[] | null {
try {
return this.membersList[memberId].sp_addresses;
} catch (e) {
return null;
}
}
public async checkConnections(members: Member[]): Promise<void> {
// Ensure the amount is available before proceeding
await this.getTokensFromFaucet();
let unconnectedAddresses = [];
const myAddress = this.getDeviceAddress();
for (const member of members) {
const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue;
for (const address of sp_addresses) {
// For now, we ignore our own device address, although there might be use cases for having a secret with ourselves
if (address === myAddress) continue;
const sharedSecret = await this.getSecretForAddress(address);
if (!sharedSecret) {
unconnectedAddresses.push(address);
}
}
}
if (unconnectedAddresses && unconnectedAddresses.length != 0) {
const apiResult = await this.connectAddresses(unconnectedAddresses);
await this.handleApiReturn(apiResult);
}
}
public async connectAddresses(addresses: string[]): Promise<ApiReturn> {
if (addresses.length === 0) {
throw new Error('Trying to connect to empty addresses list');
}
try {
return wasm.create_transaction(addresses, 1);
} catch (e) {
console.error('Failed to connect member:', e);
throw e;
}
}
private async ensureSufficientAmount(): Promise<void> {
const availableAmt: BigInt = wasm.get_available_amount();
const target: BigInt = DEFAULTAMOUNT * BigInt(10);
if (availableAmt < target) {
try {
const faucetMsg = wasm.create_faucet_msg();
this.relayManager.sendFaucetMessage(faucetMsg);
} catch (e) {
throw new Error('Failed to create faucet message');
}
await this.waitForAmount(target);
}
}
private async waitForAmount(target: BigInt): Promise<BigInt> {
let attempts = 3;
while (attempts > 0) {
const amount: BigInt = wasm.get_available_amount();
if (amount >= target) {
return amount;
}
attempts--;
if (attempts > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second
}
}
throw new Error('Amount is still 0 after 3 attempts');
}
private isFileBlob(value: any): value is { type: string, data: Uint8Array } {
return (
typeof value === 'object' &&
value !== null &&
typeof value.type === 'string' &&
value.data instanceof Uint8Array
);
}
private splitData(obj: Record<string, any>) {
const jsonCompatibleData: Record<string, any> = {};
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
for (const [key, value] of Object.entries(obj)) {
if (this.isFileBlob(value)) {
binaryData[key] = value;
} else {
jsonCompatibleData[key] = value;
}
}
return { jsonCompatibleData, binaryData };
}
public async createNewDevice() {
try {
const spAddress = wasm.create_new_device(0, 'signet');
const device = wasm.dump_device();
await this.saveDeviceInDatabase(device);
return spAddress;
} catch (e) {
throw new Error(`Failed to create new device: ${e}`);
}
}
public async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) {
await db.deleteObject(walletStore, "1");
}
await db.addObject({
storeName: walletStore,
object: { pre_id: '1', device },
key: null,
});
} catch (e) {
console.error(e);
}
}
public getPairingProcessId(): string {
try {
return wasm.get_pairing_process_id();
} catch (e) {
throw new Error(`Failed to get pairing process: ${e}`);
}
}
public async createPairingProcess(userName: string, pairWith: string[]): Promise<ApiReturn> {
if (wasm.is_paired()) {
throw new Error('Device already paired');
}
const myAddress: string = wasm.get_address();
pairWith.push(myAddress);
const privateData = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName,
pairedAddresses: pairWith,
};
const validation_fields: string[] = [...Object.keys(privateData), ...Object.keys(publicData), 'roles'];
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: [
{
quorum: 1.0,
fields: validation_fields,
min_sig_member: 1.0,
},
],
storages: this.storages
},
};
try {
return this.createProcess(
privateData,
publicData,
roles
);
} catch (e) {
throw new Error(`Creating process failed:, ${e}`);
}
}
public async createProcess(
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, RoleDefinition>,
): Promise<ApiReturn> {
const relayAddress = this.getAllRelays()[0]['spAddress'];
const feeRate = 1;
// We can't encode files as the rest because Uint8Array is not valid json
// So we first take them apart and we will encode them separately and put them back in the right object
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...wasm.encode_json(privateSplitData.jsonCompatibleData),
...wasm.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...wasm.encode_json(publicSplitData.jsonCompatibleData),
...wasm.encode_binary(publicSplitData.binaryData)
};
let members: Set<Member> = new Set();
for (const role of Object.values(roles!)) {
for (const member of role.members) {
// Check if we know the member that matches this id
const memberAddresses = this.getAddressesForMemberId(member);
if (memberAddresses && memberAddresses.length != 0) {
members.add({ sp_addresses: memberAddresses });
}
}
}
await this.checkConnections([...members]);
const result = wasm.create_new_process (
encodedPrivateData,
roles,
encodedPublicData,
relayAddress,
feeRate,
this.getAllMembers()
);
return(result);
}
// Core protocol method: Create PRD Update
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
try {
// Get the process from cache
const process = this.processes.get(processId);
if (!process) {
throw new Error('Process not found');
}
// Find the state
const state = process.states.find((s: any) => s.state_id === stateId);
if (!state) {
throw new Error('State not found');
}
// Use WASM function to create update message
const result = wasm.create_update_message(process, stateId, this.membersList);
if (result.updated_process) {
// Update our cache
this.processes.set(processId, result.updated_process.current_process);
// Save to database
await this.saveProcessToDb(processId, result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to create update message');
}
} catch (error) {
throw new Error(`WASM error: ${error}`);
}
}
// Core protocol method: Approve Change (Validate State)
async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`✅ Approving change for process ${processId}, state ${stateId}`);
try {
// Get the process from cache
const process = this.processes.get(processId);
if (!process) {
throw new Error('Process not found');
}
// Find the state
const state = process.states.find((s: any) => s.state_id === stateId);
if (!state) {
throw new Error('State not found');
}
// Use WASM function to validate state
const result = wasm.validate_state(process, stateId, this.membersList);
if (result.updated_process) {
// Update our cache
this.processes.set(processId, result.updated_process.current_process);
// Save to database
await this.saveProcessToDb(processId, result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to validate state');
}
} catch (error) {
throw new Error(`WASM error: ${error}`);
}
}
// Core protocol method: Update Process
async updateProcess(
process: any,
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, any> | null
): Promise<ApiReturn> {
console.log(`🔄 Updating process ${process.states[0]?.state_id || 'unknown'}`);
console.log('Private data:', privateData);
console.log('Public data:', publicData);
console.log('Roles:', roles);
try {
// Convert data to WASM format
const newAttributes = wasm.encode_json(privateData);
const newPublicData = wasm.encode_json(publicData);
const newRoles = roles || process.states[0]?.roles || {};
// Use WASM function to update process
const result = wasm.update_process(process, newAttributes, newRoles, newPublicData, this.membersList);
if (result.updated_process) {
// Update our cache
this.processes.set(process.states[0]?.state_id || 'unknown', result.updated_process.current_process);
// Save to database
await this.saveProcessToDb(result.updated_process.process_id, result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to update process');
}
} catch (error) {
throw new Error(`WASM error: ${error}`);
}
}
// Utility method: Get Process
async getProcess(processId: string): Promise<any | null> {
// First check in-memory cache
const cachedProcess = this.processes.get(processId);
if (cachedProcess) {
return cachedProcess;
}
// If not in cache, try to get from database
try {
const db = await Database.getInstance();
const dbProcess = await db.getObject('processes', processId);
if (dbProcess) {
// Cache it for future use
this.processes.set(processId, dbProcess);
return dbProcess;
}
} catch (error) {
console.error('Error getting process from database:', error);
}
return null;
}
// Database method: Save Process
async saveProcessToDb(processId: string, process: any): Promise<void> {
try {
const db = await Database.getInstance();
await db.addObject({
storeName: 'processes',
object: process,
key: processId
});
// Update in-memory cache
this.processes.set(processId, process);
console.log(`💾 Process ${processId} saved to database`);
} catch (error) {
console.error('Error saving process to database:', error);
throw error;
}
}
// Database method: Get All Processes
async getAllProcesses(): Promise<Record<string, any>> {
try {
const db = await Database.getInstance();
const processes = await db.dumpStore('processes');
// Update in-memory cache with all processes
for (const [processId, process] of Object.entries(processes)) {
this.processes.set(processId, process);
}
return processes;
} catch (error) {
console.error('Error getting all processes from database:', error);
return {};
}
}
// Utility method: Create a test process
async createTestProcess(processId: string): Promise<any> {
console.log(`🔧 Creating test process: ${processId}`);
try {
// Create test data
const privateData = wasm.encode_json({ secret: 'initial_secret' });
const publicData = wasm.encode_json({ name: 'Test Process', created: Date.now() });
const roles = { admin: { members: [], validation_rules: [], storages: [] } };
const relayAddress = 'test_relay_address';
const feeRate = 1;
// Use WASM to create new process
const result = wasm.create_new_process(privateData, roles, publicData, relayAddress, feeRate, this.membersList);
if (result.updated_process) {
const process = result.updated_process.current_process;
this.processes.set(processId, process);
// Save to database
await this.saveProcessToDb(processId, process);
console.log(`✅ Test process created: ${processId}`);
return process;
} else {
throw new Error('Failed to create test process');
}
} catch (error) {
console.error('Error creating test process:', error);
throw error;
}
}
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, '1');
if (dbRes) {
return dbRes['device'];
} else {
return null;
}
} catch (e) {
throw new Error(`Failed to retrieve device from db: ${e}`);
}
}
public async restoreDeviceFromDatabase(device: Device): Promise<void> {
try {
wasm.restore_device(device);
console.log('✅ Device restored in WASM successfully');
} catch (e) {
throw new Error(`Failed to restore device in WASM: ${e}`);
}
}
public isPaired(): boolean {
try {
return wasm.is_paired();
} catch (error) {
console.error('Error checking if paired:', error);
throw error;
}
}
public getLastCommitedState(process: Process): ProcessState | null {
const index = this.getLastCommitedStateIndex(process);
if (index === null) return null;
return process.states[index];
}
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) {
return i;
}
}
return null;
}
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
us = wasm.get_pairing_process_id();
} catch (e) {
throw e;
}
return this.rolesContainsMember(roles, us);
}
public rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
for (const roleDef of Object.values(roles)) {
if (roleDef.members.includes(pairingProcessId)) {
return true;
}
}
return false;
}
// Utility method: Add member to the members list
addMember(outpoint: string, member: any) {
this.membersList[outpoint] = member;
}
// Utility method: Get device address
getDeviceAddress(): string {
try {
return wasm.get_address();
} catch (error) {
console.error('Error getting device address:', error);
throw error;
}
}
// WebSocket message methods using Relay Manager
async sendNewTxMessage(message: string) {
console.log('📤 Sending NewTx message:', message);
this.relayManager.sendNewTxMessage(message);
}
async sendCommitMessage(message: string) {
console.log('📤 Sending Commit message:', message);
this.relayManager.sendCommitMessage(message);
}
async sendCipherMessages(ciphers: string[]) {
console.log('📤 Sending Cipher messages:', ciphers.length, 'ciphers');
this.relayManager.sendCipherMessages(ciphers);
}
// Blob and data storage methods
async saveBlobToDb(hash: string, data: Blob) {
const db = await Database.getInstance();
try {
await db.addObject({
storeName: 'data',
object: data,
key: hash,
});
} catch (e) {
console.error(`Failed to save data to db: ${e}`);
}
}
async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try {
return await db.getObject('data', hash);
} catch (e) {
return null;
}
}
async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
console.log('💾 Saving data to storage:', hash);
// TODO: Implement actual storage service
// const storages = [STORAGEURL];
// try {
// await storeData(storages, hash, data, ttl);
// } catch (e) {
// console.error(`Failed to store data with hash ${hash}: ${e}`);
// }
}
async saveDiffsToDb(diffs: any[]) {
const db = await Database.getInstance();
try {
for (const diff of diffs) {
await db.addObject({
storeName: 'diffs',
object: diff,
key: null,
});
}
} catch (e) {
throw new Error(`Failed to save diffs: ${e}`);
}
}
// Utility methods for data conversion
hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString);
return new Blob([uint8Array], { type: "application/octet-stream" });
}
hexToUInt8Array(hexString: string): Uint8Array {
if (hexString.length % 2 !== 0) {
throw new Error("Invalid hex string: length must be even");
}
const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return uint8Array;
}
public async handleApiReturn(apiReturn: ApiReturn) {
console.log('🔄 Handling API return:', apiReturn);
if (apiReturn.partial_tx) {
try {
const res = wasm.sign_transaction(apiReturn.partial_tx);
apiReturn.new_tx_to_send = res.new_tx_to_send;
} catch (e) {
console.error('Failed to sign transaction:', e);
}
}
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500));
}
if (apiReturn.secrets) {
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
const confirmedSecrets = apiReturn.secrets.shared_secrets;
const db = await Database.getInstance();
for (const secret of unconfirmedSecrets) {
await db.addObject({
storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
}
const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value }));
for (const entry of entries) {
try {
await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
} catch (e) {
throw e;
}
}
}
if (apiReturn.updated_process) {
const updatedProcess = apiReturn.updated_process;
const processId: string = updatedProcess.process_id;
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
const blob = this.hexToBlob(cipher);
try {
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(e);
}
}
}
// Save process to db
await this.saveProcessToDb(processId, updatedProcess.current_process);
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
try {
await this.saveDiffsToDb(updatedProcess.diffs);
} catch (e) {
console.error('Failed to save diffs to db:', e);
}
}
}
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
for (const hash of apiReturn.push_to_storage) {
const blob = await this.getBlobFromDb(hash);
if (blob) {
await this.saveDataToStorage(hash, blob, null);
} else {
console.error('Failed to get data from db');
}
}
}
if (apiReturn.commit_to_send) {
const commit = apiReturn.commit_to_send;
await this.sendCommitMessage(JSON.stringify(commit));
}
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
await this.sendCipherMessages(apiReturn.ciphers_to_send);
}
}
}

View File

@ -1,909 +0,0 @@
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;

372
src/simple-server.ts Normal file
View File

@ -0,0 +1,372 @@
import WebSocket from 'ws';
import { MessageType } from './models';
import { config } from './config';
import { Service } from './service';
import { ApiReturn, Process } from '../pkg/sdk_client';
import { EMPTY32BYTES } from './utils';
interface ServerMessageEvent {
data: {
type: MessageType;
messageId?: string;
apiKey?: string;
[key: string]: any;
};
clientId: string;
}
interface ServerResponse {
type: MessageType;
messageId?: string;
[key: string]: any;
}
class SimpleProcessHandlers {
private apiKey: string;
private service: Service;
constructor(apiKey: string, service: Service) {
this.apiKey = apiKey;
this.service = service;
}
private errorResponse = (errorMsg: string, clientId: string, messageId?: string): ServerResponse => {
return {
type: MessageType.ERROR,
error: errorMsg,
messageId
};
};
private validateApiKey(apiKey: string): boolean {
return apiKey === this.apiKey;
}
async handleNotifyUpdate(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.NOTIFY_UPDATE) {
throw new Error('Invalid message type');
}
try {
const { processId, stateId, apiKey } = event.data;
if (!apiKey || !this.validateApiKey(apiKey)) {
throw new Error('Invalid API key');
}
// Check if device is paired
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
let res: ApiReturn;
try {
res = await this.service.createPrdUpdate(processId, stateId);
await this.service.handleApiReturn(res);
} catch (e) {
throw new Error(e as string);
}
return {
type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId
};
} catch (e) {
const errorMsg = `Failed to notify update for process: ${e}`;
throw new Error(errorMsg);
}
}
async handleValidateState(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.VALIDATE_STATE) {
throw new Error('Invalid message type');
}
try {
const { processId, stateId, apiKey } = event.data;
if (!apiKey || !this.validateApiKey(apiKey)) {
throw new Error('Invalid API key');
}
// Check if device is paired
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
let res: ApiReturn;
try {
res = await this.service.approveChange(processId, stateId);
await this.service.handleApiReturn(res);
} catch (e) {
throw new Error(e as string);
}
return {
type: MessageType.STATE_VALIDATED,
validatedProcess: res.updated_process,
messageId: event.data.messageId
};
} catch (e) {
const errorMsg = `Failed to validate process: ${e}`;
throw new Error(errorMsg);
}
}
async handleUpdateProcess(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.UPDATE_PROCESS) {
throw new Error('Invalid message type');
}
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
try {
// privateFields is only used if newData contains new fields
// roles can be empty meaning that roles from the last commited state are kept
const { processId, newData, privateFields, roles, apiKey } = event.data;
if (!apiKey || !this.validateApiKey(apiKey)) {
throw new Error('Invalid API key');
}
// Check if the new data is already in the process or if it's a new field
const process = await this.service.getProcess(processId);
if (!process) {
throw new Error('Process not found');
}
const lastState = this.service.getLastCommitedState(process);
if (!lastState) {
throw new Error('Process doesn\'t have a commited state yet');
}
const lastStateIndex = this.service.getLastCommitedStateIndex(process);
if (lastStateIndex === null) {
throw new Error('Process doesn\'t have a commited state yet');
}
const privateData: Record<string, any> = {};
const publicData: Record<string, any> = {};
for (const field of Object.keys(newData)) {
// Public data are carried along each new state
// TODO I hope that at some point we stop doing that
// So the first thing we can do is check if the new data is public data
if (lastState.public_data[field]) {
// Add it to public data
publicData[field] = newData[field];
continue;
}
// If it's not a public data, it may be either a private data update, or a new field (public of private)
// Caller gave us a list of new private fields, if we see it here this is a new private field
if (privateFields.includes(field)) {
// Add it to private data
privateData[field] = newData[field];
continue;
}
// Now it can be an update of private data or a new public data
// We check that the field exists in previous states private data
for (let i = lastStateIndex; i >= 0; i--) {
const state = process.states[i];
if (state.pcd_commitment[field]) {
// We don't even check if it's a public field, we would have seen it in the last state
// TODO maybe that's an issue if we remove a public field at some point? That's not explicitly forbidden but not really supported yet
privateData[field] = newData[field];
break;
} else {
// This attribute was not modified in that state, we go back to the previous state
continue;
}
}
if (privateData[field]) continue;
// We've get back all the way to the first state without seeing it, it's a new public field
publicData[field] = newData[field];
}
// We'll let the wasm check if roles are consistent
const res = await this.service.updateProcess(process, privateData, publicData, roles);
await this.service.handleApiReturn(res);
return {
type: MessageType.PROCESS_UPDATED,
updatedProcess: res.updated_process,
messageId: event.data.messageId
};
} catch (e) {
const errorMsg = `Failed to update process: ${e}`;
throw new Error(errorMsg);
}
}
async handleMessage(event: ServerMessageEvent): Promise<ServerResponse > {
try {
switch (event.data.type) {
case MessageType.NOTIFY_UPDATE:
return await this.handleNotifyUpdate(event);
case MessageType.VALIDATE_STATE:
return await this.handleValidateState(event);
case MessageType.UPDATE_PROCESS:
return await this.handleUpdateProcess(event);
default:
throw new Error(`Unhandled message type: ${event.data.type}`);
}
} catch (error) {
return this.errorResponse(error as string, event.clientId, event.data.messageId);
}
}
}
export class Server {
private wss: WebSocket.Server;
private handlers!: SimpleProcessHandlers;
private clients: Map<WebSocket, string> = new Map();
constructor(port: number = 8080) {
this.wss = new WebSocket.Server({ port });
this.init();
}
private async init() {
try {
console.log('🚀 Initializing Simple 4NK Protocol Server...');
// Initialize service
const service = Service.getInstance();
// Initialize handlers with API key and service
this.handlers = new SimpleProcessHandlers(config.apiKey, service);
// Setup WebSocket handlers
this.setupWebSocketHandlers();
// Check if we have a device
const device = await service.getDeviceFromDatabase();
if (!device) {
const spAddress = await service.createNewDevice();
console.log('🔑 New device created:', spAddress);
} else {
console.log('🔑 Device found, restoring from database...');
await service.restoreDeviceFromDatabase(device);
console.log('🔑 Device restored successfully');
}
// Check if we are paired
if (!service.isPaired()) {
console.log('🔑 Not paired, creating pairing process...');
try {
const pairingResult = await service.createPairingProcess('', []);
console.log('🔑 Pairing process created successfully');
} catch (error) {
console.error('❌ Failed to create pairing process:', error);
}
} else {
console.log('🔑 Already paired with id:', service.getPairingProcessId());
}
// Connect to relays
await service.connectToRelays();
console.log(`✅ Simple server running on port ${this.wss.options.port}`);
console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE');
console.log('🔑 Authentication: API key required for all operations');
console.log('🔧 Services: Integrated with SimpleService protocol logic');
} catch (error) {
console.error('❌ Failed to initialize server:', error);
process.exit(1);
}
}
private setupWebSocketHandlers() {
this.wss.on('connection', (ws: WebSocket, req) => {
const clientId = this.generateClientId();
this.clients.set(ws, clientId);
console.log(`🔗 Client connected: ${clientId} from ${req.socket.remoteAddress}`);
// Send listening message
this.sendToClient(ws, {
type: MessageType.LISTENING,
clientId
});
ws.on('message', async (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString());
console.log(`📨 Received message from ${clientId}:`, message.type);
const serverEvent: ServerMessageEvent = {
data: message,
clientId
};
const response = await this.handlers.handleMessage(serverEvent);
this.sendToClient(ws, response);
} catch (error) {
console.error(`❌ Error handling message from ${clientId}:`, error);
this.sendToClient(ws, {
type: MessageType.ERROR,
error: `Server error: ${error instanceof Error ? error.message : String(error)}`,
messageId: JSON.parse(data.toString())?.messageId
});
}
});
ws.on('close', () => {
console.log(`🔌 Client disconnected: ${clientId}`);
this.clients.delete(ws);
});
ws.on('error', (error) => {
console.error(`❌ WebSocket error for ${clientId}:`, error);
this.clients.delete(ws);
});
});
this.wss.on('error', (error) => {
console.error('❌ WebSocket server error:', error);
});
}
private sendToClient(ws: WebSocket, response: ServerResponse) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(response));
}
}
private generateClientId(): string {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
public shutdown() {
console.log('🛑 Shutting down server...');
this.wss.close(() => {
console.log('✅ Server shutdown complete');
process.exit(0);
});
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Received SIGINT, shutting down gracefully...');
if (server) {
server.shutdown();
}
});
process.on('SIGTERM', () => {
console.log('\n🛑 Received SIGTERM, shutting down gracefully...');
if (server) {
server.shutdown();
}
});
// Start the server
const port = parseInt(process.env.PORT || '8080');
const server = new Server(port);

View File

@ -1,170 +0,0 @@
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;
}

8
src/utils.ts Normal file
View File

@ -0,0 +1,8 @@
// Server-specific utility functions
export const EMPTY32BYTES = String('').padStart(64, '0');
export function isValid32ByteHex(value: string): boolean {
// Check if the value is a valid 32-byte hex string (64 characters)
const hexRegex = /^[0-9a-fA-F]{64}$/;
return hexRegex.test(value);
}

View File

@ -1,117 +0,0 @@
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 };

39
test-server.js Normal file
View File

@ -0,0 +1,39 @@
const WebSocket = require('ws');
console.log('🔍 Testing WebSocket server connection...');
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', function open() {
console.log('✅ Connected to server!');
// Send a simple test message
const testMessage = {
type: 'LISTENING',
messageId: 'test-123'
};
ws.send(JSON.stringify(testMessage));
});
ws.on('message', function message(data) {
console.log('📨 Received from server:', data.toString());
// Close connection after receiving response
setTimeout(() => {
ws.close();
console.log('🔌 Connection closed');
process.exit(0);
}, 1000);
});
ws.on('error', function error(err) {
console.error('❌ Connection error:', err.message);
process.exit(1);
});
// Timeout after 5 seconds
setTimeout(() => {
console.error('❌ Connection timeout');
process.exit(1);
}, 5000);

View File

@ -1,110 +1,25 @@
{
"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. */
"outDir": "./dist",
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"lib": ["ES2020"],
"declaration": false,
"sourceMap": true
},
"include": ["./src/**/*"]
}
"include": [
"src/**/*",
"pkg/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -1,46 +0,0 @@
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',
},
};