1702 lines
54 KiB
TypeScript
Executable File
1702 lines
54 KiB
TypeScript
Executable File
import { INotification } from '~/models/notification.model';
|
||
import { IProcess } from '~/models/process.model';
|
||
import { initWebsocket, sendMessage } from '../websockets';
|
||
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
|
||
import ModalService from './modal.service';
|
||
import Database from './database.service';
|
||
import { navigate } from '../router';
|
||
import { storeData, retrieveData, testData } from './storage.service';
|
||
import { BackUp } from '~/models/backup.model';
|
||
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
||
|
||
export const U32_MAX = 4294967295;
|
||
|
||
const BASEURL = `https://demo.4nkweb.com`;
|
||
const BOOTSTRAPURL = [`${BASEURL}/ws/`];
|
||
const STORAGEURL = `${BASEURL}/storage`
|
||
const DEFAULTAMOUNT = 1000n;
|
||
const EMPTY32BYTES = String('').padStart(64, '0');
|
||
|
||
export default class Services {
|
||
private static initializing: Promise<Services> | null = null;
|
||
private static instance: Services;
|
||
private processId: string | null = null;
|
||
private stateId: string | null = null;
|
||
private sdkClient: any;
|
||
private processesCache: Record<string, Process> = {};
|
||
private myProcesses: Set<string> = new Set();
|
||
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, '');
|
||
}
|
||
}
|
||
|
||
public setProcessId(processId: string | null) {
|
||
this.processId = processId;
|
||
}
|
||
|
||
public setStateId(stateId: string | null) {
|
||
this.stateId = stateId;
|
||
}
|
||
|
||
public getProcessId(): string | null {
|
||
return this.processId;
|
||
}
|
||
|
||
public getStateId(): string | null {
|
||
return this.stateId;
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
private async getTokensFromFaucet(): Promise<void> {
|
||
try {
|
||
await this.ensureSufficientAmount();
|
||
} catch (e) {
|
||
console.error('Failed to get tokens from relay, check connection');
|
||
return;
|
||
}
|
||
}
|
||
|
||
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 this.sdkClient.create_transaction(addresses, 1);
|
||
} catch (e) {
|
||
console.error('Failed to connect member:', e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
private async ensureSufficientAmount(): Promise<void> {
|
||
const availableAmt = this.getAmount();
|
||
const target: BigInt = DEFAULTAMOUNT * BigInt(10);
|
||
|
||
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 createPairingProcess(userName: string, pairWith: string[]): Promise<ApiReturn> {
|
||
if (this.sdkClient.is_paired()) {
|
||
throw new Error('Device already paired');
|
||
}
|
||
const myAddress: string = this.sdkClient.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: [STORAGEURL]
|
||
},
|
||
};
|
||
try {
|
||
return this.createProcess(
|
||
privateData,
|
||
publicData,
|
||
roles
|
||
);
|
||
} catch (e) {
|
||
throw new Error(`Creating process failed:, ${e}`);
|
||
}
|
||
}
|
||
|
||
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 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 = {
|
||
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
||
...this.sdkClient.encode_binary(privateSplitData.binaryData)
|
||
};
|
||
const encodedPublicData = {
|
||
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
||
...this.sdkClient.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 = this.sdkClient.create_new_process (
|
||
encodedPrivateData,
|
||
roles,
|
||
encodedPublicData,
|
||
relayAddress,
|
||
feeRate,
|
||
this.getAllMembers()
|
||
);
|
||
|
||
return(result);
|
||
}
|
||
|
||
public async updateProcess(process: Process, privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
|
||
// If roles is null, we just take the last commited state roles
|
||
if (!roles) {
|
||
roles = this.getRoles(process);
|
||
} else {
|
||
// We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm
|
||
console.log('Provided new roles:', JSON.stringify(roles));
|
||
}
|
||
let members: Set<Member> = new Set();
|
||
for (const role of Object.values(roles!)) {
|
||
for (const member of role.members) {
|
||
members.add(member)
|
||
}
|
||
}
|
||
if (members.size === 0) {
|
||
// This must be a pairing process
|
||
// Check if we have a pairedAddresses in the public data
|
||
const publicData = this.getPublicData(process);
|
||
if (!publicData || !publicData['pairedAddresses']) {
|
||
throw new Error('Not a pairing process');
|
||
}
|
||
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
|
||
if (decodedAddresses.length === 0) {
|
||
throw new Error('Not a pairing process');
|
||
}
|
||
members.add({ sp_addresses: decodedAddresses });
|
||
}
|
||
await this.checkConnections([...members]);
|
||
const privateSplitData = this.splitData(privateData);
|
||
const publicSplitData = this.splitData(publicData);
|
||
const encodedPrivateData = {
|
||
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
||
...this.sdkClient.encode_binary(privateSplitData.binaryData)
|
||
};
|
||
const encodedPublicData = {
|
||
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
||
...this.sdkClient.encode_binary(publicSplitData.binaryData)
|
||
};
|
||
try {
|
||
return this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers());
|
||
} catch (e) {
|
||
throw new Error(`Failed to update process: ${e}`);
|
||
}
|
||
}
|
||
|
||
public async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
|
||
const process = await this.getProcess(processId);
|
||
if (!process) {
|
||
throw new Error('Unknown process');
|
||
}
|
||
try {
|
||
return this.sdkClient.create_update_message(process, stateId, this.getAllMembers());
|
||
} catch (e) {
|
||
throw new Error(`Failed to create prd update: ${e}`);
|
||
}
|
||
}
|
||
|
||
public async createPrdResponse(processId: string, stateId: string): Promise<ApiReturn> {
|
||
const process = await this.getProcess(processId);
|
||
if (!process) {
|
||
throw new Error('Unknown process');
|
||
}
|
||
try {
|
||
return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers());
|
||
} catch (e) {
|
||
throw new Error(`Failed to create response prd: ${e}`);
|
||
}
|
||
}
|
||
|
||
public async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
|
||
const process = await this.getProcess(processId);
|
||
if (!process) {
|
||
throw new Error('Failed to get process from db');
|
||
}
|
||
try {
|
||
return this.sdkClient.validate_state(process, stateId, this.getAllMembers());
|
||
} catch (e) {
|
||
throw new Error(`Failed to create prd response: ${e}`);
|
||
}
|
||
}
|
||
|
||
public async rejectChange(processId: string, stateId: string): Promise<ApiReturn> {
|
||
const process = await this.getProcess(processId);
|
||
if (!process) {
|
||
throw new Error('Failed to get process from db');
|
||
}
|
||
try {
|
||
return this.sdkClient.refuse_state(process, stateId);
|
||
} 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) {
|
||
const membersList = this.getAllMembers();
|
||
try {
|
||
// console.log('parsing new cipher');
|
||
const apiReturn = this.sdkClient.parse_cipher(message, membersList);
|
||
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) {
|
||
const membersList = this.getAllMembers();
|
||
try {
|
||
const parsedTx = this.sdkClient.parse_new_tx(tx, 0, membersList);
|
||
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 async handleApiReturn(apiReturn: ApiReturn) {
|
||
console.log(apiReturn);
|
||
if (apiReturn.partial_tx) {
|
||
try {
|
||
const res = this.sdkClient.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;
|
||
}
|
||
|
||
// We don't want to throw an error, it could simply be that we registered directly the shared secret
|
||
// this.removeUnconfirmedSecret(entry.value);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
public async openPairingConfirmationModal(processId: string) {
|
||
const process = await this.getProcess(processId);
|
||
if (!process) {
|
||
console.error('Failed to find pairing process');
|
||
return;
|
||
}
|
||
const firstState = process.states[0];
|
||
const roles = firstState.roles;
|
||
const stateId = firstState.state_id;
|
||
try {
|
||
await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
public async confirmPairing() {
|
||
if (!this.processId || !this.stateId) {
|
||
console.error('Missing process and/or state ID');
|
||
return;
|
||
}
|
||
let createPrdUpdateReturn;
|
||
try {
|
||
createPrdUpdateReturn = await this.createPrdUpdate(this.processId, this.stateId);
|
||
} catch (e) {
|
||
throw new Error(`createPrdUpdate failed: ${e}`);
|
||
}
|
||
await this.handleApiReturn(createPrdUpdateReturn);
|
||
|
||
let approveChangeReturn;
|
||
try {
|
||
approveChangeReturn = await this.approveChange(this.processId, this.stateId);
|
||
} catch (e) {
|
||
throw new Error(`approveChange failed: ${e}`);
|
||
}
|
||
await this.handleApiReturn(approveChangeReturn);
|
||
|
||
await this.pairDevice();
|
||
|
||
this.processId = null;
|
||
this.stateId = null;
|
||
const newDevice = this.dumpDeviceFromMemory();
|
||
await this.saveDeviceInDatabase(newDevice);
|
||
await navigate('account');
|
||
}
|
||
|
||
public async updateDevice(): Promise<void> {
|
||
let myPairingProcessId: string;
|
||
try {
|
||
myPairingProcessId = this.getPairingProcessId();
|
||
} catch (e) {
|
||
console.error('Failed to get pairing process id');
|
||
return;
|
||
}
|
||
|
||
const myPairingProcess = await this.getProcess(myPairingProcessId);
|
||
if (!myPairingProcess) {
|
||
console.error('Unknown pairing process');
|
||
return;
|
||
}
|
||
const myPairingState = this.getLastCommitedState(myPairingProcess);
|
||
if (myPairingState) {
|
||
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
|
||
const spAddressList = this.decodeValue(encodedSpAddressList);
|
||
if (spAddressList.length === 0) {
|
||
console.error('Empty pairedAddresses');
|
||
return;
|
||
}
|
||
// We can check if our address is included and simply unpair if it's not
|
||
if (!spAddressList.includes(this.getDeviceAddress())) {
|
||
await this.unpairDevice();
|
||
return;
|
||
}
|
||
// We can update the device with the new addresses
|
||
this.sdkClient.unpair_device();
|
||
this.sdkClient.pair_device(myPairingProcessId, spAddressList);
|
||
const newDevice = this.dumpDeviceFromMemory();
|
||
await this.saveDeviceInDatabase(newDevice);
|
||
}
|
||
}
|
||
|
||
public async pairDevice() {
|
||
if (!this.processId) {
|
||
console.error('No processId set');
|
||
return;
|
||
}
|
||
const process = await this.getProcess(this.processId);
|
||
if (!process) {
|
||
console.error('Unknown process');
|
||
return;
|
||
}
|
||
|
||
let spAddressList: string[] = [];
|
||
try {
|
||
let encodedSpAddressList: number[] = [];
|
||
if (this.stateId) {
|
||
const state = process.states.find(state => state.state_id === this.stateId);
|
||
if (state) {
|
||
encodedSpAddressList = state.public_data['pairedAddresses'];
|
||
}
|
||
} else {
|
||
// We assume it's the last commited state
|
||
const lastCommitedState = this.getLastCommitedState(process);
|
||
if (lastCommitedState) {
|
||
encodedSpAddressList = lastCommitedState.public_data['pairedAddresses'];
|
||
}
|
||
}
|
||
spAddressList = this.sdkClient.decode_value(encodedSpAddressList);
|
||
if (!spAddressList || spAddressList.length == 0) {
|
||
throw new Error('Empty pairedAddresses');
|
||
}
|
||
} catch (e) {
|
||
throw new Error(`Failed to get pairedAddresses from process: ${e}`);
|
||
}
|
||
try {
|
||
this.sdkClient.pair_device(this.processId, spAddressList);
|
||
} catch (e) {
|
||
throw new Error(`Failed to pair device: ${e}`);
|
||
}
|
||
}
|
||
|
||
public getAmount(): BigInt {
|
||
const amount = this.sdkClient.get_available_amount();
|
||
return amount;
|
||
}
|
||
|
||
getDeviceAddress(): string {
|
||
try {
|
||
return this.sdkClient.get_address();
|
||
} catch (e) {
|
||
throw new Error(`Failed to get device address: ${e}`);
|
||
}
|
||
}
|
||
|
||
public dumpDeviceFromMemory(): string {
|
||
try {
|
||
return this.sdkClient.dump_device();
|
||
} catch (e) {
|
||
throw new Error(`Failed to dump device: ${e}`);
|
||
}
|
||
}
|
||
|
||
public dumpNeuteredDevice(): Device | null {
|
||
try {
|
||
return this.sdkClient.dump_neutered_device();
|
||
} catch (e) {
|
||
console.error(`Failed to dump device: ${e}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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: Record<string, RoleDefinition>): boolean {
|
||
let us;
|
||
try {
|
||
us = this.sdkClient.get_pairing_process_id();
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
|
||
return this.rolesContainsMember(roles, us);
|
||
}
|
||
|
||
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
|
||
for (const roleDef of Object.values(roles)) {
|
||
if (roleDef.members.includes(pairingProcessId)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
async dumpWallet() {
|
||
const wallet = await this.sdkClient.dump_wallet();
|
||
return wallet;
|
||
}
|
||
|
||
public createFaucetMessage() {
|
||
const message = this.sdkClient.create_faucet_msg();
|
||
return message;
|
||
}
|
||
|
||
async createNewDevice() {
|
||
let spAddress = '';
|
||
try {
|
||
spAddress = await this.sdkClient.create_new_device(0, 'signet');
|
||
const device = this.dumpDeviceFromMemory();
|
||
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 batchSaveProcessesToDb(processes: Record<string, Process>) {
|
||
const db = await Database.getInstance();
|
||
const storeName = 'processes';
|
||
try {
|
||
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
|
||
this.processesCache = { ...this.processesCache, ...processes };
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
public async saveProcessToDb(processId: string, process: Process) {
|
||
const db = await Database.getInstance();
|
||
const storeName = 'processes';
|
||
try {
|
||
await db.addObject({
|
||
storeName,
|
||
object: process,
|
||
key: processId,
|
||
});
|
||
|
||
// Update the process in the cache
|
||
this.processesCache[processId] = process;
|
||
} catch (e) {
|
||
console.error(`Failed to save process ${processId}: ${e}`);
|
||
}
|
||
}
|
||
|
||
public 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}`);
|
||
}
|
||
}
|
||
|
||
public async getBlobFromDb(hash: string): Promise<Blob | null> {
|
||
const db = await Database.getInstance();
|
||
try {
|
||
return await db.getObject('data', hash);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
|
||
const storages = [STORAGEURL];
|
||
|
||
try {
|
||
await storeData(storages, hash, data, 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<Record<string, boolean | null> | 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(processId: string): Promise<Process | null> {
|
||
if (this.processesCache[processId]) {
|
||
return this.processesCache[processId];
|
||
} else {
|
||
const db = await Database.getInstance();
|
||
const process = await db.getObject('processes', processId);
|
||
return process;
|
||
}
|
||
}
|
||
|
||
public async getProcesses(): Promise<Record<string, Process>> {
|
||
if (Object.keys(this.processesCache).length > 0) {
|
||
return this.processesCache;
|
||
} else {
|
||
try {
|
||
const db = await Database.getInstance();
|
||
this.processesCache = await db.dumpStore('processes');
|
||
return this.processesCache;
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
|
||
const db = await Database.getInstance();
|
||
const storeName = 'processes';
|
||
try {
|
||
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
|
||
await this.restoreProcessesFromDB();
|
||
}
|
||
|
||
// 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.processesCache = processes;
|
||
this.sdkClient.set_process_cache(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;
|
||
}
|
||
}
|
||
|
||
decodeValue(value: number[]): any | null {
|
||
try {
|
||
return this.sdkClient.decode_value(value);
|
||
} catch (e) {
|
||
console.error(`Failed to decode value: ${e}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> {
|
||
let hash = state.pcd_commitment[attribute];
|
||
if (!hash) {
|
||
// attribute doesn't exist
|
||
return null;
|
||
}
|
||
let key = state.keys[attribute];
|
||
const pairingProcessId = this.getPairingProcessId();
|
||
|
||
// If key is missing, request an update and then retry
|
||
if (!key) {
|
||
const roles = state.roles;
|
||
let hasAccess = false;
|
||
// If we're not supposed to have access to this attribute, ignore
|
||
for (const role of Object.values(roles)) {
|
||
for (const rule of Object.values(role.validation_rules)) {
|
||
if (rule.fields.includes(attribute)) {
|
||
if (role.members.includes(pairingProcessId)) {
|
||
// We have access to this attribute
|
||
hasAccess = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasAccess) return null;
|
||
|
||
// We should have the key, so we're going to ask other members for it
|
||
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
|
||
|
||
const maxRetries = 5;
|
||
const retryDelay = 500; // delay in milliseconds
|
||
let retries = 0;
|
||
|
||
while ((!hash || !key) && retries < maxRetries) {
|
||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||
// Re-read hash and key after waiting
|
||
hash = state.pcd_commitment[attribute];
|
||
key = state.keys[attribute];
|
||
retries++;
|
||
}
|
||
}
|
||
|
||
if (hash && key) {
|
||
const blob = await this.getBlobFromDb(hash);
|
||
if (blob) {
|
||
// Decrypt the data
|
||
const buf = await blob.arrayBuffer();
|
||
const cipher = new Uint8Array(buf);
|
||
|
||
const keyUIntArray = this.hexToUInt8Array(key);
|
||
|
||
try {
|
||
const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher);
|
||
if (clear) {
|
||
// deserialize the result to get the actual data
|
||
const decoded = this.sdkClient.decode_value(clear);
|
||
return decoded;
|
||
} else {
|
||
throw new Error('decrypt_data returned null');
|
||
}
|
||
} catch (e) {
|
||
console.error(`Failed to decrypt data: ${e}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
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) {
|
||
try {
|
||
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
|
||
this.updateRelay(url, handshakeMsg.sp_address);
|
||
if (this.membersList && Object.keys(this.membersList).length === 0) {
|
||
// We start from an empty list, just copy it over
|
||
this.membersList = handshakeMsg.peers_list;
|
||
} else {
|
||
// We are incrementing our list
|
||
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
|
||
this.membersList[processId] = member as Member;
|
||
}
|
||
}
|
||
|
||
setTimeout(async () => {
|
||
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list;
|
||
if (!newProcesses || Object.keys(newProcesses).length === 0) {
|
||
console.debug('Received empty processes list from', url);
|
||
return;
|
||
}
|
||
|
||
if (this.processesCache && Object.keys(this.processesCache).length === 0) {
|
||
// We restored db but cache is empty, meaning we're starting from scratch
|
||
try {
|
||
await this.batchSaveProcessesToDb(newProcesses);
|
||
} catch (e) {
|
||
console.error('Failed to save processes to db:', e);
|
||
}
|
||
} else {
|
||
// We need to update our processes with what relay provides
|
||
const toSave: Record<string, Process> = {};
|
||
for (const [processId, process] of Object.entries(newProcesses)) {
|
||
const existing = await this.getProcess(processId);
|
||
if (existing) {
|
||
// Look for state id we don't know yet
|
||
let new_states = [];
|
||
let roles = [];
|
||
for (const state of process.states) {
|
||
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; }
|
||
if (!this.lookForStateId(existing, state.state_id)) {
|
||
if (this.rolesContainsUs(state.roles)) {
|
||
new_states.push(state.state_id);
|
||
roles.push(state.roles);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (new_states.length != 0) {
|
||
// We request the new states
|
||
await this.requestDataFromPeers(processId, new_states, roles);
|
||
toSave[processId] = process;
|
||
}
|
||
|
||
// Just to be sure check if that's a pairing process
|
||
const lastCommitedState = this.getLastCommitedState(process);
|
||
if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) {
|
||
// This is a pairing process
|
||
try {
|
||
const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses']);
|
||
// Are we part of it?
|
||
if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) {
|
||
// We save the process to db
|
||
await this.saveProcessToDb(processId, process as Process);
|
||
// We update the device
|
||
await this.updateDevice();
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to check for pairing process:', e);
|
||
}
|
||
}
|
||
|
||
// Otherwise we're probably just in the initial loading at page initialization
|
||
|
||
// We may learn an update for this process
|
||
// TODO maybe actually check if what the relay is sending us contains more information than what we have
|
||
// relay should always have more info than us, but we never know
|
||
// For now let's keep it simple and let the worker do the job
|
||
} else {
|
||
// We add it to db
|
||
console.log(`Saving ${processId} to db`);
|
||
toSave[processId] = process;
|
||
}
|
||
}
|
||
|
||
await this.batchSaveProcessesToDb(toSave);
|
||
}
|
||
}, 500)
|
||
} catch (e) {
|
||
console.error('Failed to parse init message:', e);
|
||
}
|
||
}
|
||
|
||
private lookForStateId(process: Process, stateId: string): boolean {
|
||
for (const state of process.states) {
|
||
if (state.state_id === stateId) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Retourne la liste de tous les membres ordonnés par leur process id
|
||
* @returns Un tableau contenant tous les membres
|
||
*/
|
||
public getAllMembersSorted(): Record<string, Member> {
|
||
return Object.fromEntries(
|
||
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||
);
|
||
}
|
||
|
||
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 compareMembers(memberA: string[], memberB: string[]): boolean {
|
||
if (!memberA || !memberB) { return false }
|
||
if (memberA.length !== memberB.length) { return false }
|
||
|
||
const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item));
|
||
|
||
return res;
|
||
}
|
||
|
||
public async handleCommitError(response: string) {
|
||
const content = JSON.parse(response);
|
||
const error = content.error;
|
||
const errorMsg = error['GenericError'];
|
||
const dontRetry = [
|
||
'State is identical to the previous state',
|
||
'Not enough valid proofs',
|
||
'Not enough members to validate',
|
||
];
|
||
if (dontRetry.includes(errorMsg)) { return; }
|
||
// Wait and retry
|
||
setTimeout(async () => {
|
||
await this.sendCommitMessage(JSON.stringify(content));
|
||
}, 1000)
|
||
}
|
||
|
||
public getRoles(process: Process): Record<string, RoleDefinition> | null {
|
||
const lastCommitedState = this.getLastCommitedState(process);
|
||
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
|
||
return lastCommitedState!.roles;
|
||
} else if (process.states.length === 2) {
|
||
const firstState = process.states[0];
|
||
if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) {
|
||
return firstState!.roles;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public getPublicData(process: Process): Record<string, any> | null {
|
||
const lastCommitedState = this.getLastCommitedState(process);
|
||
if (lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0) {
|
||
return lastCommitedState!.public_data;
|
||
} else if (process.states.length === 2) {
|
||
const firstState = process.states[0];
|
||
if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) {
|
||
return firstState!.public_data;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public getProcessName(process: Process): string | null {
|
||
const lastCommitedState = this.getLastCommitedState(process);
|
||
if (lastCommitedState && lastCommitedState.public_data) {
|
||
const processName = lastCommitedState!.public_data['processName'];
|
||
if (processName) { return this.decodeValue(processName) }
|
||
else { return null }
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async getMyProcesses(): Promise<string[] | null> {
|
||
// If we're not paired yet, just skip it
|
||
try {
|
||
this.getPairingProcessId();
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
try {
|
||
const processes = await this.getProcesses();
|
||
|
||
const newMyProcesses = new Set<string>(this.myProcesses || []);
|
||
for (const [processId, process] of Object.entries(processes)) {
|
||
// We use myProcesses attribute to not reevaluate all processes everytime
|
||
if (newMyProcesses.has(processId)) {
|
||
continue;
|
||
}
|
||
try {
|
||
const roles = this.getRoles(process);
|
||
|
||
if (roles && this.rolesContainsUs(roles)) {
|
||
newMyProcesses.add(processId);
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
this.myProcesses = newMyProcesses; // atomic update
|
||
return Array.from(this.myProcesses);
|
||
} catch (e) {
|
||
console.error("Failed to get processes:", e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record<string, RoleDefinition>[]) {
|
||
console.log('Requesting data from peers');
|
||
const membersList = this.getAllMembers();
|
||
try {
|
||
const res = this.sdkClient.request_data(processId, stateIds, roles, membersList);
|
||
await this.handleApiReturn(res);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
public hexToBlob(hexString: string): Blob {
|
||
const uint8Array = this.hexToUInt8Array(hexString);
|
||
|
||
return new Blob([uint8Array], { type: "application/octet-stream" });
|
||
}
|
||
|
||
public 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 blobToHex(blob: Blob): Promise<string> {
|
||
const buffer = await blob.arrayBuffer();
|
||
const bytes = new Uint8Array(buffer);
|
||
return Array.from(bytes)
|
||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||
.join('');
|
||
}
|
||
|
||
public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string {
|
||
return this.sdkClient.hash_value(fileBlob, commitedIn, label);
|
||
}
|
||
|
||
public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
|
||
return this.sdkClient.get_merkle_proof(processState, attributeName);
|
||
}
|
||
|
||
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
|
||
try {
|
||
return this.sdkClient.validate_merkle_proof(proof, hash);
|
||
} catch (e) {
|
||
throw new Error(`Failed to validate merkle proof: ${e}`);
|
||
}
|
||
}
|
||
|
||
public getLastCommitedState(process: Process): ProcessState | null {
|
||
if (process.states.length === 0) return null;
|
||
const processTip = process.states[process.states.length - 1].commited_in;
|
||
const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip);
|
||
if (lastCommitedState) {
|
||
return lastCommitedState;
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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 getUncommitedStates(process: Process): ProcessState[] {
|
||
if (process.states.length === 0) return [];
|
||
const processTip = process.states[process.states.length - 1].commited_in;
|
||
const res = process.states.filter(state => state.commited_in === processTip);
|
||
return res.filter(state => state.state_id !== EMPTY32BYTES);
|
||
}
|
||
|
||
public getStateFromId(process: Process, stateId: string): ProcessState | null {
|
||
if (process.states.length === 0) return null;
|
||
const state = process.states.find(state => state.state_id === stateId);
|
||
if (state) {
|
||
return state;
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
|
||
if (process.states.length === 0) return null;
|
||
|
||
const index = process.states.findIndex(state => state.state_id === stateId);
|
||
|
||
if (index !== -1 && index < process.states.length - 1) {
|
||
return process.states[index + 1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
|
||
if (Object.keys(roles).length != 1) { return false }
|
||
const pairingRole = roles['pairing'];
|
||
if (pairingRole) {
|
||
// For now that's enough, we should probably test more things
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public async updateMemberPublicName(process: Process, newName: string): Promise<ApiReturn> {
|
||
const publicData = {
|
||
'memberPublicName': newName
|
||
};
|
||
|
||
return await this.updateProcess(process, {}, publicData, null);
|
||
}
|
||
|
||
public async generateProcessPdf(processId: string, processState: ProcessState): Promise<void> {
|
||
const pdfDoc = await PDFDocument.create();
|
||
const page = pdfDoc.addPage([595.28, 841.89]);
|
||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||
|
||
const drawText = (text: string, x: number, y: number, opts: { size?: number, bold?: boolean } = {}) => {
|
||
const fontSize = opts.size || 12;
|
||
const usedFont = opts.bold ? fontBold : font;
|
||
page.drawText(text, { x, y, size: fontSize, font: usedFont, color: rgb(0, 0, 0) });
|
||
};
|
||
|
||
let y = 800;
|
||
|
||
// Header
|
||
drawText("Cabinet de Maître Jean Dupont", 50, y, { bold: true });
|
||
drawText("Notaire à Paris", 50, y -= 15);
|
||
drawText("12 rue des Archives", 50, y -= 15);
|
||
drawText("75003 Paris", 50, y -= 15);
|
||
drawText("Téléphone : 01 23 45 67 89", 50, y -= 15);
|
||
drawText("Email : contact@notairedupont.fr", 50, y -= 15);
|
||
|
||
// Client
|
||
y -= 30;
|
||
drawText("Client : Mme Sophie Martin", 50, y);
|
||
drawText("8 avenue de la Liberté", 50, y -= 15);
|
||
drawText("69003 Lyon", 50, y -= 15);
|
||
drawText("Email : sophie.martin@email.com", 50, y -= 15);
|
||
|
||
// Title
|
||
y -= 40;
|
||
drawText("Certificat de Validation de Données", 50, y, { size: 14, bold: true });
|
||
|
||
// Certification paragraph
|
||
y -= 40;
|
||
const certText = `Je soussigné, Maître Jean Dupont, notaire à Paris, certifie par la présente que les données suivantes ont été vérifiées et horodatées à l’aide d’une empreinte cryptographique enregistrée sur la blockchain Bitcoin.`;
|
||
page.drawText(certText, {
|
||
x: 50, y,
|
||
size: 11,
|
||
font,
|
||
lineHeight: 14,
|
||
maxWidth: 500
|
||
});
|
||
|
||
// Dossier number
|
||
y -= 60;
|
||
drawText("Numéro de dossier : N-2025-0456-PAR", 50, y);
|
||
|
||
// Process ID
|
||
y -= 40;
|
||
drawText(`Identifiant du process: ${processId.split(':')[0]}`, 50, y);
|
||
|
||
// Hash table
|
||
y -= 40;
|
||
|
||
drawText("Nom", 50, y -= 20, { bold: true });
|
||
drawText("Empreinte cryptographique (SHA-256)", 150, y, { bold: true });
|
||
|
||
for (const [label, hash] of Object.entries(processState.pcd_commitment)) {
|
||
drawText(label, 50, y -= 18);
|
||
drawText(hash, 150, y);
|
||
}
|
||
|
||
// Add the state id as hash total
|
||
drawText('Ensemble', 50, y -= 18);
|
||
drawText(processState.state_id, 150, y);
|
||
|
||
// Transaction
|
||
y -= 40;
|
||
drawText("Transaction enregistrée sur la blockchain Bitcoin :", 50, y);
|
||
drawText(processState.commited_in, 50, y -= 15);
|
||
|
||
// Date & signature
|
||
y -= 50;
|
||
drawText("Fait à Paris, le 10 juin 2025.", 50, y);
|
||
drawText("Généré automatiquement par lecoffre.io", 50, y -= 30);
|
||
// drawText("Signature du notaire : ___________________________", 50, y -= 30);
|
||
|
||
const pdfBytes = await pdfDoc.save();
|
||
|
||
// Download
|
||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `Certificat_Validation_${processId.slice(0,8)}-${processState.state_id.slice(0,8)}.pdf`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
}
|