import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { environment } from '@src/environments/environment'; import { first } from 'rxjs/operators'; import { ArgPair, Envelope, Phone, Syncable, User } from 'cic-client-meta'; import { AccountDetails } from '@app/_models'; import { LoggingService } from '@app/_services/logging.service'; import { TokenService } from '@app/_services/token.service'; import { MutableKeyStore, PGPSigner, Signer } from '@app/_pgp'; import { RegistryService } from '@app/_services/registry.service'; import { CICRegistry } from '@cicnet/cic-client'; import { personValidation, updateSyncable, vcardValidation } from '@app/_helpers'; import { add0x } from '@src/assets/js/ethtx/dist/hex'; import { KeystoreService } from '@app/_services/keystore.service'; const vCard = require('vcard-parser'); @Injectable({ providedIn: 'root', }) export class UserService { headers: HttpHeaders = new HttpHeaders({ 'x-cic-automerge': 'client' }); keystore: MutableKeyStore; signer: Signer; registry: CICRegistry; accounts: Array = []; private accountsList: BehaviorSubject> = new BehaviorSubject< Array >(this.accounts); accountsSubject: Observable> = this.accountsList.asObservable(); actions: Array = []; private actionsList: BehaviorSubject = new BehaviorSubject(this.actions); actionsSubject: Observable> = this.actionsList.asObservable(); categories: object = {}; private categoriesList: BehaviorSubject = new BehaviorSubject(this.categories); categoriesSubject: Observable = this.categoriesList.asObservable(); constructor( private httpClient: HttpClient, private loggingService: LoggingService, private tokenService: TokenService ) {} async init(): Promise { this.keystore = await KeystoreService.getKeystore(); this.signer = new PGPSigner(this.keystore); this.registry = await RegistryService.getRegistry(); } resetPin(phone: string): Observable { const params: HttpParams = new HttpParams().set('phoneNumber', phone); return this.httpClient.put(`${environment.cicUssdUrl}/pin`, { params }); } getAccountStatus(phone: string): Observable { const params: HttpParams = new HttpParams().set('phoneNumber', phone); return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params }); } getLockedAccounts(offset: number, limit: number): Observable { return this.httpClient.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`); } async changeAccountInfo( address: string, name: string, phoneNumber: string, age: string, type: string, bio: string, gender: string, businessCategory: string, userLocation: string, location: string, locationType: string ): Promise { const accountInfo: any = { vcard: { fn: [{}], n: [{}], tel: [{}], }, location: {}, }; if (name) { accountInfo.vcard.fn[0].value = name; accountInfo.vcard.n[0].value = name.split(' '); } if (phoneNumber) { accountInfo.vcard.tel[0].value = phoneNumber; } if (bio) { accountInfo.products = [bio]; } if (gender) { accountInfo.gender = gender; } if (age) { accountInfo.age = age; } if (type) { accountInfo.type = type; } if (businessCategory) { accountInfo.category = businessCategory; } if (location) { accountInfo.location.area = location; } if (userLocation) { accountInfo.location.area_name = userLocation; } if (locationType) { accountInfo.location.area_type = locationType; } await vcardValidation(accountInfo.vcard); accountInfo.vcard = btoa(vCard.generate(accountInfo.vcard)); const accountKey: string = await User.toKey(address); this.getAccountDetailsFromMeta(accountKey) .pipe(first()) .subscribe( async (res) => { const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const update: Array = []; for (const prop of Object.keys(accountInfo)) { update.push(new ArgPair(prop, accountInfo[prop])); } updateSyncable(update, 'client-branch', syncableAccount); await personValidation(syncableAccount.m.data); await this.updateMeta(syncableAccount, accountKey, this.headers); }, async (error) => { this.loggingService.sendErrorLevelMessage( 'Cannot find account info in meta service', this, { error } ); const syncableAccount: Syncable = new Syncable(accountKey, accountInfo); await this.updateMeta(syncableAccount, accountKey, this.headers); } ); return accountKey; } async updateMeta( syncableAccount: Syncable, accountKey: string, headers: HttpHeaders ): Promise { const envelope: Envelope = await this.wrap(syncableAccount, this.signer); const reqBody: string = envelope.toJSON(); this.httpClient .put(`${environment.cicMetaUrl}/${accountKey}`, reqBody, { headers }) .pipe(first()) .subscribe((res) => { this.loggingService.sendInfoLevelMessage(`Response: ${res}`); }); } getActions(): void { this.httpClient .get(`${environment.cicCacheUrl}/actions`) .pipe(first()) .subscribe((res) => this.actionsList.next(res)); } getActionById(id: string): Observable { return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`); } approveAction(id: string): Observable { return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: true }); } revokeAction(id: string): Observable { return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false }); } getAccountDetailsFromMeta(userKey: string): Observable { return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers }); } wrap(syncable: Syncable, signer: Signer): Promise { return new Promise(async (resolve, reject) => { syncable.setSigner(signer); syncable.onwrap = async (env) => { if (env === undefined) { reject(); return; } resolve(env); }; await syncable.sign(); }); } async loadAccounts(limit: number = 100, offset: number = 0): Promise { try { const accountRegistry = await RegistryService.getAccountRegistry(); const accountAddresses: Array = await accountRegistry.last(offset + limit); this.loggingService.sendInfoLevelMessage(accountAddresses); if (typeof Worker !== 'undefined') { const worker = new Worker('@app/_workers/fetch-accounts.worker', { type: 'module' }); worker.onmessage = ({ data }) => { this.tokenService.load.subscribe(async (status: boolean) => { if (status) { data.balance = await this.tokenService.getTokenBalance( data.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] ); } }); this.addAccount(data, limit); }; worker.postMessage({ addresses: accountAddresses.slice(offset, offset + limit), url: environment.cicMetaUrl, }); } else { this.loggingService.sendInfoLevelMessage( 'Web workers are not supported in this environment' ); for (const accountAddress of accountAddresses.slice(offset, offset + limit)) { await this.getAccountByAddress(accountAddress, limit); } } } catch (error) { this.loggingService.sendErrorLevelMessage('Unable to load accounts.', 'user.service', error); throw error; } } async getAccountByAddress( accountAddress: string, limit: number = 100 ): Promise> { const accountSubject: Subject = new Subject(); this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress))) .pipe(first()) .subscribe(async (res) => { const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const accountInfo = account.m.data; await personValidation(accountInfo); this.tokenService.load.subscribe(async (status: boolean) => { if (status) { accountInfo.balance = await this.tokenService.getTokenBalance( accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] ); } }); accountInfo.vcard = vCard.parse(atob(accountInfo.vcard)); await vcardValidation(accountInfo.vcard); this.addAccount(accountInfo, limit); accountSubject.next(accountInfo); }); return accountSubject.asObservable(); } async getAccountByPhone( phoneNumber: string, limit: number = 100 ): Promise> { const accountSubject: Subject = new Subject(); this.getAccountDetailsFromMeta(await Phone.toKey(phoneNumber)) .pipe(first()) .subscribe(async (res) => { const response: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const address: string = response.m.data; const account: Observable = await this.getAccountByAddress(address, limit); account.subscribe((result) => { accountSubject.next(result); }); }); return accountSubject.asObservable(); } resetAccountsList(): void { this.accounts = []; this.accountsList.next(this.accounts); } getCategories(): void { this.httpClient .get(`${environment.cicMetaUrl}/categories`) .pipe(first()) .subscribe((res: object) => this.categoriesList.next(res)); } getCategoryByProduct(product: string, categories: object): string { const keywords = product.toLowerCase().split(' '); for (const keyword of keywords) { const queriedCategory: string = Object.keys(categories).find((key) => categories[key].includes(keyword) ); if (queriedCategory) { return queriedCategory; } } return 'other'; } getAccountTypes(): Observable { return this.httpClient.get(`${environment.cicMetaUrl}/accounttypes`); } getTransactionTypes(): Observable { return this.httpClient.get(`${environment.cicMetaUrl}/transactiontypes`); } getGenders(): Observable { return this.httpClient.get(`${environment.cicMetaUrl}/genders`); } addAccount(account: AccountDetails, cacheSize: number): void { const savedIndex = this.accounts.findIndex( (acc) => acc.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] === account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] ); if (savedIndex === 0) { return; } if (savedIndex > 0) { this.accounts.splice(savedIndex, 1); } this.accounts.unshift(account); if (this.accounts.length > cacheSize) { this.accounts.length = Math.min(this.accounts.length, cacheSize); } this.accountsList.next(this.accounts); } }