1280 lines
39 KiB
TypeScript
Executable File
1280 lines
39 KiB
TypeScript
Executable File
// import { WebSocketClient } from '../websockets';
|
|
import { INotification } from '~/models/notification.model';
|
|
import { IProcess } from '~/models/process.model';
|
|
// import Database from './database';
|
|
import { initWebsocket, sendMessage } from '../websockets';
|
|
import { ApiReturn, Device, HandshakeMessage, Member, Process, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
|
|
import ModalService from './modal.service';
|
|
import Database from './database.service';
|
|
import { storeData, retrieveData } from './storage.service';
|
|
import { BackUp } from '~/models/backup.model';
|
|
|
|
export const U32_MAX = 4294967295;
|
|
|
|
const storageUrl = `/storage`;
|
|
const BOOTSTRAPURL = [`https://demo.4nkweb.com/ws/`];
|
|
const DEFAULTAMOUNT = 1000n;
|
|
|
|
export default class Services {
|
|
private static initializing: Promise<Services> | null = null;
|
|
private static instance: Services;
|
|
private currentProcess: string | null = null;
|
|
private pendingUpdates: any | null = null;
|
|
private currentUpdateMerkleRoot: string | null = null;
|
|
private localAddress: string | null = null;
|
|
private pairedAddresses: string[] = [];
|
|
private sdkClient: any;
|
|
private processes: IProcess[] | null = null;
|
|
private notifications: any[] | null = null;
|
|
private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
|
|
private database: any;
|
|
private routingInstance!: ModalService;
|
|
private relayAddresses: { [wsurl: string]: string } = {};
|
|
private membersList: Record<string, Member> = {};
|
|
// 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) {
|
|
return Services.instance;
|
|
}
|
|
|
|
if (!Services.initializing) {
|
|
Services.initializing = (async () => {
|
|
const instance = new Services();
|
|
await instance.init();
|
|
instance.routingInstance = await ModalService.getInstance();
|
|
return instance;
|
|
})();
|
|
}
|
|
|
|
console.log('initializing services');
|
|
Services.instance = await Services.initializing;
|
|
Services.initializing = null; // Reset for potential future use
|
|
return Services.instance;
|
|
}
|
|
|
|
public async init(): Promise<void> {
|
|
this.notifications = this.getNotifications();
|
|
this.sdkClient = await import('../../pkg/sdk_client');
|
|
this.sdkClient.setup();
|
|
for (const wsurl of Object.values(BOOTSTRAPURL)) {
|
|
this.updateRelay(wsurl, '');
|
|
}
|
|
await this.connectAllRelays();
|
|
}
|
|
|
|
/**
|
|
* Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses.
|
|
*/
|
|
public async connectAllRelays(): Promise<void> {
|
|
for (const wsurl of Object.keys(this.relayAddresses)) {
|
|
try {
|
|
console.log(`Connecting to: ${wsurl}`);
|
|
await this.addWebsocketConnection(wsurl);
|
|
console.log(`Successfully connected to: ${wsurl}`);
|
|
} catch (error) {
|
|
console.error(`Failed to connect to ${wsurl}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async addWebsocketConnection(url: string): Promise<void> {
|
|
console.log('Opening new websocket connection');
|
|
await initWebsocket(url);
|
|
}
|
|
|
|
/**
|
|
* Add or update a key/value pair in relayAddresses.
|
|
* @param wsurl - The WebSocket URL (key).
|
|
* @param spAddress - The SP Address (value).
|
|
*/
|
|
public updateRelay(wsurl: string, spAddress: string): void {
|
|
this.relayAddresses[wsurl] = spAddress;
|
|
console.log(`Updated: ${wsurl} -> ${spAddress}`);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the spAddress for a given wsurl.
|
|
* @param wsurl - The WebSocket URL to look up.
|
|
* @returns The SP Address if found, or undefined if not.
|
|
*/
|
|
public getSpAddress(wsurl: string): string | undefined {
|
|
return this.relayAddresses[wsurl];
|
|
}
|
|
|
|
/**
|
|
* Get all key/value pairs from relayAddresses.
|
|
* @returns An array of objects containing wsurl and spAddress.
|
|
*/
|
|
public getAllRelays(): { wsurl: string; spAddress: string }[] {
|
|
return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({
|
|
wsurl,
|
|
spAddress,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Print all key/value pairs for debugging.
|
|
*/
|
|
public printAllRelays(): void {
|
|
console.log("Current relay addresses:");
|
|
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
|
|
console.log(`${wsurl} -> ${spAddress}`);
|
|
}
|
|
}
|
|
|
|
public isPaired(): boolean {
|
|
try {
|
|
return this.sdkClient.is_paired();
|
|
} catch (e) {
|
|
throw new Error(`isPaired ~ Error: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async unpairDevice(): Promise<void> {
|
|
try {
|
|
this.sdkClient.unpair_device();
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
} catch (e) {
|
|
throw new Error(`Failed to unpair device: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async getSecretForAddress(address: string): Promise<string | null> {
|
|
const db = await Database.getInstance();
|
|
return await db.getObject('shared_secrets', address);
|
|
}
|
|
|
|
public async getAllSecrets(): Promise<SecretsStore> {
|
|
const db = await Database.getInstance();
|
|
const sharedSecrets = await db.dumpStore('shared_secrets');
|
|
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values
|
|
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
|
};
|
|
|
|
return secretsStore;
|
|
}
|
|
|
|
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
|
|
const db = await Database.getInstance();
|
|
return await db.dumpStore('diffs');
|
|
}
|
|
|
|
public async getDiffByValue(value: string): Promise<UserDiff | null> {
|
|
const db = await Database.getInstance();
|
|
const store = 'diffs';
|
|
const res = await db.getObject(store, value);
|
|
return res;
|
|
}
|
|
|
|
public async checkConnections(members: Member[]): Promise<void> {
|
|
// Ensure the amount is available before proceeding
|
|
try {
|
|
await this.ensureSufficientAmount();
|
|
} catch (e) {
|
|
console.error('Failed to get tokens from relay, check connection');
|
|
return;
|
|
}
|
|
let unconnectedAddresses = [];
|
|
for (const member of members) {
|
|
for (const address of member.sp_addresses) {
|
|
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 this.sdkClient.create_connect_transaction(addresses, 1);
|
|
} catch (e) {
|
|
console.error('Failed to connect member:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private async ensureSufficientAmount(): Promise<void> {
|
|
let availableAmt = this.getAmount();
|
|
const target: BigInt = DEFAULTAMOUNT * BigInt(2);
|
|
|
|
if (availableAmt < target) {
|
|
const faucetMsg = this.createFaucetMessage();
|
|
this.sendFaucetMessage(faucetMsg);
|
|
|
|
await this.waitForAmount(target);
|
|
}
|
|
}
|
|
|
|
private async waitForAmount(target: BigInt): Promise<BigInt> {
|
|
let attempts = 3;
|
|
|
|
while (attempts > 0) {
|
|
const amount = this.getAmount();
|
|
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');
|
|
}
|
|
|
|
public async createMessagingProcess(otherMembers: Member[],relayAddress: string, feeRate: number): Promise<ApiReturn> {
|
|
if (!this.isPaired()) {
|
|
throw new Error('Device not paired');
|
|
}
|
|
const me = await this.getMemberFromDevice();
|
|
if (!me) {
|
|
throw new Error('No paired member in device');
|
|
}
|
|
const allMembers: Member[] = otherMembers;
|
|
const meAndOne = [{ sp_addresses: me }, otherMembers.pop()!];
|
|
allMembers.push({ sp_addresses: me });
|
|
const everyOneElse = otherMembers;
|
|
const messagingTemplate = {
|
|
description: 'messaging',
|
|
roles: {
|
|
public: {
|
|
members: allMembers,
|
|
validation_rules: [
|
|
{
|
|
quorum: 0.0,
|
|
fields: ['description', 'roles'],
|
|
min_sig_member: 0.0,
|
|
},
|
|
],
|
|
storages: [storageUrl]
|
|
},
|
|
owner: {
|
|
members: meAndOne,
|
|
validation_rules: [
|
|
{
|
|
quorum: 1.0,
|
|
fields: ['description', 'roles'],
|
|
min_sig_member: 1.0,
|
|
},
|
|
],
|
|
storages: [storageUrl]
|
|
},
|
|
users: {
|
|
members: everyOneElse,
|
|
validation_rules: [
|
|
{
|
|
quorum: 0.0,
|
|
fields: ['description', 'roles'],
|
|
min_sig_member: 0.0,
|
|
},
|
|
],
|
|
storages: [storageUrl]
|
|
},
|
|
},
|
|
};
|
|
try {
|
|
return this.sdkClient.create_new_process(JSON.stringify(messagingTemplate), null, relayAddress, feeRate);
|
|
} catch (e) {
|
|
throw new Error(`Creating process failed: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise<ApiReturn> {
|
|
if (this.sdkClient.is_paired()) {
|
|
throw new Error('Device already paired');
|
|
}
|
|
const myAddress: string = this.sdkClient.get_address();
|
|
pairWith.push(myAddress);
|
|
const newKey = this.sdkClient.get_new_keypair();
|
|
const pairingTemplate = {
|
|
description: 'pairing',
|
|
roles: {
|
|
owner: {
|
|
members: [{ sp_addresses: pairWith }],
|
|
validation_rules: [
|
|
{
|
|
quorum: 1.0,
|
|
fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'],
|
|
min_sig_member: 1.0,
|
|
},
|
|
],
|
|
storages: [storageUrl]
|
|
},
|
|
},
|
|
session_privkey: newKey['private_key'],
|
|
session_pubkey: newKey['x_only_public_key'],
|
|
key_parity: newKey['key_parity'],
|
|
};
|
|
try {
|
|
return this.sdkClient.create_new_process(JSON.stringify(pairingTemplate), null, relayAddress, feeRate);
|
|
} catch (e) {
|
|
throw new Error(`Creating process failed:, ${e}`);
|
|
}
|
|
}
|
|
|
|
public async createDmProcess(
|
|
otherMember: string[],
|
|
): Promise<ApiReturn> {
|
|
if (otherMember.length === 0) {
|
|
throw new Error('Can\'t open dm with empty user');
|
|
}
|
|
try {
|
|
console.log('🚀 Début createDmProcess');
|
|
console.log('👥 Other Member:', otherMember);
|
|
|
|
if (!this.isPaired()) {
|
|
throw new Error('Device not paired');
|
|
}
|
|
|
|
const myAddresses = await this.getMemberFromDevice();
|
|
console.log('🔑 Mes adresses:', myAddresses);
|
|
|
|
if (!myAddresses) {
|
|
throw new Error('No paired member found');
|
|
}
|
|
|
|
const dmTemplate = {
|
|
description: 'dm',
|
|
message: '',
|
|
roles: {
|
|
dm: {
|
|
members: [
|
|
{ sp_addresses: myAddresses },
|
|
{ sp_addresses: otherMember }
|
|
],
|
|
validation_rules: [
|
|
{
|
|
quorum: 0.01,
|
|
fields: ['message', 'description', 'roles'],
|
|
min_sig_member: 0.01,
|
|
},
|
|
],
|
|
storages: [storageUrl]
|
|
}
|
|
}
|
|
};
|
|
|
|
console.log('📋 Template final:', JSON.stringify(dmTemplate, null, 2));
|
|
|
|
const relayAddress = this.getAllRelays()[0]['spAddress'];
|
|
const feeRate = 1;
|
|
const initState = JSON.stringify(dmTemplate);
|
|
|
|
await this.checkConnections ([{ sp_addresses: otherMember}]);
|
|
|
|
const result = this.sdkClient.create_new_process(
|
|
initState,
|
|
null,
|
|
relayAddress,
|
|
feeRate
|
|
);
|
|
|
|
return result;
|
|
|
|
} catch (e) {
|
|
console.error('❌ Erreur:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public async updateProcess(processId: string, new_state: any): Promise<ApiReturn> {
|
|
const roles = new_state.roles;
|
|
if (!roles) {
|
|
throw new Error('new state doesn\'t contain roles');
|
|
}
|
|
|
|
let members = new Set();
|
|
for (const role of Object.values(roles)) {
|
|
for (const member of role.members) {
|
|
members.add(member)
|
|
}
|
|
}
|
|
console.log(members);
|
|
await this.checkConnections([...members]);
|
|
try {
|
|
return this.sdkClient.update_process(processId, JSON.stringify(new_state));
|
|
} catch (e) {
|
|
throw new Error(`Failed to update process: ${e}`);
|
|
}
|
|
}
|
|
|
|
// Create prd update for current process and update
|
|
public createPrdUpdate(processId: string, stateId: string): ApiReturn {
|
|
try {
|
|
return this.sdkClient.create_update_message(processId, stateId);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd update: ${e}`);
|
|
}
|
|
}
|
|
|
|
public createPrdResponse(pcdMerkleRoot: string): ApiReturn {
|
|
if (!this.currentProcess) {
|
|
throw new Error('No current process defined');
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.create_response_prd(this.currentProcess, pcdMerkleRoot);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public approveChange(processId: string, stateId: string): ApiReturn {
|
|
try {
|
|
return this.sdkClient.validate_state(processId, stateId);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
|
|
public rejectChange(): ApiReturn {
|
|
if (!this.currentProcess || !this.currentUpdateMerkleRoot) {
|
|
throw new Error('No current process and/or current update defined');
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.refuse_state(this.currentProcess, this.currentUpdateMerkleRoot);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
|
|
async resetDevice() {
|
|
this.sdkClient.reset_device();
|
|
|
|
// Clear all stores
|
|
const db = await Database.getInstance();
|
|
await db.clearStore('wallet');
|
|
await db.clearStore('shared_secrets');
|
|
await db.clearStore('unconfirmed_secrets');
|
|
await db.clearStore('processes');
|
|
await db.clearStore('diffs');
|
|
}
|
|
|
|
async sendNewTxMessage(message: string) {
|
|
sendMessage('NewTx', message);
|
|
}
|
|
|
|
async sendCommitMessage(message: string) {
|
|
sendMessage('Commit', message);
|
|
}
|
|
|
|
async sendCipherMessages(ciphers: string[]) {
|
|
for (let i = 0; i < ciphers.length; i++) {
|
|
const cipher = ciphers[i];
|
|
sendMessage('Cipher', cipher);
|
|
}
|
|
}
|
|
|
|
sendFaucetMessage(message: string): void {
|
|
sendMessage('Faucet', message);
|
|
}
|
|
|
|
async parseCipher(message: string) {
|
|
try {
|
|
// console.log('parsing new cipher');
|
|
const apiReturn = this.sdkClient.parse_cipher(message);
|
|
console.log('🚀 ~ Services ~ parseCipher ~ apiReturn:', apiReturn);
|
|
await this.handleApiReturn(apiReturn);
|
|
|
|
// Device 1 wait Device 2
|
|
const waitingModal = document.getElementById('waiting-modal');
|
|
if (waitingModal) {
|
|
this.device2Ready = true;
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error(`Parsed cipher with error: ${e}`);
|
|
}
|
|
// await this.saveCipherTxToDb(parsedTx)
|
|
}
|
|
|
|
async parseNewTx(tx: string) {
|
|
try {
|
|
const parsedTx = this.sdkClient.parse_new_tx(tx, 0);
|
|
if (parsedTx) {
|
|
try {
|
|
await this.handleApiReturn(parsedTx);
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
} catch (e) {
|
|
console.error('Failed to update device with new tx');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.trace(e);
|
|
}
|
|
}
|
|
|
|
private async getCipherForDiff(diff: UserDiff): Promise<string | null> {
|
|
// get the process
|
|
try {
|
|
const process = await this.getProcess(diff.process_id);
|
|
} catch (e) {
|
|
console.error('Failed to get process:', e);
|
|
return null;
|
|
}
|
|
const state = process.states.find(state => state.state_id === diff.state_id);
|
|
if (state) {
|
|
// Now we return the encrypted value for that field
|
|
const cipher = state.encrypted_pcd[diff.field];
|
|
if (cipher) {
|
|
return cipher;
|
|
} else {
|
|
console.error('Failed to get encrypted value');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async tryFetchDiffValue(diffs: UserDiff[]): Promise<[UserDiff[], Record<string, string>]>{
|
|
if (diffs.length === 0) {
|
|
return [[], {}];
|
|
}
|
|
|
|
// We check if we have the value in diffs
|
|
let retrievedValues: Record<string, string> = {};
|
|
for (const diff of diffs) {
|
|
const hash = diff.value_commitment;
|
|
if (!hash) {
|
|
console.error('No commitment for diff');
|
|
continue;
|
|
}
|
|
|
|
const value = diff.new_value;
|
|
// Check if `new_value` is missing
|
|
if (value === null) {
|
|
try {
|
|
const res = await this.fetchValueFromStorage(hash);
|
|
if (!res) {
|
|
console.error('Failed to fetch value for hash', hash);
|
|
} else {
|
|
diff.new_value = res['value'];
|
|
retrievedValues[hash] = res['value'];
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to fetch new_value for diff: ${JSON.stringify(diff)}`, error);
|
|
}
|
|
} else {
|
|
// We should have it in db if it came from the wasm, but just in case
|
|
try {
|
|
await this.saveDiffsToDb(diff);
|
|
} catch (e) {
|
|
console.error(`Failed to save diff to db: ${e}`);
|
|
}
|
|
|
|
// We already have this value, so we check if it's on storage and push it if not
|
|
const onStorage = this.testDataInStorage(hash);
|
|
if (onStorage === null) {
|
|
console.error('Failed to test data presence in storage');
|
|
continue;
|
|
}
|
|
if (!onStorage) {
|
|
// We push the encrypted data on storage with default ttl
|
|
// We need to take the encrypted data from the state
|
|
const cipher = await getCipherForDiff(diff);
|
|
if (cipher) {
|
|
try {
|
|
await this.saveDataToStorage(hash, cipher, null);
|
|
} catch (e) {
|
|
console.error(`Failed to save to storage: ${e}`);
|
|
}
|
|
}
|
|
} else {
|
|
// We could pump the ttl here
|
|
// for now, do nothing
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [diffs, retrievedValues];
|
|
}
|
|
|
|
public async handleApiReturn(apiReturn: ApiReturn) {
|
|
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;
|
|
}
|
|
|
|
// We don't want to throw an error, it could simply be that we registered directly the shared secret
|
|
// this.removeUnconfirmedSecret(entry.value);
|
|
}
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
if (apiReturn.updated_process) {
|
|
const updatedProcess = apiReturn.updated_process;
|
|
|
|
const processId: string = updatedProcess.process_id;
|
|
|
|
// Save process to db
|
|
try {
|
|
await this.saveProcessToDb(processId, updatedProcess.current_process);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
|
|
const isPaired = this.isPaired();
|
|
|
|
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
|
|
const [updatedDiffs, retrievedValues] = await this.tryFetchDiffValue(updatedProcess.diffs);
|
|
if (Object.entries(retrievedValues).length != 0) {
|
|
const stateId = updatedDiffs[0].state_id;
|
|
const processId = updatedDiffs[0].process_id;
|
|
// We update the process with the value we retrieved
|
|
const hashToValues = JSON.stringify(retrievedValues);
|
|
const apiReturn = this.sdkClient.update_process_state(processId, stateId, hashToValues);
|
|
await this.handleApiReturn(apiReturn);
|
|
} else {
|
|
try {
|
|
await this.saveDiffsToDb(updatedDiffs);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
if (!isPaired) {
|
|
await this.openPairingConfirmationModal(updatedDiffs);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (updatedProcess.modified_state) {
|
|
const responsePrdReturn = this.sdkClient.create_response_prd(processId, updatedProcess.modified_state);
|
|
await this.handleApiReturn(responsePrdReturn);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
public async openPairingConfirmationModal(diffs: UserDiff[]) {
|
|
const rolesDiff = diffs.find((diff) => diff.field === 'roles');
|
|
if (!rolesDiff) {
|
|
throw new Error('Pairing process must have roles');
|
|
}
|
|
const processId = rolesDiff.process_id;
|
|
const stateId = rolesDiff.state_id;
|
|
try {
|
|
await this.routingInstance.openPairingConfirmationModal(rolesDiff.new_value, processId, stateId);
|
|
} catch (e) {
|
|
throw new Error(`${e}`);
|
|
}
|
|
}
|
|
|
|
pairDevice(spAddressList: string[], pairingProcess: string) {
|
|
try {
|
|
this.sdkClient.pair_device(pairingProcess, spAddressList);
|
|
} catch (e) {
|
|
throw new Error(`Failed to pair device: ${e}`);
|
|
}
|
|
}
|
|
|
|
public getAmount(): BigInt {
|
|
const amount = this.sdkClient.get_available_amount();
|
|
return amount;
|
|
}
|
|
|
|
async getDeviceAddress() {
|
|
return await this.sdkClient.get_address();
|
|
}
|
|
|
|
public dumpDeviceFromMemory(): string {
|
|
try {
|
|
return this.sdkClient.dump_device();
|
|
} catch (e) {
|
|
throw new Error(`Failed to dump device: ${e}`);
|
|
}
|
|
}
|
|
|
|
public getPairingProcessId(): string {
|
|
try {
|
|
return this.sdkClient.get_pairing_process_id();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get pairing process: ${e}`);
|
|
}
|
|
}
|
|
|
|
async saveDeviceInDatabase(device: any): 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);
|
|
}
|
|
}
|
|
|
|
async getDeviceFromDatabase(): Promise<string | null> {
|
|
const db = await Database.getInstance();
|
|
const walletStore = 'wallet';
|
|
try {
|
|
const dbRes = await db.getObject(walletStore, '1');
|
|
if (dbRes) {
|
|
const wallet = dbRes['device'];
|
|
return wallet;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve device from db: ${e}`);
|
|
}
|
|
}
|
|
|
|
async getMemberFromDevice(): Promise<string[] | null> {
|
|
try {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (device) {
|
|
const parsed: Device = JSON.parse(device);
|
|
const pairedMember = parsed['paired_member'];
|
|
return pairedMember.sp_addresses;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve paired_member from device: ${e}`);
|
|
}
|
|
}
|
|
|
|
isChildRole(parent: any, child: any): boolean {
|
|
try {
|
|
this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
rolesContainsUs(roles: any): boolean {
|
|
try {
|
|
this.sdkClient.roles_contains_us(JSON.stringify(roles));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
rolesContainsMember(roles: any, member: string[]): boolean {
|
|
try {
|
|
this.sdkClient.roles_contains_member(JSON.stringify(roles), member);
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
membersInSameRoleThanUs(roles: any): Member[] | null {
|
|
try {
|
|
return this.sdkClient.members_in_same_roles_me(JSON.stringify(roles));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async dumpWallet() {
|
|
const wallet = await this.sdkClient.dump_wallet();
|
|
console.log('🚀 ~ Services ~ dumpWallet ~ wallet:', wallet);
|
|
return wallet;
|
|
}
|
|
|
|
public createFaucetMessage() {
|
|
const message = this.sdkClient.create_faucet_msg();
|
|
console.log('🚀 ~ Services ~ createFaucetMessage ~ message:', message);
|
|
return message;
|
|
}
|
|
|
|
async createNewDevice() {
|
|
let spAddress = '';
|
|
try {
|
|
spAddress = await this.sdkClient.create_new_device(0, 'signet');
|
|
const device = this.dumpDeviceFromMemory();
|
|
console.log('🚀 ~ Services ~ device:', device);
|
|
await this.saveDeviceInDatabase(device);
|
|
} catch (e) {
|
|
console.error('Services ~ Error:', e);
|
|
}
|
|
|
|
return spAddress;
|
|
}
|
|
|
|
restoreDevice(device: string) {
|
|
try {
|
|
this.sdkClient.restore_device(device);
|
|
const spAddress = this.sdkClient.get_address();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
private async removeProcess(processId: string): Promise<void> {
|
|
const db = await Database.getInstance();
|
|
const storeName = 'processes';
|
|
|
|
try {
|
|
await db.deleteObject(storeName, processId);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async saveProcessToDb(processId: string, process: Process) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.addObject({
|
|
storeName: 'processes',
|
|
object: process,
|
|
key: processId,
|
|
});
|
|
} catch (e) {
|
|
throw new Error(`Failed to save process: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async saveStatesToStorage(process: Process, state_ids: string[]) {
|
|
// We check how many copies in storage nodes
|
|
// We check the storage nodes in the process itself
|
|
// this.sdkClient.get_storages(commitedIn);
|
|
const storages = [storageUrl];
|
|
|
|
for (const state of process.states) {
|
|
if (state.state_id === "") {
|
|
continue;
|
|
}
|
|
if (!state.encrypted_pcd) {
|
|
console.warn('Empty encrypted pcd, skipping...');
|
|
continue;
|
|
}
|
|
if (state_ids.includes(state.state_id)) {
|
|
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
|
|
// get the encrypted value with the field name
|
|
const value = state.encrypted_pcd[field];
|
|
await storeData(storages, hash, value, null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async saveDataToStorage(hash: string, data: string, ttl: number | null) {
|
|
const storages = [storageUrl];
|
|
|
|
try {
|
|
await storeData(storages, hash, value, ttl);
|
|
} catch (e) {
|
|
console.error(`Failed to store data with hash ${hash}: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async fetchValueFromStorage(hash: string): Promise<any | null> {
|
|
const storages = [storageUrl];
|
|
|
|
return await retrieveData(storages, hash);
|
|
}
|
|
|
|
public async testDataInStorage(hash: string): Promise<boolean | null> {
|
|
const storages = [storageUrl];
|
|
|
|
return await testData(storages, hash);
|
|
}
|
|
|
|
public async saveDiffsToDb(diffs: UserDiff[]) {
|
|
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 process: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async getProcess(commitedIn: string): Promise<Process> {
|
|
const db = await Database.getInstance();
|
|
return await db.getObject('processes', commitedIn);
|
|
}
|
|
|
|
public async getProcesses(): Promise<Record<string, Process>> {
|
|
const db = await Database.getInstance();
|
|
|
|
const processes: Record<string, Process> = await db.dumpStore('processes');
|
|
return processes;
|
|
}
|
|
|
|
public async getChildrenOfProcess(processId: string): Promise<string[]> {
|
|
const processes = await this.getProcesses();
|
|
|
|
const res = [];
|
|
for (const [hash, process] of Object.entries(processes)) {
|
|
const firstState = process.states[0];
|
|
const pcdCommitment = firstState['pcd_commitment'];
|
|
try {
|
|
const parentIdHash = pcdCommitment['parent_id'];
|
|
const diff = await this.getDiffByValue(parentIdHash);
|
|
if (diff && diff['new_value'] === processId) {
|
|
res.push(JSON.stringify(process));
|
|
}
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
|
|
const db = await Database.getInstance();
|
|
for (const [commitedIn, process] of Object.entries(processes)) {
|
|
await db.addObject({ storeName: 'processes', object: process, key: commitedIn});
|
|
}
|
|
|
|
await this.restoreProcessesFromDB();
|
|
}
|
|
|
|
// Match what we get from relay against what we already know and fetch missing data
|
|
public async updateProcessesFromRelay(processes: Record<string, Process>) {
|
|
const db = await Database.getInstance();
|
|
for (const [processId, process] of Object.entries(processes)) {
|
|
try {
|
|
this.sdkClient.sync_process_from_relay(processId, JSON.stringify(process));
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore process in wasm with persistent storage
|
|
public async restoreProcessesFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const processes: Record<string, Process> = await db.dumpStore('processes');
|
|
if (processes && Object.keys(processes).length != 0) {
|
|
console.log(`Restoring ${Object.keys(processes).length} processes`);
|
|
this.sdkClient.set_process_cache(JSON.stringify(processes));
|
|
} else {
|
|
console.log('No processes to restore!');
|
|
}
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public async clearSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.clearStore('shared_secrets');
|
|
await db.clearStore('unconfirmed_secrets');
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
|
|
const db = await Database.getInstance();
|
|
|
|
for (const secret of secretsStore.unconfirmed_secrets) {
|
|
await db.addObject({
|
|
storeName: 'unconfirmed_secrets',
|
|
object: secret,
|
|
key: null,
|
|
});
|
|
}
|
|
const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
|
|
for (const entry of entries) {
|
|
await db.addObject({
|
|
storeName: 'shared_secrets',
|
|
object: entry.value,
|
|
key: entry.key,
|
|
});
|
|
}
|
|
|
|
// Now we can transfer them to memory
|
|
await this.restoreSecretsFromDB();
|
|
}
|
|
|
|
public async restoreSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const sharedSecrets: Record<string, string> = await db.dumpStore('shared_secrets');
|
|
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets');
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
|
};
|
|
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async getDescription(processId: string, process: Process): Promise<string | null> {
|
|
const service = await Services.getInstance();
|
|
// Get the `commited_in` value of the last state and remove it from the array
|
|
const currentCommitedIn = process.states.at(-1)?.commited_in;
|
|
|
|
if (currentCommitedIn === undefined) {
|
|
return null; // No states available
|
|
}
|
|
|
|
// Find the last state where `commited_in` is different
|
|
let lastDifferentState = process.states.findLast(
|
|
state => state.commited_in !== currentCommitedIn
|
|
);
|
|
|
|
if (!lastDifferentState) {
|
|
// It means that we only have one state that is not commited yet, that can happen with process we just created
|
|
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
|
|
lastDifferentState = process.states.at(-1);
|
|
}
|
|
|
|
if (!lastDifferentState.pcd_commitment) {
|
|
return null;
|
|
}
|
|
|
|
// Take the description out of the state, if any
|
|
const description = lastDifferentState!.pcd_commitment['description'];
|
|
if (description) {
|
|
const userDiff = await service.getDiffByValue(description);
|
|
if (userDiff) {
|
|
return userDiff.new_value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getNotifications(): any[] | null {
|
|
// return [
|
|
// {
|
|
// id: 1,
|
|
// title: 'Notif 1',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif1',
|
|
// },
|
|
// {
|
|
// id: 2,
|
|
// title: 'Notif 2',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif2',
|
|
// },
|
|
// {
|
|
// id: 3,
|
|
// title: 'Notif 3',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif3',
|
|
// },
|
|
// ];
|
|
return this.notifications;
|
|
}
|
|
|
|
setNotifications(notifications: any[]) {
|
|
this.notifications = notifications;
|
|
}
|
|
|
|
async importJSON(backup: BackUp): Promise<void> {
|
|
const device = JSON.stringify(backup.device);
|
|
|
|
// Reset current device
|
|
await this.resetDevice();
|
|
|
|
await this.saveDeviceInDatabase(device);
|
|
|
|
this.restoreDevice(device);
|
|
|
|
// TODO restore secrets and processes from file
|
|
const secretsStore = backup.secrets;
|
|
await this.restoreSecretsFromBackUp(secretsStore);
|
|
|
|
const processes = backup.processes;
|
|
await this.restoreProcessesFromBackUp(processes);
|
|
}
|
|
|
|
public async createBackUp(): Promise<BackUp | null> {
|
|
// Get the device from indexedDB
|
|
const deviceStr = await this.getDeviceFromDatabase();
|
|
if (!deviceStr) {
|
|
console.error('No device loaded');
|
|
return null;
|
|
}
|
|
|
|
const device: Device = JSON.parse(deviceStr);
|
|
|
|
// Get the processes
|
|
const processes = await this.getProcesses();
|
|
|
|
// Get the shared secrets
|
|
const secrets = await this.getAllSecrets();
|
|
|
|
// Create a backup object
|
|
const backUp = {
|
|
device: device,
|
|
secrets: secrets,
|
|
processes: processes,
|
|
};
|
|
|
|
return backUp;
|
|
}
|
|
|
|
// Device 1 wait Device 2
|
|
public device1: boolean = false;
|
|
public device2Ready: boolean = false;
|
|
|
|
public resetState() {
|
|
this.device1 = false;
|
|
this.device2Ready = false;
|
|
}
|
|
|
|
|
|
// Handle the handshake message
|
|
public async handleHandshakeMsg(url: string, parsedMsg: any) {
|
|
// Get the current user
|
|
const us = this.getMemberFromDevice();
|
|
console.log("Je suis le us de la fonction handleHandshakeMsg:", us);
|
|
try {
|
|
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
|
|
this.updateRelay(url, handshakeMsg.sp_address);
|
|
const members = handshakeMsg.peers_list;
|
|
if (this.membersList && Object.keys(this.membersList).length === 0) {
|
|
this.membersList = handshakeMsg.peers_list;
|
|
} else {
|
|
// console.log('Received members:');
|
|
// console.log(handshakeMsg.peers_list);
|
|
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
|
|
this.membersList[processId] = member as Member;
|
|
}
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
const newProcesses = handshakeMsg.processes_list;
|
|
if (newProcesses && Object.keys(newProcesses).length !== 0) {
|
|
for (const [processId, process] of Object.entries(newProcesses)) {
|
|
// We check if we're part of the process
|
|
if (process.states.length < 2) continue;
|
|
let stateIds = [];
|
|
let managers = new Set();
|
|
for (const state of process.states) {
|
|
if (state.encrypted_pcd === null) continue;
|
|
const roles = state.encrypted_pcd['roles'];
|
|
if (!roles) {
|
|
console.error('Can\'t find roles');
|
|
continue;
|
|
}
|
|
|
|
if (this.rolesContainsUs(roles)) {
|
|
// We add this state to the list to request
|
|
stateIds.push(state.state_id);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
// For now we just add everyone that is in the same role than us
|
|
// const sendTo = this.membersInSameRoleThanUs(roles);
|
|
for (const [_, role] of Object.entries(roles)) {
|
|
if (!role.members.includes(us)) continue;
|
|
for (const member of role.members) {
|
|
if (member !== us) {
|
|
managers.push(member);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
this.sdkClient.request_data(processId, stateIds, managers);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
await this.updateProcessesFromRelay(newProcesses);
|
|
}
|
|
}, 500)
|
|
} catch (e) {
|
|
console.error('Failed to parse init message:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retourne la liste de tous les membres
|
|
* @returns Un tableau contenant tous les membres
|
|
*/
|
|
public getAllMembers(): Record<string, Member> {
|
|
return Object.fromEntries(
|
|
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
|
);
|
|
}
|
|
|
|
|
|
public getAddressesForMemberId(memberId: string): string[] | null {
|
|
return this.membersList[memberId];
|
|
}
|
|
}
|