Compare commits

..

No commits in common. "72d20c646ec891313b8c1a67243a68fe63791bfa" and "a7e9043ae47fcb66f07d09bb4394a204ca723894" have entirely different histories.

2 changed files with 1750 additions and 1754 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sdk_client", "name": "sdk_client",
"version": "1.1.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sdk_client", "name": "sdk_client",
"version": "1.1.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@angular/elements": "^19.0.1", "@angular/elements": "^19.0.1",

View File

@ -1,7 +1,7 @@
import { INotification } from '~/models/notification.model'; import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model'; import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets'; import { initWebsocket, sendMessage } from '../websockets';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; import type { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
import ModalService from './modal.service'; import ModalService from './modal.service';
import Database from './database.service'; import Database from './database.service';
import { navigate } from '../router'; import { navigate } from '../router';
@ -12,8 +12,8 @@ export const U32_MAX = 4294967295;
const BASEURL = `http://localhost`; const BASEURL = `http://localhost`;
const BOOTSTRAPURL = [`${BASEURL}:8090`]; const BOOTSTRAPURL = [`${BASEURL}:8090`];
const STORAGEURL = `${BASEURL}:8081` const STORAGEURL = `${BASEURL}:8081`;
const BLINDBITURL = `${BASEURL}:8000` const BLINDBITURL = `${BASEURL}:8000`;
const DEFAULTAMOUNT = 1000n; const DEFAULTAMOUNT = 1000n;
const EMPTY32BYTES = String('').padStart(64, '0'); const EMPTY32BYTES = String('').padStart(64, '0');
@ -60,6 +60,14 @@ export default class Services {
this.notifications = this.getNotifications(); this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient = await import('../../pkg/sdk_client');
this.sdkClient.setup(); this.sdkClient.setup();
const params = new URLSearchParams(window.location.search);
const isE2E = params.has('e2e');
if (isE2E) {
// Mode E2E: ne pas tenter de connexion aux relays
return;
}
for (const wsurl of Object.values(BOOTSTRAPURL)) { for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, ''); this.updateRelay(wsurl, '');
} }
@ -86,24 +94,24 @@ export default class Services {
* Waits for at least one handshake message before returning. * Waits for at least one handshake message before returning.
*/ */
public async connectAllRelays(): Promise<void> { public async connectAllRelays(): Promise<void> {
const connectedUrls: string[] = []; const connectedUrls: string[] = [];
// Connect to all relays // Connect to all relays
for (const wsurl of Object.keys(this.relayAddresses)) { for (const wsurl of Object.keys(this.relayAddresses)) {
try { try {
console.log(`Connecting to: ${wsurl}`); console.log(`Connecting to: ${wsurl}`);
await this.addWebsocketConnection(wsurl); await this.addWebsocketConnection(wsurl);
connectedUrls.push(wsurl); connectedUrls.push(wsurl);
console.log(`Successfully connected to: ${wsurl}`); console.log(`Successfully connected to: ${wsurl}`);
} catch (error) { } catch (error) {
console.error(`Failed to connect to ${wsurl}:`, error); console.error(`Failed to connect to ${wsurl}:`, error);
}
} }
}
// Wait for at least one handshake message if we have connections // Wait for at least one handshake message if we have connections
if (connectedUrls.length > 0) { if (connectedUrls.length > 0) {
await this.waitForHandshakeMessage(); await this.waitForHandshakeMessage();
} }
} }
public async addWebsocketConnection(url: string): Promise<void> { public async addWebsocketConnection(url: string): Promise<void> {
@ -117,8 +125,8 @@ export default class Services {
* @param spAddress - The SP Address (value). * @param spAddress - The SP Address (value).
*/ */
public updateRelay(wsurl: string, spAddress: string): void { public updateRelay(wsurl: string, spAddress: string): void {
this.relayAddresses[wsurl] = spAddress; this.relayAddresses[wsurl] = spAddress;
console.log(`Updated: ${wsurl} -> ${spAddress}`); console.log(`Updated: ${wsurl} -> ${spAddress}`);
} }
/** /**
@ -127,7 +135,7 @@ export default class Services {
* @returns The SP Address if found, or undefined if not. * @returns The SP Address if found, or undefined if not.
*/ */
public getSpAddress(wsurl: string): string | undefined { public getSpAddress(wsurl: string): string | undefined {
return this.relayAddresses[wsurl]; return this.relayAddresses[wsurl];
} }
/** /**
@ -135,20 +143,20 @@ export default class Services {
* @returns An array of objects containing wsurl and spAddress. * @returns An array of objects containing wsurl and spAddress.
*/ */
public getAllRelays(): { wsurl: string; spAddress: string }[] { public getAllRelays(): { wsurl: string; spAddress: string }[] {
return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({
wsurl, wsurl,
spAddress, spAddress,
})); }));
} }
/** /**
* Print all key/value pairs for debugging. * Print all key/value pairs for debugging.
*/ */
public printAllRelays(): void { public printAllRelays(): void {
console.log("Current relay addresses:"); console.log('Current relay addresses:');
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
console.log(`${wsurl} -> ${spAddress}`); console.log(`${wsurl} -> ${spAddress}`);
} }
} }
public isPaired(): boolean { public isPaired(): boolean {
@ -299,27 +307,18 @@ export default class Services {
min_sig_member: 1.0, min_sig_member: 1.0,
}, },
], ],
storages: [STORAGEURL] storages: [STORAGEURL],
}, },
}; };
try { try {
return this.createProcess( return this.createProcess(privateData, publicData, roles);
privateData,
publicData,
roles
);
} catch (e) { } catch (e) {
throw new Error(`Creating process failed:, ${e}`); throw new Error(`Creating process failed:, ${e}`);
} }
} }
private isFileBlob(value: any): value is { type: string, data: Uint8Array } { private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
return ( return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array;
typeof value === 'object' &&
value !== null &&
typeof value.type === 'string' &&
value.data instanceof Uint8Array
);
} }
private splitData(obj: Record<string, any>) { private splitData(obj: Record<string, any>) {
@ -337,11 +336,7 @@ export default class Services {
return { jsonCompatibleData, binaryData }; return { jsonCompatibleData, binaryData };
} }
public async createProcess( public async createProcess(privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition>): Promise<ApiReturn> {
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, RoleDefinition>,
): Promise<ApiReturn> {
let relayAddress = this.getAllRelays()[0]?.spAddress; let relayAddress = this.getAllRelays()[0]?.spAddress;
if (!relayAddress || relayAddress === '') { if (!relayAddress || relayAddress === '') {
@ -364,11 +359,11 @@ export default class Services {
const publicSplitData = this.splitData(publicData); const publicSplitData = this.splitData(publicData);
const encodedPrivateData = { const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData) ...this.sdkClient.encode_binary(privateSplitData.binaryData),
}; };
const encodedPublicData = { const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData) ...this.sdkClient.encode_binary(publicSplitData.binaryData),
}; };
console.log('encoded data:', encodedPrivateData); console.log('encoded data:', encodedPrivateData);
@ -387,16 +382,9 @@ export default class Services {
console.log('members:', members); console.log('members:', members);
await this.checkConnections([...members]); await this.checkConnections([...members]);
const result = this.sdkClient.create_new_process ( const result = this.sdkClient.create_new_process(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, this.getAllMembers());
encodedPrivateData,
roles,
encodedPublicData,
relayAddress,
feeRate,
this.getAllMembers()
);
return(result); return result;
} }
public async updateProcess(process: Process, privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> { public async updateProcess(process: Process, privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
@ -410,7 +398,7 @@ export default class Services {
let members: Set<Member> = new Set(); let members: Set<Member> = new Set();
for (const role of Object.values(roles!)) { for (const role of Object.values(roles!)) {
for (const member of role.members) { for (const member of role.members) {
members.add(member) members.add(member);
} }
} }
if (members.size === 0) { if (members.size === 0) {
@ -431,11 +419,11 @@ export default class Services {
const publicSplitData = this.splitData(publicData); const publicSplitData = this.splitData(publicData);
const encodedPrivateData = { const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData) ...this.sdkClient.encode_binary(privateSplitData.binaryData),
}; };
const encodedPublicData = { const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData) ...this.sdkClient.encode_binary(publicSplitData.binaryData),
}; };
try { try {
return this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers()); return this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers());
@ -536,7 +524,6 @@ export default class Services {
if (waitingModal) { if (waitingModal) {
this.device2Ready = true; this.device2Ready = true;
} }
} catch (e) { } catch (e) {
console.error(`Parsed cipher with error: ${e}`); console.error(`Parsed cipher with error: ${e}`);
} }
@ -604,7 +591,7 @@ export default class Services {
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500)); await new Promise((r) => setTimeout(r, 500));
} }
if (apiReturn.secrets) { if (apiReturn.secrets) {
@ -781,7 +768,7 @@ export default class Services {
try { try {
let encodedSpAddressList: number[] = []; let encodedSpAddressList: number[] = [];
if (this.stateId) { if (this.stateId) {
const state = process.states.find(state => state.state_id === this.stateId); const state = process.states.find((state) => state.state_id === this.stateId);
if (state) { if (state) {
encodedSpAddressList = state.public_data['pairedAddresses']; encodedSpAddressList = state.public_data['pairedAddresses'];
} }
@ -850,7 +837,7 @@ export default class Services {
try { try {
const prevDevice = await this.getDeviceFromDatabase(); const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) { if (prevDevice) {
await db.deleteObject(walletStore, "1"); await db.deleteObject(walletStore, '1');
} }
await db.addObject({ await db.addObject({
storeName: walletStore, storeName: walletStore,
@ -1227,7 +1214,7 @@ export default class Services {
let hasAccess = false; let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore // If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) { for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) { for (const rule of Object.values(role.validation_rules) as any[]) {
if (rule.fields.includes(attribute)) { if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) { if (role.members.includes(pairingProcessId)) {
// We have access to this attribute // We have access to this attribute
@ -1248,7 +1235,7 @@ export default class Services {
let retries = 0; let retries = 0;
while ((!hash || !key) && retries < maxRetries) { while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay)); await new Promise((resolve) => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting // Re-read hash and key after waiting
hash = state.pcd_commitment[attribute]; hash = state.pcd_commitment[attribute];
key = state.keys[attribute]; key = state.keys[attribute];
@ -1365,7 +1352,6 @@ export default class Services {
this.device2Ready = false; this.device2Ready = false;
} }
// Handle the handshake message // Handle the handshake message
public async handleHandshakeMsg(url: string, parsedMsg: any) { public async handleHandshakeMsg(url: string, parsedMsg: any) {
try { try {
@ -1403,10 +1389,12 @@ export default class Services {
const existing = await this.getProcess(processId); const existing = await this.getProcess(processId);
if (existing) { if (existing) {
// Look for state id we don't know yet // Look for state id we don't know yet
let new_states = []; let new_states: string[] = [];
let roles = []; let roles: Record<string, RoleDefinition>[] = [];
for (const state of process.states) { for (const state of process.states) {
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; } if (!state.state_id || state.state_id === EMPTY32BYTES) {
continue;
}
if (!this.lookForStateId(existing, state.state_id)) { if (!this.lookForStateId(existing, state.state_id)) {
if (this.rolesContainsUs(state.roles)) { if (this.rolesContainsUs(state.roles)) {
new_states.push(state.state_id); new_states.push(state.state_id);
@ -1454,7 +1442,7 @@ export default class Services {
await this.batchSaveProcessesToDb(toSave); await this.batchSaveProcessesToDb(toSave);
} }
}, 500) }, 500);
} catch (e) { } catch (e) {
console.error('Failed to parse init message:', e); console.error('Failed to parse init message:', e);
} }
@ -1507,9 +1495,7 @@ export default class Services {
* @returns Un tableau contenant tous les membres * @returns Un tableau contenant tous les membres
*/ */
public getAllMembersSorted(): Record<string, Member> { public getAllMembersSorted(): Record<string, Member> {
return Object.fromEntries( return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
);
} }
public getAllMembers(): Record<string, Member> { public getAllMembers(): Record<string, Member> {
@ -1525,10 +1511,14 @@ export default class Services {
} }
public compareMembers(memberA: string[], memberB: string[]): boolean { public compareMembers(memberA: string[], memberB: string[]): boolean {
if (!memberA || !memberB) { return false } if (!memberA || !memberB) {
if (memberA.length !== memberB.length) { return false } return false;
}
if (memberA.length !== memberB.length) {
return false;
}
const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item)); const res = memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
return res; return res;
} }
@ -1537,16 +1527,14 @@ export default class Services {
const content = JSON.parse(response); const content = JSON.parse(response);
const error = content.error; const error = content.error;
const errorMsg = error['GenericError']; const errorMsg = error['GenericError'];
const dontRetry = [ const dontRetry = ['State is identical to the previous state', 'Not enough valid proofs', 'Not enough members to validate'];
'State is identical to the previous state', if (dontRetry.includes(errorMsg)) {
'Not enough valid proofs', return;
'Not enough members to validate', }
];
if (dontRetry.includes(errorMsg)) { return; }
// Wait and retry // Wait and retry
setTimeout(async () => { setTimeout(async () => {
this.sendCommitMessage(JSON.stringify(content)); this.sendCommitMessage(JSON.stringify(content));
}, 1000) }, 1000);
} }
public getRoles(process: Process): Record<string, RoleDefinition> | null { public getRoles(process: Process): Record<string, RoleDefinition> | null {
@ -1579,8 +1567,11 @@ export default class Services {
const lastCommitedState = this.getLastCommitedState(process); const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) { if (lastCommitedState && lastCommitedState.public_data) {
const processName = lastCommitedState!.public_data['processName']; const processName = lastCommitedState!.public_data['processName'];
if (processName) { return this.decodeValue(processName) } if (processName) {
else { return null } return this.decodeValue(processName);
} else {
return null;
}
} else { } else {
return null; return null;
} }
@ -1622,7 +1613,7 @@ export default class Services {
this.myProcesses = newMyProcesses; // atomic update this.myProcesses = newMyProcesses; // atomic update
return Array.from(this.myProcesses); return Array.from(this.myProcesses);
} catch (e) { } catch (e) {
console.error("Failed to get processes:", e); console.error('Failed to get processes:', e);
return null; return null;
} }
} }
@ -1640,13 +1631,16 @@ export default class Services {
public hexToBlob(hexString: string): Blob { public hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString); const uint8Array = this.hexToUInt8Array(hexString);
// Crée un ArrayBuffer standard et copie les données pour éviter ArrayBufferLike/SharedArrayBuffer
return new Blob([uint8Array], { type: "application/octet-stream" }); const ab = new ArrayBuffer(uint8Array.length);
const view = new Uint8Array(ab);
view.set(uint8Array);
return new Blob([ab], { type: 'application/octet-stream' });
} }
public hexToUInt8Array(hexString: string): Uint8Array { public hexToUInt8Array(hexString: string): Uint8Array {
if (hexString.length % 2 !== 0) { if (hexString.length % 2 !== 0) {
throw new Error("Invalid hex string: length must be even"); throw new Error('Invalid hex string: length must be even');
} }
const uint8Array = new Uint8Array(hexString.length / 2); const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) { for (let i = 0; i < hexString.length; i += 2) {
@ -1660,7 +1654,7 @@ export default class Services {
const buffer = await blob.arrayBuffer(); const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
return Array.from(bytes) return Array.from(bytes)
.map(byte => byte.toString(16).padStart(2, '0')) .map((byte) => byte.toString(16).padStart(2, '0'))
.join(''); .join('');
} }
@ -1683,7 +1677,7 @@ export default class Services {
public getLastCommitedState(process: Process): ProcessState | null { public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip); const lastCommitedState = process.states.findLast((state: ProcessState) => state.commited_in !== processTip);
if (lastCommitedState) { if (lastCommitedState) {
return lastCommitedState; return lastCommitedState;
} else { } else {
@ -1705,13 +1699,13 @@ export default class Services {
public getUncommitedStates(process: Process): ProcessState[] { public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return []; if (process.states.length === 0) return [];
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
const res = process.states.filter(state => state.commited_in === processTip); const res = process.states.filter((state: ProcessState) => state.commited_in === processTip);
return res.filter(state => state.state_id !== EMPTY32BYTES); return res.filter((state: ProcessState) => state.state_id !== EMPTY32BYTES);
} }
public getStateFromId(process: Process, stateId: string): ProcessState | null { public getStateFromId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const state = process.states.find(state => state.state_id === stateId); const state = process.states.find((state: ProcessState) => state.state_id === stateId);
if (state) { if (state) {
return state; return state;
} else { } else {
@ -1722,7 +1716,7 @@ export default class Services {
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null { public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const index = process.states.findIndex(state => state.state_id === stateId); const index = process.states.findIndex((state: ProcessState) => state.state_id === stateId);
if (index !== -1 && index < process.states.length - 1) { if (index !== -1 && index < process.states.length - 1) {
return process.states[index + 1]; return process.states[index + 1];
@ -1732,7 +1726,9 @@ export default class Services {
} }
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean { public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
if (Object.keys(roles).length != 1) { return false } if (Object.keys(roles).length != 1) {
return false;
}
const pairingRole = roles['pairing']; const pairingRole = roles['pairing'];
if (pairingRole) { if (pairingRole) {
// For now that's enough, we should probably test more things // For now that's enough, we should probably test more things
@ -1744,7 +1740,7 @@ export default class Services {
public async updateMemberPublicName(process: Process, newName: string): Promise<ApiReturn> { public async updateMemberPublicName(process: Process, newName: string): Promise<ApiReturn> {
const publicData = { const publicData = {
'memberPublicName': newName memberPublicName: newName,
}; };
return await this.updateProcess(process, {}, publicData, null); return await this.updateProcess(process, {}, publicData, null);