596 lines
18 KiB
TypeScript
Executable File
596 lines
18 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, Member, Process, RoleDefinition, UserDiff } from '../../pkg/sdk_client';
|
|
import ModalService from './modal.service';
|
|
import Database from './database.service';
|
|
import { storeData, retrieveData } from './storage.service';
|
|
|
|
export const U32_MAX = 4294967295;
|
|
const RELAY_ADDRESS = "sprt1qqdg4x69xdyhxpz4weuel0985qyswa0x9ycl4q6xc0fngf78jtj27gqj5vff4fvlt3fydx4g7vv0mh7vqv8jncgusp6n2zv860nufdzkyy59pqrdr";
|
|
const wsurl = `https://demo.4nkweb.com/ws/`;
|
|
|
|
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 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();
|
|
await this.addWebsocketConnection(wsurl);
|
|
}
|
|
|
|
public async addWebsocketConnection(url: string): Promise<void> {
|
|
console.log('Opening new websocket connection');
|
|
await initWebsocket(url);
|
|
}
|
|
|
|
public async getRelayAddresses(): Promise<string[]> {
|
|
// We just return one hardcoded address for now
|
|
return [RELAY_ADDRESS];
|
|
}
|
|
|
|
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 connectMember(members: Member[]): Promise<ApiReturn> {
|
|
if (members.length === 0) {
|
|
throw new Error('Trying to connect to empty members list');
|
|
}
|
|
|
|
const members_str = members.map(member => JSON.stringify(member));
|
|
|
|
const waitForAmount = async (): Promise<BigInt> => {
|
|
let attempts = 3;
|
|
while (attempts > 0) {
|
|
const amount = this.getAmount();
|
|
if (amount !== 0n) {
|
|
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');
|
|
};
|
|
|
|
let availableAmt = this.getAmount();
|
|
if (availableAmt === 0n) {
|
|
const faucetMsg = this.createFaucetMessage();
|
|
this.sendFaucetMessage(faucetMsg);
|
|
|
|
try {
|
|
availableAmt = await waitForAmount();
|
|
} catch (e) {
|
|
console.error('Failed to retrieve amount:', e);
|
|
throw e; // Rethrow the error if needed
|
|
}
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.create_connect_transaction(members_str, 1);
|
|
} catch (e) {
|
|
throw 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: []
|
|
},
|
|
},
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
// Create prd update for current process and update
|
|
public createPrdUpdate(pcdMerkleRoot: string): ApiReturn {
|
|
if (!this.currentProcess) {
|
|
throw new Error('No current process defined');
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.create_update_message(this.currentProcess, pcdMerkleRoot);
|
|
} 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(currentPcdMerkleRoot: string): ApiReturn {
|
|
if (!this.currentProcess) {
|
|
throw new Error('No current process defined');
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.validate_state(this.currentProcess, currentPcdMerkleRoot);
|
|
} 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() {
|
|
await this.sdkClient.reset_device();
|
|
}
|
|
|
|
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);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
public getUpdateProposals(commitmentOutpoint: string) {
|
|
try {
|
|
const proposals: any = this.sdkClient.get_update_proposals(commitmentOutpoint);
|
|
if (proposals.decrypted_pcds && proposals.decrypted_pcds.length != 0) {
|
|
this.currentProcess = commitmentOutpoint;
|
|
this.pendingUpdates = proposals;
|
|
} else {
|
|
throw new Error('No pending proposals');
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to get proposal updates for process ${commitmentOutpoint}: ${e}`);
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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;
|
|
|
|
// Save process to storage
|
|
try {
|
|
await this.saveProcess(updatedProcess.commitment_tx, updatedProcess.current_process);
|
|
await this.saveDiffs(updatedProcess.new_diffs);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
|
|
if ((updatedProcess as any).new_state) {
|
|
this.currentProcess = updatedProcess.commitment_tx;
|
|
|
|
this.getUpdateProposals(this.currentProcess!);
|
|
|
|
await this.evaluatePendingUpdates();
|
|
} else if (updatedProcess.modified_state) {
|
|
// We added validation tokens
|
|
// We check if the state is now valid
|
|
// If enough validation tokens we shoot a commit msg to the relay
|
|
const [previous_state, new_state] = updatedProcess.modified_state;
|
|
const init_commitment = updatedProcess.commitment_tx;
|
|
try {
|
|
const apiReturn = this.sdkClient.evaluate_state(init_commitment, null, JSON.stringify(new_state));
|
|
await this.handleApiReturn(apiReturn);
|
|
} catch (e) {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
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 evaluatePendingUpdates() {
|
|
if (!this.currentProcess) {
|
|
throw new Error('No current process');
|
|
}
|
|
|
|
try {
|
|
await this.openConfirmationModal();
|
|
} catch (e) {
|
|
throw new Error(`Error while evaluating pending updates for process ${this.currentProcess}: ${e}`)
|
|
}
|
|
}
|
|
|
|
private async openConfirmationModal() {
|
|
if (!this.pendingUpdates || this.pendingUpdates.modified_values.length === 0) {
|
|
console.log('No pending updates to validate');
|
|
}
|
|
|
|
for (const value of this.pendingUpdates!.modified_values) {
|
|
if (value.notify_user) {
|
|
// TODO notification pop up
|
|
}
|
|
if (!value.need_validation) {
|
|
continue;
|
|
}
|
|
if (value.proof) {
|
|
// It seems we already validated that, check the proof and if valid just notify user
|
|
continue;
|
|
}
|
|
const actualProposal: Record<string, RoleDefinition> = JSON.parse(value.new_value);
|
|
const merkleRoot: string = value.new_state_merkle_root;
|
|
try {
|
|
await this.routingInstance.openPairingConfirmationModal(actualProposal, this.currentProcess!, merkleRoot);
|
|
} catch (e) {
|
|
throw new Error(`${e}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
pairDevice(spAddressList: string[]) {
|
|
if (this.currentProcess) {
|
|
try {
|
|
this.sdkClient.pair_device(this.currentProcess, 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}`);
|
|
}
|
|
}
|
|
|
|
async saveDeviceInDatabase(device: any): Promise<void> {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.addObject({
|
|
storeName: 'wallet',
|
|
object: { pre_id: '1', device },
|
|
key: null,
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
async getDeviceFromDatabase(): Promise<string | null> {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const dbRes = await db.getObject('wallet', '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 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, 'regtest');
|
|
const device = this.dumpDeviceFromMemory();
|
|
console.log('🚀 ~ Services ~ device:', device);
|
|
await this.saveDeviceInDatabase(device);
|
|
} catch (e) {
|
|
console.error('Services ~ Error:', e);
|
|
}
|
|
|
|
return spAddress;
|
|
}
|
|
|
|
async restoreDevice(device: string) {
|
|
try {
|
|
await this.sdkClient.restore_device(device);
|
|
const spAddress = this.sdkClient.get_address();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async saveProcess(commitedIn: string, process: any) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.addObject({
|
|
storeName: 'processes',
|
|
object: process,
|
|
key: commitedIn,
|
|
});
|
|
} catch (e) {
|
|
throw new Error(`Failed to save process: ${e}`)
|
|
}
|
|
}
|
|
public async saveDiffs(diffs: UserDiff[]) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
for(const diff of diffs) {
|
|
await db.addObject({
|
|
storeName: 'diffs',
|
|
object: diff,
|
|
key: diff.value_commitment,
|
|
});
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
// Restore process in wasm with persistent storage
|
|
public async restoreProcesses() {
|
|
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 restoreSecrets() {
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
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(content: any): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
}
|