sdk_client/src/services.ts
2024-04-17 09:17:13 +02:00

639 lines
24 KiB
TypeScript

import { createUserReturn, User, Process } 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 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 createId(event: Event): Promise<void> {
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;
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;
const services = await Services.getInstance();
let createUserReturn: createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
let user = createUserReturn.user;
const shares = user.shares;
// send the shares on the network
const revokeData = user.revoke_data;
if (!revokeData) {
console.error('Failed to get revoke data from wasm');
return;
}
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) {
console.error("Failed to write user object :", error);
}
let sp_address = "";
try {
sp_address = services.sdkClient.get_receiving_address(user.pre_id);
console.info('Using sp_address:', sp_address);
} catch (error) {
console.error(error);
}
await services.obtainTokenWithFaucet(sp_address);
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();
console.log("JS recover submit ");
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;
// TODO
alert("Recover submit to do ...");
}
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);
}
}
}
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): Promise<parseNetworkMsgReturn | null> {
const services = await Services.getInstance();
try {
const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw);
return msg;
} catch (error) {
console.error(error);
return null;
}
}
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 checkTransaction(tx: string, tweak_data: string, blkheight: number): Promise<string | null> {
const services = await Services.getInstance();
try {
const updated_user: string = services.sdkClient.check_transaction_for_silent_payments(tx, blkheight, tweak_data);
await services.updateOwnedOutputsForUser(updated_user);
return updated_user;
} catch (error) {
console.error(error);
return null;
}
}
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 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(spaddress: string): Promise<string | null> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
return null;
}
try {
const flag: AnkFlag = "Faucet";
const faucetMsg: FaucetMessage = {
'sp_address': spaddress
}
connection.sendMessage(flag, JSON.stringify(faucetMsg));
} catch (error) {
console.error("Failed to obtain tokens with relay ", connection.getUrl());
return null;
}
return null;
}
}
export default Services;