Compare commits
No commits in common. "main" and "master" have entirely different histories.
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,6 +1,4 @@
|
||||
target/
|
||||
pkg/
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode
|
||||
node_modules
|
||||
pkg
|
||||
dist
|
||||
data
|
@ -1,5 +0,0 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/sp_client"
|
||||
]
|
93
README.md
93
README.md
@ -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
171
SERVER_README.md
Normal 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
|
||||
```
|
@ -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
@ -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)
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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>,
|
||||
}
|
@ -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
|
||||
}
|
@ -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
4819
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file → Executable file
24
package.json
Normal file → Executable 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
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
19
src/config.ts
Normal 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
276
src/database.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
178
src/database.ts
178
src/database.ts
@ -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;
|
@ -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>
|
27
src/index.ts
27
src/index.ts
@ -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
60
src/models.ts
Normal 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
96
src/relay-example.ts
Normal 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
396
src/relay-manager.ts
Normal 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
819
src/service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
909
src/services.ts
909
src/services.ts
@ -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
372
src/simple-server.ts
Normal 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);
|
@ -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
8
src/utils.ts
Normal 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);
|
||||
}
|
@ -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
39
test-server.js
Normal 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);
|
127
tsconfig.json
127
tsconfig.json
@ -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"
|
||||
]
|
||||
}
|
@ -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',
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user