From f228a9866b9ec4ebfb29ad32c1c19478fa0adea8 Mon Sep 17 00:00:00 2001 From: Spencer Ofwiti Date: Wed, 25 Nov 2020 10:51:15 +0300 Subject: [PATCH] Add services. - Add block sync service. - Add mock token service. - Add mock user service. - Add user info to transactions. --- src/app/_services/block-sync.service.spec.ts | 22 ++++ src/app/_services/block-sync.service.ts | 108 ++++++++++++++++++ src/app/_services/index.ts | 4 + src/app/_services/token.service.spec.ts | 24 ++++ src/app/_services/token.service.ts | 27 +++++ src/app/_services/transaction.service.spec.ts | 20 +++- src/app/_services/transaction.service.ts | 51 ++++++++- src/app/_services/user.service.spec.ts | 80 +++++++++++++ src/app/_services/user.service.ts | 69 +++++++++++ 9 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 src/app/_services/block-sync.service.spec.ts create mode 100644 src/app/_services/block-sync.service.ts create mode 100644 src/app/_services/index.ts create mode 100644 src/app/_services/token.service.spec.ts create mode 100644 src/app/_services/token.service.ts create mode 100644 src/app/_services/user.service.spec.ts create mode 100644 src/app/_services/user.service.ts diff --git a/src/app/_services/block-sync.service.spec.ts b/src/app/_services/block-sync.service.spec.ts new file mode 100644 index 0000000..5efe867 --- /dev/null +++ b/src/app/_services/block-sync.service.spec.ts @@ -0,0 +1,22 @@ +import { TestBed } from '@angular/core/testing'; + +import { BlockSyncService } from './block-sync.service'; +import {TransactionService} from './transaction.service'; +import {TransactionServiceStub} from '../../testing'; + +describe('BlockSyncService', () => { + let service: BlockSyncService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: TransactionService, useClass: TransactionServiceStub } + ] + }); + service = TestBed.inject(BlockSyncService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/_services/block-sync.service.ts b/src/app/_services/block-sync.service.ts new file mode 100644 index 0000000..d8a9a57 --- /dev/null +++ b/src/app/_services/block-sync.service.ts @@ -0,0 +1,108 @@ +import {Injectable} from '@angular/core'; +import {Settings} from '../_models'; +import Web3 from 'web3'; +import {abi, Registry, TransactionHelper} from 'cic-client'; +import {first} from 'rxjs/operators'; +import {TransactionService} from './transaction.service'; + +@Injectable({ + providedIn: 'root' +}) +export class BlockSyncService { + registryAddress: string = '0xb708175e3f6Cd850643aAF7B32212AFad50e2549'; + readyStateTarget: number = 3; + readyState: number = 0; + + constructor(private transactionService: TransactionService) { } + + blockSync(): any { + const settings = new Settings(this.scan); + const provider = 'ws://localhost:8545'; + const readyStateElements = { + token: 1, + network: 2, + }; + settings.w3.provider = provider; + settings.w3.engine = new Web3(provider); + settings.registry = new Registry(settings.w3.engine, this.registryAddress, abi); + settings.txHelper = new TransactionHelper(settings.registry); + + settings.txHelper.ontransfer = async (transaction: any): Promise => { + window.dispatchEvent(this.newTransferEvent(transaction)); + }; + settings.txHelper.onconversion = async (transaction: any): Promise => { + window.dispatchEvent(this.newConversionEvent(transaction)); + }; + settings.registry.ontokensload = (tokenCount: number): void => { + // console.debug('loaded tokens', tokenCount); + this.readyStateProcessor(settings, readyStateElements.token); + }; + settings.registry.onregistryload = (addressReturned: number): void => { + // console.debug('loaded network contracts', addressReturned); + this.readyStateProcessor(settings, readyStateElements.network); + }; + + settings.registry.load(); + } + + readyStateProcessor(settings, bit): void { + this.readyState |= bit; + if (this.readyStateTarget === this.readyState && this.readyStateTarget) { + // console.log('reached readyState target', this.readyStateTarget); + this.fetcher(settings); + } + } + + newTransferEvent(tx): any { + return new CustomEvent('cic_transfer', { + detail: { + tx, + }, + }); + } + + newConversionEvent(tx): any { + return new CustomEvent('cic_convert', { + detail: { + tx, + }, + }); + } + + async scan(settings, lo, hi, bloomBlockBytes, bloomBlocktxBytes, bloomRounds): Promise { + const w = new Worker('./../assets/js/worker.js'); + w.onmessage = (m) => { + settings.txHelper.processReceipt(m.data); + }; + w.postMessage({ + w3_provider: settings.w3.provider, + lo, + hi, + filters: [ + bloomBlockBytes, + bloomBlocktxBytes, + ], + filter_rounds: bloomRounds, + }); + } + + fetcher(settings: any, offset: number = 0, limit: number = 100): void { + this.transactionService.getAllTransactions(offset, limit).pipe(first()).subscribe(data => { + const blockFilterBinstr = window.atob(data.block_filter); + const bOne = new Uint8Array(blockFilterBinstr.length); + bOne.map((e, i, v) => v[i] = blockFilterBinstr.charCodeAt(i)); + + const blocktxFilterBinstr = window.atob(data.blocktx_filter); + const bTwo = new Uint8Array(blocktxFilterBinstr.length); + bTwo.map((e, i, v) => v[i] = blocktxFilterBinstr.charCodeAt(i)); + + for (let i = 0; i < blockFilterBinstr.length; i++) { + if (bOne[i] > 0 ) { + // console.debug('blocktx value on', i); + } + } + + settings.scanFilter(settings, data.low, data.high, bOne, bTwo, data.filter_rounds); + }); + } +} diff --git a/src/app/_services/index.ts b/src/app/_services/index.ts new file mode 100644 index 0000000..802b6c6 --- /dev/null +++ b/src/app/_services/index.ts @@ -0,0 +1,4 @@ +export * from './transaction.service'; +export * from './user.service'; +export * from './token.service'; +export * from './block-sync.service'; diff --git a/src/app/_services/token.service.spec.ts b/src/app/_services/token.service.spec.ts new file mode 100644 index 0000000..2d8ff6e --- /dev/null +++ b/src/app/_services/token.service.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing'; + +import { TokenService } from './token.service'; + +describe('TokenService', () => { + let service: TokenService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TokenService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return token for available token', () => { + expect(service.getBySymbol('RSV')).toEqual({ name: 'Reserve', symbol: 'RSV' }); + }); + + it('should not return token for unavailable token', () => { + expect(service.getBySymbol('ABC')).toBeUndefined(); + }); +}); diff --git a/src/app/_services/token.service.ts b/src/app/_services/token.service.ts new file mode 100644 index 0000000..50c66de --- /dev/null +++ b/src/app/_services/token.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class TokenService { + data = [ + { name: 'Reserve', symbol: 'RSV' }, + { name: 'Bert', symbol: 'BRT' }, + { name: 'Ernie', symbol: 'ERN' }, + { name: 'Reserve', symbol: 'RSV' }, + { name: 'Bert', symbol: 'BRT' }, + { name: 'Ernie', symbol: 'ERN' }, + { name: 'Reserve', symbol: 'RSV' }, + { name: 'Bert', symbol: 'BRT' }, + { name: 'Ernie', symbol: 'ERN' }, + { name: 'Reserve', symbol: 'RSV' }, + { name: 'Bert', symbol: 'BRT' }, + { name: 'Ernie', symbol: 'ERN' }, + ]; + + constructor() { } + + getBySymbol(symbol: string): any { + return this.data.find(token => token.symbol === symbol); + } +} diff --git a/src/app/_services/transaction.service.spec.ts b/src/app/_services/transaction.service.spec.ts index 82b19f8..ef09b9c 100644 --- a/src/app/_services/transaction.service.spec.ts +++ b/src/app/_services/transaction.service.spec.ts @@ -1,16 +1,32 @@ -import { TestBed } from '@angular/core/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; import { TransactionService } from './transaction.service'; +import {HttpClient} from '@angular/common/http'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; describe('TransactionService', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; let service: TransactionService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + }); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(TransactionService); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + // it('#getUser() should fetch userInfo', fakeAsync(() => { + // expect(service.userInfo).toBeUndefined(); + // service.getUser('0x9D7c284907acbd4a0cE2dDD0AA69147A921a573D').then( + // ); + // tick(); + // expect(service.userInfo).toBe('Hey'); + // })); }); diff --git a/src/app/_services/transaction.service.ts b/src/app/_services/transaction.service.ts index 894ac8a..bf027ca 100644 --- a/src/app/_services/transaction.service.ts +++ b/src/app/_services/transaction.service.ts @@ -1,9 +1,12 @@ import { Injectable } from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {map} from 'rxjs/operators'; -import {Observable} from 'rxjs'; +import {first, map} from 'rxjs/operators'; +import {BehaviorSubject, Observable} from 'rxjs'; import {environment} from '../../environments/environment'; import {Conversion, Transaction} from '../_models'; +import {User} from 'cic-client-meta'; +import {UserService} from './user.service'; +import {parse} from '../../assets/js/parse-vcard'; @Injectable({ providedIn: 'root' @@ -11,8 +14,16 @@ import {Conversion, Transaction} from '../_models'; export class TransactionService { transactions: Transaction[] = []; conversions: Conversion[] = []; + private transactionList = new BehaviorSubject(this.transactions); + transactionsSubject = this.transactionList.asObservable(); + private conversionList = new BehaviorSubject(this.conversions); + conversionsSubject = this.conversionList.asObservable(); + userInfo: any; - constructor(private http: HttpClient) { } + constructor( + private http: HttpClient, + private userService: UserService + ) { } getAllTransactions(offset: number, limit: number): Observable { return this.http.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`) @@ -28,11 +39,39 @@ export class TransactionService { })); } - setTransaction(transaction): void { - this.transactions.push(transaction); + setTransaction(transaction, cacheSize: number): void { + const cachedTransaction = this.transactions.find(cachedTx => cachedTx.tx.txHash === transaction.tx.txHash); + if (cachedTransaction) { return; } + this.getUser(transaction.from).then(() => { + transaction.sender = this.userInfo; + this.getUser(transaction.to).then(() => { + transaction.recipient = this.userInfo; + this.transactions.unshift(transaction); + if (this.transactions.length > cacheSize) { + this.transactions.length = cacheSize; + } + this.transactionList.next(this.transactions); + }); + }); } setConversion(conversion): void { - this.conversions.push(conversion); + const cachedConversion = this.conversions.find(cachedTx => cachedTx.tx.txHash === conversion.tx.txHash); + if (cachedConversion) { return; } + this.getUser(conversion.trader).then(() => { + conversion.user = this.userInfo; + this.conversions.push(conversion); + this.conversionList.next(this.conversions); + }); + } + + async getUser(address: string): Promise { + this.userService.getUser(await User.toKey(address)).pipe(first()).subscribe(res => { + const vcard = parse(atob(res.vcard)); + res.vcard = vcard; + this.userInfo = res; + }, error => { + console.log(error); + }); } } diff --git a/src/app/_services/user.service.spec.ts b/src/app/_services/user.service.spec.ts new file mode 100644 index 0000000..83439c6 --- /dev/null +++ b/src/app/_services/user.service.spec.ts @@ -0,0 +1,80 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; +import {HttpClient} from '@angular/common/http'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; + +describe('UserService', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + }); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return user for available id', () => { + expect(service.getUserById('1')).toEqual({ + id: 1, + name: 'John Doe', + phone: '+25412345678', + address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', + type: 'user', + created: '08/16/2020', + balance: '12987', + failedPinAttempts: 1, + status: 'approved', + bio: 'Bodaboda', + gender: 'male' + }); + }); + + it('should not return user for unavailable id', () => { + expect(service.getUserById('9999999999')).toBeUndefined(); + }); + + it('should return action for available id', () => { + expect(service.getActionById('1')).toEqual({ + id: 1, + user: 'Tom', + role: 'enroller', + action: 'Disburse RSV 100', + approval: false + }); + }); + + it('should not return action for unavailable id', () => { + expect(service.getActionById('9999999999')).toBeUndefined(); + }); + + it('should switch action approval from false to true', () => { + service.approveAction('1'); + expect(service.getActionById('1')).toEqual({ + id: 1, + user: 'Tom', + role: 'enroller', + action: 'Disburse RSV 100', + approval: true + }); + }); + + it('should switch action approval from true to false', () => { + service.revokeAction('2'); + expect(service.getActionById('2')).toEqual({ + id: 2, + user: 'Christine', + role: 'admin', + action: 'Change user phone number', + approval: false + }); + }); +}); diff --git a/src/app/_services/user.service.ts b/src/app/_services/user.service.ts new file mode 100644 index 0000000..c55b924 --- /dev/null +++ b/src/app/_services/user.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; +import {Observable} from 'rxjs'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {environment} from '../../environments/environment'; +import {map} from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + users = [ + {id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'approved', bio: 'Bodaboda', gender: 'male'}, + {id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'vendor', created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'approved', bio: 'Groceries', gender: 'female'}, + {id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'group', created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'unapproved', bio: 'Food', gender: 'male'}, + {id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'approved', bio: 'Shop', gender: 'female'}, + {id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'token agent', created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'unapproved', bio: 'Electronics', gender: 'male'}, + {id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'approved', bio: 'Bodaboda', gender: 'male'}, + {id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'vendor', created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'approved', bio: 'Groceries', gender: 'female'}, + {id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'group', created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'unapproved', bio: 'Food', gender: 'male'}, + {id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'approved', bio: 'Shop', gender: 'female'}, + {id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'token agent', created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'unapproved', bio: 'Electronics', gender: 'male'}, + {id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'approved', bio: 'Bodaboda', gender: 'male'}, + {id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'vendor', created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'approved', bio: 'Groceries', gender: 'female'}, + {id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'group', created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'unapproved', bio: 'Food', gender: 'male'}, + {id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'approved', bio: 'Shop', gender: 'female'}, + {id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'token agent', created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'unapproved', bio: 'Electronics', gender: 'male'}, + {id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'approved', bio: 'Bodaboda', gender: 'male'}, + {id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'vendor', created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'approved', bio: 'Groceries', gender: 'female'}, + {id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'group', created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'unapproved', bio: 'Food', gender: 'male'}, + {id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'approved', bio: 'Shop', gender: 'female'}, + {id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'token agent', created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'unapproved', bio: 'Electronics', gender: 'male'}, + ]; + actions = [ + { id: 1, user: 'Tom', role: 'enroller', action: 'Disburse RSV 100', approval: false }, + { id: 2, user: 'Christine', role: 'admin', action: 'Change user phone number', approval: true }, + { id: 3, user: 'Will', role: 'superadmin', action: 'Reclaim RSV 1000', approval: true }, + { id: 4, user: 'Vivian', role: 'enroller', action: 'Complete user profile', approval: true }, + { id: 5, user: 'Jack', role: 'enroller', action: 'Reclaim RSV 200', approval: false }, + { id: 6, user: 'Patience', role: 'enroller', action: 'Change user information', approval: false } + ]; + + constructor(private http: HttpClient) { } + + getUser(userKey: string): Observable { + const params = new HttpParams().set('userKey', '0970c6e9cdad650ba9006e5a1caf090a13da312792389a147263c98ac78cd037'); + return this.http.get(`${environment.cicScriptsUrl}`, { params }) + .pipe(map(response => { + return response; + })); + } + + getUserById(id: string): any { + return this.users.find(user => user.id === parseInt(id, 10)); + } + + getActionById(id: string): any { + return this.actions.find(action => action.id === parseInt(id, 10)); + } + + approveAction(id: string): void { + const action = this.actions.find(queriedAction => queriedAction.id === parseInt(id, 10)); + action.approval = true; + } + + revokeAction(id: string): void { + const action = this.actions.find(queriedAction => queriedAction.id === parseInt(id, 10)); + action.approval = false; + } +}