Merge branch 'master' into spencer/keystore-singleton

# Conflicts:
#	src/app/_services/transaction.service.ts
This commit is contained in:
Spencer Ofwiti
2021-06-14 16:37:21 +03:00
470 changed files with 130336 additions and 1535 deletions

View File

@@ -1,14 +1,36 @@
// Third party imports
import Web3 from 'web3';
import { Web3Service } from '@app/_services/web3.service';
// Application imports
import { Web3Service } from '@app/_services/web3.service';
import { environment } from '@src/environments/environment';
/** Fetch the account registry contract's ABI. */
const abi: Array<any> = require('@src/assets/js/block-sync/data/AccountsIndex.json');
/** Establish a connection to the blockchain network. */
const web3: Web3 = Web3Service.getInstance();
/**
* Provides an instance of the accounts registry contract.
* Allows querying of accounts that have been registered as valid accounts in the network.
*
* @remarks
* This is our interface to the accounts registry contract.
*/
export class AccountIndex {
contractAddress: string;
signerAddress: string;
/** The instance of the account registry contract. */
contract: any;
/** The deployed account registry contract's address. */
contractAddress: string;
/** The account address of the account that deployed the account registry contract. */
signerAddress: string;
/**
* Create a connection to the deployed account registry contract.
*
* @param contractAddress - The deployed account registry contract's address.
* @param signerAddress - The account address of the account that deployed the account registry contract.
*/
constructor(contractAddress: string, signerAddress?: string) {
this.contractAddress = contractAddress;
this.contract = new web3.eth.Contract(abi, this.contractAddress);
@@ -19,14 +41,58 @@ export class AccountIndex {
}
}
public async totalAccounts(): Promise<number> {
return await this.contract.methods.entryCount().call();
/**
* Registers an account to the accounts registry.
* Requires availability of the signer address.
*
* @async
* @example
* Prints "true" for registration of '0xc0ffee254729296a45a3885639AC7E10F9d54979':
* ```typescript
* console.log(await addToAccountRegistry('0xc0ffee254729296a45a3885639AC7E10F9d54979'));
* ```
*
* @param address - The account address to be registered to the accounts registry contract.
* @returns true - If registration is successful or account had already been registered.
*/
public async addToAccountRegistry(address: string): Promise<boolean> {
if (!(await this.haveAccount(address))) {
return await this.contract.methods.add(address).send({ from: this.signerAddress });
}
return true;
}
/**
* Checks whether a specific account address has been registered in the accounts registry.
* Returns "true" for available and "false" otherwise.
*
* @async
* @example
* Prints "true" or "false" depending on whether '0xc0ffee254729296a45a3885639AC7E10F9d54979' has been registered:
* ```typescript
* console.log(await haveAccount('0xc0ffee254729296a45a3885639AC7E10F9d54979'));
* ```
*
* @param address - The account address to be validated.
* @returns true - If the address has been registered in the accounts registry.
*/
public async haveAccount(address: string): Promise<boolean> {
return (await this.contract.methods.have(address).call()) !== 0;
}
/**
* Returns a specified number of the most recently registered accounts.
*
* @async
* @example
* Prints an array of accounts:
* ```typescript
* console.log(await last(5));
* ```
*
* @param numberOfAccounts - The number of accounts to return from the accounts registry.
* @returns An array of registered account addresses.
*/
public async last(numberOfAccounts: number): Promise<Array<string>> {
const count: number = await this.totalAccounts();
let lowest: number = count - numberOfAccounts;
@@ -41,10 +107,19 @@ export class AccountIndex {
return accounts;
}
public async addToAccountRegistry(address: string): Promise<boolean> {
if (!(await this.haveAccount(address))) {
return await this.contract.methods.add(address).send({ from: this.signerAddress });
}
return true;
/**
* Returns the total number of accounts that have been registered in the network.
*
* @async
* @example
* Prints the total number of registered accounts:
* ```typescript
* console.log(await totalAccounts());
* ```
*
* @returns The total number of registered accounts.
*/
public async totalAccounts(): Promise<number> {
return await this.contract.methods.entryCount().call();
}
}

View File

@@ -1,3 +1,4 @@
// Application imports
import { TokenRegistry } from '@app/_eth/token-registry';
import { environment } from '@src/environments/environment';

View File

@@ -1,14 +1,36 @@
// Third party imports
import Web3 from 'web3';
// Application imports
import { environment } from '@src/environments/environment';
import { Web3Service } from '@app/_services/web3.service';
/** Fetch the token registry contract's ABI. */
const abi: Array<any> = require('@src/assets/js/block-sync/data/TokenUniqueSymbolIndex.json');
/** Establish a connection to the blockchain network. */
const web3: Web3 = Web3Service.getInstance();
/**
* Provides an instance of the token registry contract.
* Allows querying of tokens that have been registered as valid tokens in the network.
*
* @remarks
* This is our interface to the token registry contract.
*/
export class TokenRegistry {
contractAddress: string;
signerAddress: string;
/** The instance of the token registry contract. */
contract: any;
/** The deployed token registry contract's address. */
contractAddress: string;
/** The account address of the account that deployed the token registry contract. */
signerAddress: string;
/**
* Create a connection to the deployed token registry contract.
*
* @param contractAddress - The deployed token registry contract's address.
* @param signerAddress - The account address of the account that deployed the token registry contract.
*/
constructor(contractAddress: string, signerAddress?: string) {
this.contractAddress = contractAddress;
this.contract = new web3.eth.Contract(abi, this.contractAddress);
@@ -19,16 +41,54 @@ export class TokenRegistry {
}
}
public async totalTokens(): Promise<number> {
return await this.contract.methods.entryCount().call();
}
public async entry(serial: number): Promise<string> {
return await this.contract.methods.entry(serial).call();
}
/**
* Returns the address of the token with a given identifier.
*
* @async
* @example
* Prints the address of the token with the identifier 'sarafu':
* ```typescript
* console.log(await addressOf('sarafu'));
* ```
*
* @param identifier - The name or identifier of the token to be fetched from the token registry.
* @returns The address of the token assigned the specified identifier in the token registry.
*/
public async addressOf(identifier: string): Promise<string> {
const id: string = web3.eth.abi.encodeParameter('bytes32', web3.utils.toHex(identifier));
return await this.contract.methods.addressOf(id).call();
}
/**
* Returns the address of a token with the given serial in the token registry.
*
* @async
* @example
* Prints the address of the token with the serial '2':
* ```typescript
* console.log(await entry(2));
* ```
*
* @param serial - The serial number of the token to be fetched.
* @return The address of the token with the specified serial number.
*/
public async entry(serial: number): Promise<string> {
return await this.contract.methods.entry(serial).call();
}
/**
* Returns the total number of tokens that have been registered in the network.
*
* @async
* @example
* Prints the total number of registered tokens:
* ```typescript
* console.log(await totalTokens());
* ```
*
* @returns The total number of registered tokens.
*/
public async totalTokens(): Promise<number> {
return await this.contract.methods.entryCount().call();
}
}

View File

@@ -1,5 +1,7 @@
// Core imports
import { TestBed } from '@angular/core/testing';
// Application imports
import { AuthGuard } from '@app/_guards/auth.guard';
describe('AuthGuard', () => {

View File

@@ -1,19 +1,40 @@
// Core imports
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
Router,
} from '@angular/router';
// Third party imports
import { Observable } from 'rxjs';
/**
* Auth guard implementation.
* Dictates access to routes depending on the authentication status.
*/
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
/**
* Instantiates the auth guard class.
*
* @param router - A service that provides navigation among views and URL manipulation capabilities.
*/
constructor(private router: Router) {}
/**
* Returns whether navigation to a specific route is acceptable.
* Checks if the user has uploaded a private key.
*
* @param route - Contains the information about a route associated with a component loaded in an outlet at a particular moment in time.
* ActivatedRouteSnapshot can also be used to traverse the router state tree.
* @param state - Represents the state of the router at a moment in time.
* @returns true - If there is an active private key in the user's localStorage.
*/
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot

View File

@@ -1,5 +1,7 @@
// Core imports
import { TestBed } from '@angular/core/testing';
// Application imports
import { RoleGuard } from '@app/_guards/role.guard';
describe('RoleGuard', () => {

View File

@@ -1,19 +1,40 @@
// Core imports
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
Router,
} from '@angular/router';
// Third party imports
import { Observable } from 'rxjs';
/**
* Role guard implementation.
* Dictates access to routes depending on the user's role.
*/
@Injectable({
providedIn: 'root',
})
export class RoleGuard implements CanActivate {
/**
* Instantiates the role guard class.
*
* @param router - A service that provides navigation among views and URL manipulation capabilities.
*/
constructor(private router: Router) {}
/**
* Returns whether navigation to a specific route is acceptable.
* Checks if the user has the required role to access the route.
*
* @param route - Contains the information about a route associated with a component loaded in an outlet at a particular moment in time.
* ActivatedRouteSnapshot can also be used to traverse the router state tree.
* @param state - Represents the state of the router at a moment in time.
* @returns true - If the user's role matches with accepted roles.
*/
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot

View File

@@ -1,5 +1,18 @@
/**
* Returns the sum of all values in an array.
*
* @example
* Prints 6 for the array [1, 2, 3]:
* ```typescript
* console.log(arraySum([1, 2, 3]));
* ```
*
* @param arr - An array of numbers.
* @return The sum of all values in the array.
*/
function arraySum(arr: Array<number>): number {
return arr.reduce((accumulator, current) => accumulator + current, 0);
}
/** @exports */
export { arraySum };

View File

@@ -1,3 +1,15 @@
/**
* Copies set text to clipboard.
*
* @example
* copies 'Hello World!' to the clipboard and prints "true":
* ```typescript
* console.log(copyToClipboard('Hello World!'));
* ```
*
* @param text - The text to be copied to the clipboard.
* @returns true - If the copy operation is successful.
*/
function copyToClipboard(text: any): boolean {
// create our hidden div element
const hiddenCopy: HTMLDivElement = document.createElement('div');
@@ -48,4 +60,5 @@ function copyToClipboard(text: any): boolean {
return true;
}
/** @exports */
export { copyToClipboard };

View File

@@ -1,3 +1,4 @@
// Application imports
import { CustomErrorStateMatcher } from '@app/_helpers/custom-error-state-matcher';
describe('CustomErrorStateMatcher', () => {

View File

@@ -1,7 +1,19 @@
// Core imports
import { ErrorStateMatcher } from '@angular/material/core';
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
/**
* Custom provider that defines how form controls behave with regards to displaying error messages.
*
*/
export class CustomErrorStateMatcher implements ErrorStateMatcher {
/**
* Checks whether an invalid input has been made and an error should be made.
*
* @param control - Tracks the value and validation status of an individual form control.
* @param form - Binding of an existing FormGroup to a DOM element.
* @returns true - If an invalid input has been made to the form control.
*/
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted: boolean = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));

View File

@@ -1,3 +1,4 @@
// Application imports
import { CustomValidator } from '@app/_helpers/custom.validator';
describe('Custom.Validator', () => {

View File

@@ -1,6 +1,15 @@
// Core imports
import { AbstractControl, ValidationErrors } from '@angular/forms';
/**
* Provides methods to perform custom validation to form inputs.
*/
export class CustomValidator {
/**
* Sets errors to the confirm password input field if it does not match with the value in the password input field.
*
* @param control - The control object of the form being validated.
*/
static passwordMatchValidator(control: AbstractControl): void {
const password: string = control.get('password').value;
const confirmPassword: string = control.get('confirmPassword').value;
@@ -9,6 +18,13 @@ export class CustomValidator {
}
}
/**
* Sets errors to a form field if it does not match with the regular expression given.
*
* @param regex - The regular expression to match with the form field.
* @param error - Defines the map of errors to return from failed validation checks.
* @returns The map of errors returned from failed validation checks.
*/
static patternValidator(regex: RegExp, error: ValidationErrors): ValidationErrors | null {
return (control: AbstractControl): { [key: string]: any } => {
if (!control.value) {

View File

@@ -1,3 +1,11 @@
/**
* Exports data to a CSV format and provides a download file.
*
* @param arrayData - An array of data to be converted to CSV format.
* @param filename - The name of the file to be downloaded.
* @param delimiter - The delimiter to be used when converting to CSV format.
* Defaults to commas.
*/
function exportCsv(arrayData: Array<any>, filename: string, delimiter: string = ','): void {
if (arrayData === undefined || arrayData.length === 0) {
alert('No data to be exported!');
@@ -26,11 +34,5 @@ function exportCsv(arrayData: Array<any>, filename: string, delimiter: string =
downloadLink.click();
}
function removeSpecialChar(str: string): string {
if (str === null || str === '') {
return '';
}
return str.replace(/[^a-zA-Z0-9 ]/g, '');
}
/** @exports */
export { exportCsv };

View File

@@ -1,4 +1,5 @@
import { GlobalErrorHandler } from './global-error-handler';
// Application imports
import { GlobalErrorHandler } from '@app/_helpers/global-error-handler';
describe('GlobalErrorHandler', () => {
it('should create an instance', () => {

View File

@@ -1,11 +1,26 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { LoggingService } from '@app/_services/logging.service';
// Core imports
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable } from '@angular/core';
import { Router } from '@angular/router';
// A generalized http response error
// Application imports
import { LoggingService } from '@app/_services/logging.service';
/**
* A generalized http response error.
*
* @extends Error
*/
export class HttpError extends Error {
/** The error's status code. */
public status: number;
/**
* Initialize the HttpError class.
*
* @param message - The message given by the error.
* @param status - The status code given by the error.
*/
constructor(message: string, status: number) {
super(message);
this.status = status;
@@ -13,14 +28,33 @@ export class HttpError extends Error {
}
}
/**
* Provides a hook for centralized exception handling.
*
* @extends ErrorHandler
*/
@Injectable()
export class GlobalErrorHandler extends ErrorHandler {
/**
* An array of sentence sections that denote warnings.
*/
private sentencesForWarningLogging: Array<string> = [];
/**
* Initialization of the Global Error Handler.
*
* @param loggingService - A service that provides logging capabilities.
* @param router - A service that provides navigation among views and URL manipulation capabilities.
*/
constructor(private loggingService: LoggingService, private router: Router) {
super();
}
/**
* Handles different types of errors.
*
* @param error - An error objects thrown when a runtime errors occurs.
*/
handleError(error: Error): void {
this.logError(error);
const message: string = error.message ? error.message : error.toString();
@@ -41,6 +75,32 @@ export class GlobalErrorHandler extends ErrorHandler {
throw error;
}
/**
* Checks if an error is of type warning.
*
* @param errorTraceString - A description of the error and it's stack trace.
* @returns true - If the error is of type warning.
*/
private isWarning(errorTraceString: string): boolean {
let isWarning: boolean = true;
if (errorTraceString.includes('/src/app/')) {
isWarning = false;
}
this.sentencesForWarningLogging.forEach((whiteListSentence: string) => {
if (errorTraceString.includes(whiteListSentence)) {
isWarning = true;
}
});
return isWarning;
}
/**
* Write appropriate logs according to the type of error.
*
* @param error - An error objects thrown when a runtime errors occurs.
*/
logError(error: any): void {
const route: string = this.router.url;
if (error instanceof HttpErrorResponse) {
@@ -71,21 +131,6 @@ export class GlobalErrorHandler extends ErrorHandler {
);
}
}
private isWarning(errorTraceString: string): boolean {
let isWarning: boolean = true;
if (errorTraceString.includes('/src/app/')) {
isWarning = false;
}
this.sentencesForWarningLogging.forEach((whiteListSentence: string) => {
if (errorTraceString.includes(whiteListSentence)) {
isWarning = true;
}
});
return isWarning;
}
}
export function rejectBody(error): { status: any; statusText: any } {

View File

@@ -1,7 +1,14 @@
import { rejectBody } from '@app/_helpers/global-error-handler';
/** Provides an avenue of fetching resources via HTTP calls. */
function HttpGetter(): void {}
/**
* Fetches files using HTTP get requests.
*
* @param filename - The filename to fetch.
* @returns The HTTP response text.
*/
HttpGetter.prototype.get = (filename) =>
new Promise((resolve, reject) => {
fetch(filename).then((response) => {
@@ -14,4 +21,5 @@ HttpGetter.prototype.get = (filename) =>
});
});
/** @exports */
export { HttpGetter };

View File

@@ -1,10 +1,10 @@
export * from '@app/_helpers/array-sum';
export * from '@app/_helpers/clipboard-copy';
export * from '@app/_helpers/custom.validator';
export * from '@app/_helpers/custom-error-state-matcher';
export * from '@app/_helpers/mock-backend';
export * from '@app/_helpers/array-sum';
export * from '@app/_helpers/http-getter';
export * from '@app/_helpers/global-error-handler';
export * from '@app/_helpers/export-csv';
export * from '@app/_helpers/global-error-handler';
export * from '@app/_helpers/http-getter';
export * from '@app/_helpers/mock-backend';
export * from '@app/_helpers/read-csv';
export * from '@app/_helpers/clipboard-copy';
export * from '@app/_helpers/schema-validation';

View File

@@ -1,3 +1,4 @@
// Core imports
import {
HTTP_INTERCEPTORS,
HttpEvent,
@@ -7,10 +8,18 @@ import {
HttpResponse,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
// Third party imports
import { Observable, of, throwError } from 'rxjs';
import { delay, dematerialize, materialize, mergeMap } from 'rxjs/operators';
// Application imports
import { Action, AreaName, AreaType, Category, Token } from '@app/_models';
/** A mock of the curated account types. */
const accountTypes: Array<string> = ['user', 'cashier', 'vendor', 'tokenagent', 'group'];
/** A mock of actions made by the admin staff. */
const actions: Array<Action> = [
{ 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 },
@@ -20,83 +29,335 @@ const actions: Array<Action> = [
{ id: 6, user: 'Patience', role: 'enroller', action: 'Change user information', approval: false },
];
const tokens: Array<Token> = [
/** A mock of curated area names. */
const areaNames: Array<AreaName> = [
{
name: 'Giftable Reserve',
symbol: 'GRZ',
address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E',
supply: '1000000001000000000000000000',
decimals: '18',
reserves: {},
name: 'Mukuru Nairobi',
locations: [
'kayaba',
'kayba',
'kambi',
'mukuru',
'masai',
'hazina',
'south',
'tetra',
'tetrapak',
'ruben',
'rueben',
'kingston',
'korokocho',
'kingstone',
'kamongo',
'lungalunga',
'sinai',
'sigei',
'lungu',
'lunga lunga',
'owino road',
'seigei',
],
},
{
name: 'Demo Token',
symbol: 'DEMO',
address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187',
supply: '99999999999999998976',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {
weight: '1000000',
balance: '99999999999999998976',
},
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
name: 'Kinango Kwale',
locations: [
'amani',
'bofu',
'chibuga',
'chikomani',
'chilongoni',
'chigojoni',
'chinguluni',
'chigato',
'chigale',
'chikole',
'chilongoni',
'chilumani',
'chigojoni',
'chikomani',
'chizini',
'chikomeni',
'chidzuvini',
'chidzivuni',
'chikuyu',
'chizingo',
'doti',
'dzugwe',
'dzivani',
'dzovuni',
'hanje',
'kasemeni',
'katundani',
'kibandaogo',
'kibandaongo',
'kwale',
'kinango',
'kidzuvini',
'kalalani',
'kafuduni',
'kaloleni',
'kilibole',
'lutsangani',
'peku',
'gona',
'guro',
'gandini',
'mkanyeni',
'myenzeni',
'miyenzeni',
'miatsiani',
'mienzeni',
'mnyenzeni',
'minyenzeni',
'miyani',
'mioleni',
'makuluni',
'mariakani',
'makobeni',
'madewani',
'mwangaraba',
'mwashanga',
'miloeni',
'mabesheni',
'mazeras',
'mazera',
'mlola',
'muugano',
'mulunguni',
'mabesheni',
'miatsani',
'miatsiani',
'mwache',
'mwangani',
'mwehavikonje',
'miguneni',
'nzora',
'nzovuni',
'vikinduni',
'vikolani',
'vitangani',
'viogato',
'vyogato',
'vistangani',
'yapha',
'yava',
'yowani',
'ziwani',
'majengo',
'matuga',
'vigungani',
'vidziweni',
'vinyunduni',
'ukunda',
'kokotoni',
'mikindani',
],
},
{
name: 'Foo Token',
symbol: 'FOO',
address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354',
supply: '1000000000000000001014',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {
weight: '1000000',
balance: '1000000000000000001014',
},
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
name: 'Misc Nairobi',
locations: [
'nairobi',
'west',
'lindi',
'kibera',
'kibira',
'kibra',
'makina',
'soweto',
'olympic',
'kangemi',
'ruiru',
'congo',
'kawangware',
'kwangware',
'donholm',
'dagoreti',
'dandora',
'kabete',
'sinai',
'donhom',
'donholm',
'huruma',
'kitengela',
'makadara',
',mlolongo',
'kenyatta',
'mlolongo',
'tassia',
'tasia',
'gatina',
'56',
'industrial',
'kariobangi',
'kasarani',
'kayole',
'mathare',
'pipe',
'juja',
'uchumi',
'jogoo',
'umoja',
'thika',
'kikuyu',
'stadium',
'buru buru',
'ngong',
'starehe',
'mwiki',
'fuata',
'kware',
'kabiro',
'embakassi',
'embakasi',
'kmoja',
'east',
'githurai',
'landi',
'langata',
'limuru',
'mathere',
'dagoretti',
'kirembe',
'muugano',
'mwiki',
'toi market',
],
},
{
name: 'testb',
symbol: 'tstb',
address: '0xC63cFA91A3BFf41cE31Ff436f67D3ACBC977DB95',
supply: '99000',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '99000' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
name: 'Misc Mombasa',
locations: [
'mombasa',
'likoni',
'bangla',
'bangladesh',
'kizingo',
'old town',
'makupa',
'mvita',
'ngombeni',
'ngómbeni',
'ombeni',
'magongo',
'miritini',
'changamwe',
'jomvu',
'ohuru',
'tudor',
'diani',
],
},
{
name: 'testa',
symbol: 'tsta',
address: '0x8fA4101ef19D0a078239d035659e92b278bD083C',
supply: '9981',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '9981' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
name: 'Kisauni',
locations: [
'bamburi',
'kisauni',
'mworoni',
'nyali',
'shanzu',
'bombolulu',
'mtopanga',
'mjambere',
'majaoni',
'manyani',
'magogoni',
'junda',
'mwakirunge',
'mshomoroni',
],
},
{
name: 'testc',
symbol: 'tstc',
address: '0x4A6fA6bc3BfE4C9661bC692D9798425350C9e3D4',
supply: '100990',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '100990' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
name: 'Kilifi',
locations: [
'kilfi',
'kilifi',
'mtwapa',
'takaungu',
'makongeni',
'mnarani',
'mnarani',
'office',
'g.e',
'ge',
'raibai',
'ribe',
],
},
{
name: 'Kakuma',
locations: ['kakuma'],
},
{
name: 'Kitui',
locations: ['kitui', 'mwingi'],
},
{
name: 'Nyanza',
locations: [
'busia',
'nyalgunga',
'mbita',
'siaya',
'kisumu',
'nyalenda',
'hawinga',
'rangala',
'uyoma',
'mumias',
'homabay',
'homaboy',
'migori',
'kusumu',
],
},
{
name: 'Misc Rural Counties',
locations: [
'makueni',
'meru',
'kisii',
'bomet',
'machakos',
'bungoma',
'eldoret',
'kakamega',
'kericho',
'kajiado',
'nandi',
'nyeri',
'wote',
'kiambu',
'mwea',
'nakuru',
'narok',
],
},
{
name: 'other',
locations: ['other', 'none', 'unknown'],
},
];
/** A mock of curated area types. */
const areaTypes: Array<AreaType> = [
{
name: 'urban',
area: ['urban', 'nairobi', 'mombasa'],
},
{
name: 'rural',
area: ['rural', 'kakuma', 'kwale', 'kinango', 'kitui', 'nyanza'],
},
{
name: 'periurban',
area: ['kilifi', 'periurban'],
},
{
name: 'other',
area: ['other'],
},
];
/** A mock of the user's business categories */
const categories: Array<Category> = [
{
name: 'system',
@@ -730,333 +991,88 @@ const categories: Array<Category> = [
},
];
const areaNames: Array<AreaName> = [
/** A mock of curated genders */
const genders: Array<string> = ['male', 'female', 'other'];
/** A mock of the tokens in the system. */
const tokens: Array<Token> = [
{
name: 'Mukuru Nairobi',
locations: [
'kayaba',
'kayba',
'kambi',
'mukuru',
'masai',
'hazina',
'south',
'tetra',
'tetrapak',
'ruben',
'rueben',
'kingston',
'korokocho',
'kingstone',
'kamongo',
'lungalunga',
'sinai',
'sigei',
'lungu',
'lunga lunga',
'owino road',
'seigei',
],
name: 'Giftable Reserve',
symbol: 'GRZ',
address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E',
supply: '1000000001000000000000000000',
decimals: '18',
reserves: {},
},
{
name: 'Kinango Kwale',
locations: [
'amani',
'bofu',
'chibuga',
'chikomani',
'chilongoni',
'chigojoni',
'chinguluni',
'chigato',
'chigale',
'chikole',
'chilongoni',
'chilumani',
'chigojoni',
'chikomani',
'chizini',
'chikomeni',
'chidzuvini',
'chidzivuni',
'chikuyu',
'chizingo',
'doti',
'dzugwe',
'dzivani',
'dzovuni',
'hanje',
'kasemeni',
'katundani',
'kibandaogo',
'kibandaongo',
'kwale',
'kinango',
'kidzuvini',
'kalalani',
'kafuduni',
'kaloleni',
'kilibole',
'lutsangani',
'peku',
'gona',
'guro',
'gandini',
'mkanyeni',
'myenzeni',
'miyenzeni',
'miatsiani',
'mienzeni',
'mnyenzeni',
'minyenzeni',
'miyani',
'mioleni',
'makuluni',
'mariakani',
'makobeni',
'madewani',
'mwangaraba',
'mwashanga',
'miloeni',
'mabesheni',
'mazeras',
'mazera',
'mlola',
'muugano',
'mulunguni',
'mabesheni',
'miatsani',
'miatsiani',
'mwache',
'mwangani',
'mwehavikonje',
'miguneni',
'nzora',
'nzovuni',
'vikinduni',
'vikolani',
'vitangani',
'viogato',
'vyogato',
'vistangani',
'yapha',
'yava',
'yowani',
'ziwani',
'majengo',
'matuga',
'vigungani',
'vidziweni',
'vinyunduni',
'ukunda',
'kokotoni',
'mikindani',
],
name: 'Demo Token',
symbol: 'DEMO',
address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187',
supply: '99999999999999998976',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {
weight: '1000000',
balance: '99999999999999998976',
},
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
},
{
name: 'Misc Nairobi',
locations: [
'nairobi',
'west',
'lindi',
'kibera',
'kibira',
'kibra',
'makina',
'soweto',
'olympic',
'kangemi',
'ruiru',
'congo',
'kawangware',
'kwangware',
'donholm',
'dagoreti',
'dandora',
'kabete',
'sinai',
'donhom',
'donholm',
'huruma',
'kitengela',
'makadara',
',mlolongo',
'kenyatta',
'mlolongo',
'tassia',
'tasia',
'gatina',
'56',
'industrial',
'kariobangi',
'kasarani',
'kayole',
'mathare',
'pipe',
'juja',
'uchumi',
'jogoo',
'umoja',
'thika',
'kikuyu',
'stadium',
'buru buru',
'ngong',
'starehe',
'mwiki',
'fuata',
'kware',
'kabiro',
'embakassi',
'embakasi',
'kmoja',
'east',
'githurai',
'landi',
'langata',
'limuru',
'mathere',
'dagoretti',
'kirembe',
'muugano',
'mwiki',
'toi market',
],
name: 'Foo Token',
symbol: 'FOO',
address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354',
supply: '1000000000000000001014',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {
weight: '1000000',
balance: '1000000000000000001014',
},
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
},
{
name: 'Misc Mombasa',
locations: [
'mombasa',
'likoni',
'bangla',
'bangladesh',
'kizingo',
'old town',
'makupa',
'mvita',
'ngombeni',
'ngómbeni',
'ombeni',
'magongo',
'miritini',
'changamwe',
'jomvu',
'ohuru',
'tudor',
'diani',
],
name: 'testb',
symbol: 'tstb',
address: '0xC63cFA91A3BFf41cE31Ff436f67D3ACBC977DB95',
supply: '99000',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '99000' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
},
{
name: 'Kisauni',
locations: [
'bamburi',
'kisauni',
'mworoni',
'nyali',
'shanzu',
'bombolulu',
'mtopanga',
'mjambere',
'majaoni',
'manyani',
'magogoni',
'junda',
'mwakirunge',
'mshomoroni',
],
name: 'testa',
symbol: 'tsta',
address: '0x8fA4101ef19D0a078239d035659e92b278bD083C',
supply: '9981',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '9981' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
},
{
name: 'Kilifi',
locations: [
'kilfi',
'kilifi',
'mtwapa',
'takaungu',
'makongeni',
'mnarani',
'mnarani',
'office',
'g.e',
'ge',
'raibai',
'ribe',
],
},
{
name: 'Kakuma',
locations: ['kakuma'],
},
{
name: 'Kitui',
locations: ['kitui', 'mwingi'],
},
{
name: 'Nyanza',
locations: [
'busia',
'nyalgunga',
'mbita',
'siaya',
'kisumu',
'nyalenda',
'hawinga',
'rangala',
'uyoma',
'mumias',
'homabay',
'homaboy',
'migori',
'kusumu',
],
},
{
name: 'Misc Rural Counties',
locations: [
'makueni',
'meru',
'kisii',
'bomet',
'machakos',
'bungoma',
'eldoret',
'kakamega',
'kericho',
'kajiado',
'nandi',
'nyeri',
'wote',
'kiambu',
'mwea',
'nakuru',
'narok',
],
},
{
name: 'other',
locations: ['other', 'none', 'unknown'],
name: 'testc',
symbol: 'tstc',
address: '0x4A6fA6bc3BfE4C9661bC692D9798425350C9e3D4',
supply: '100990',
decimals: '18',
reserves: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E': { weight: '1000000', balance: '100990' },
},
reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a',
},
];
const areaTypes: Array<AreaType> = [
{
name: 'urban',
area: ['urban', 'nairobi', 'mombasa'],
},
{
name: 'rural',
area: ['rural', 'kakuma', 'kwale', 'kinango', 'kitui', 'nyanza'],
},
{
name: 'periurban',
area: ['kilifi', 'periurban'],
},
{
name: 'other',
area: ['other'],
},
];
const accountTypes: Array<string> = ['user', 'cashier', 'vendor', 'tokenagent', 'group'];
/** A mock of curated transaction types. */
const transactionTypes: Array<string> = [
'transactions',
'conversions',
@@ -1064,10 +1080,20 @@ const transactionTypes: Array<string> = [
'rewards',
'reclamation',
];
const genders: Array<string> = ['male', 'female', 'other'];
/**
* Intercepts HTTP requests and handles some specified requests internally.
* Provides a backend that can handle requests for certain data items.
*/
@Injectable()
export class MockBackendInterceptor implements HttpInterceptor {
/**
* Intercepts HTTP requests.
*
* @param request - An outgoing HTTP request with an optional typed body.
* @param next - The next HTTP handler or the outgoing request dispatcher.
* @returns The response from the resolved request.
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
@@ -1079,22 +1105,17 @@ export class MockBackendInterceptor implements HttpInterceptor {
.pipe(delay(500))
.pipe(dematerialize());
/** Forward requests from select routes to their internal handlers. */
function handleRoute(): Observable<any> {
switch (true) {
case url.endsWith('/accounttypes') && method === 'GET':
return getAccountTypes();
case url.endsWith('/actions') && method === 'GET':
return getActions();
case url.match(/\/actions\/\d+$/) && method === 'GET':
return getActionById();
case url.match(/\/actions\/\d+$/) && method === 'POST':
return approveAction();
case url.endsWith('/tokens') && method === 'GET':
return getTokens();
case url.match(/\/tokens\/\w+$/) && method === 'GET':
return getTokenBySymbol();
case url.endsWith('/categories') && method === 'GET':
return getCategories();
case url.match(/\/categories\/\w+$/) && method === 'GET':
return getCategoryByProduct();
case url.endsWith('/areanames') && method === 'GET':
return getAreaNames();
case url.match(/\/areanames\/\w+$/) && method === 'GET':
@@ -1103,12 +1124,18 @@ export class MockBackendInterceptor implements HttpInterceptor {
return getAreaTypes();
case url.match(/\/areatypes\/\w+$/) && method === 'GET':
return getAreaTypeByArea();
case url.endsWith('/accounttypes') && method === 'GET':
return getAccountTypes();
case url.endsWith('/transactiontypes') && method === 'GET':
return getTransactionTypes();
case url.endsWith('/categories') && method === 'GET':
return getCategories();
case url.match(/\/categories\/\w+$/) && method === 'GET':
return getCategoryByProduct();
case url.endsWith('/genders') && method === 'GET':
return getGenders();
case url.endsWith('/tokens') && method === 'GET':
return getTokens();
case url.match(/\/tokens\/\w+$/) && method === 'GET':
return getTokenBySymbol();
case url.endsWith('/transactiontypes') && method === 'GET':
return getTransactionTypes();
default:
// pass through any requests not handled above
return next.handle(request);
@@ -1117,15 +1144,6 @@ export class MockBackendInterceptor implements HttpInterceptor {
// route functions
function getActions(): Observable<HttpResponse<any>> {
return ok(actions);
}
function getActionById(): Observable<HttpResponse<any>> {
const queriedAction: Action = actions.find((action) => action.id === idFromUrl());
return ok(queriedAction);
}
function approveAction(): Observable<HttpResponse<any>> {
const queriedAction: Action = actions.find((action) => action.id === idFromUrl());
queriedAction.approval = body.approval;
@@ -1133,25 +1151,17 @@ export class MockBackendInterceptor implements HttpInterceptor {
return ok(message);
}
function getTokens(): Observable<HttpResponse<any>> {
return ok(tokens);
function getAccountTypes(): Observable<HttpResponse<any>> {
return ok(accountTypes);
}
function getTokenBySymbol(): Observable<HttpResponse<any>> {
const queriedToken: Token = tokens.find((token) => token.symbol === stringFromUrl());
return ok(queriedToken);
function getActions(): Observable<HttpResponse<any>> {
return ok(actions);
}
function getCategories(): Observable<HttpResponse<any>> {
const categoryList: Array<string> = categories.map((category) => category.name);
return ok(categoryList);
}
function getCategoryByProduct(): Observable<HttpResponse<any>> {
const queriedCategory: Category = categories.find((category) =>
category.products.includes(stringFromUrl())
);
return ok(queriedCategory.name || 'other');
function getActionById(): Observable<HttpResponse<any>> {
const queriedAction: Action = actions.find((action) => action.id === idFromUrl());
return ok(queriedAction);
}
function getAreaNames(): Observable<HttpResponse<any>> {
@@ -1178,24 +1188,37 @@ export class MockBackendInterceptor implements HttpInterceptor {
return ok(queriedAreaType.name || 'other');
}
function getAccountTypes(): Observable<HttpResponse<any>> {
return ok(accountTypes);
function getCategories(): Observable<HttpResponse<any>> {
const categoryList: Array<string> = categories.map((category) => category.name);
return ok(categoryList);
}
function getTransactionTypes(): Observable<HttpResponse<any>> {
return ok(transactionTypes);
function getCategoryByProduct(): Observable<HttpResponse<any>> {
const queriedCategory: Category = categories.find((category) =>
category.products.includes(stringFromUrl())
);
return ok(queriedCategory.name || 'other');
}
function getGenders(): Observable<HttpResponse<any>> {
return ok(genders);
}
// helper functions
function ok(responseBody: any): Observable<HttpResponse<any>> {
return of(new HttpResponse({ status: 200, body: responseBody }));
function getTokens(): Observable<HttpResponse<any>> {
return ok(tokens);
}
function getTokenBySymbol(): Observable<HttpResponse<any>> {
const queriedToken: Token = tokens.find((token) => token.symbol === stringFromUrl());
return ok(queriedToken);
}
function getTransactionTypes(): Observable<HttpResponse<any>> {
return ok(transactionTypes);
}
// helper functions
function error(message): Observable<any> {
return throwError({ status: 400, error: { message } });
}
@@ -1205,6 +1228,10 @@ export class MockBackendInterceptor implements HttpInterceptor {
return parseInt(urlParts[urlParts.length - 1], 10);
}
function ok(responseBody: any): Observable<HttpResponse<any>> {
return of(new HttpResponse({ status: 200, body: responseBody }));
}
function stringFromUrl(): string {
const urlParts: Array<string> = url.split('/');
return urlParts[urlParts.length - 1];
@@ -1212,6 +1239,7 @@ export class MockBackendInterceptor implements HttpInterceptor {
}
}
/** Exports the MockBackendInterceptor as an Angular provider. */
export const MockBackendProvider = {
provide: HTTP_INTERCEPTORS,
useClass: MockBackendInterceptor,

View File

@@ -1,8 +1,31 @@
/** An object defining the properties of the data read. */
const objCsv: { size: number; dataFile: any } = {
size: 0,
dataFile: [],
};
/**
* Parses data to CSV format.
*
* @param data - The data to be parsed.
* @returns An array of the parsed data.
*/
function parseData(data: any): Array<any> {
const csvData: Array<any> = [];
const lineBreak: Array<any> = data.split('\n');
lineBreak.forEach((res) => {
csvData.push(res.split(','));
});
console.table(csvData);
return csvData;
}
/**
* Reads a csv file and converts it to an array.
*
* @param input - The file to be read.
* @returns An array of the read data.
*/
function readCsv(input: any): Array<any> | void {
if (input.files && input.files[0]) {
const reader: FileReader = new FileReader();
@@ -15,14 +38,5 @@ function readCsv(input: any): Array<any> | void {
}
}
function parseData(data: any): Array<any> {
const csvData: Array<any> = [];
const lineBreak: Array<any> = data.split('\n');
lineBreak.forEach((res) => {
csvData.push(res.split(','));
});
console.table(csvData);
return csvData;
}
/** @exports */
export { readCsv };

View File

@@ -1,5 +1,11 @@
// Third party imports
import { validatePerson, validateVcard } from '@cicnet/schemas-data-validator';
/**
* Validates a person object against the defined Person schema.
*
* @param person - A person object to be validated.
*/
async function personValidation(person: any): Promise<void> {
const personValidationErrors: any = await validatePerson(person);
@@ -8,6 +14,11 @@ async function personValidation(person: any): Promise<void> {
}
}
/**
* Validates a vcard object against the defined Vcard schema.
*
* @param vcard - A vcard object to be validated.
*/
async function vcardValidation(vcard: any): Promise<void> {
const vcardValidationErrors: any = await validateVcard(vcard);
@@ -16,4 +27,5 @@ async function vcardValidation(vcard: any): Promise<void> {
}
}
/** @exports */
export { personValidation, vcardValidation };

View File

@@ -1,5 +1,7 @@
// Core imports
import { TestBed } from '@angular/core/testing';
// Application imports
import { ErrorInterceptor } from '@app/_interceptors/error.interceptor';
describe('ErrorInterceptor', () => {

View File

@@ -1,24 +1,44 @@
import { Injectable } from '@angular/core';
// Core imports
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ErrorDialogService, LoggingService } from '@app/_services';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
// Third party imports
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
// Application imports
import { ErrorDialogService, LoggingService } from '@app/_services';
/** Intercepts and handles errors from outgoing HTTP request. */
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
/**
* Initialization of the error interceptor.
*
* @param errorDialogService - A service that provides a dialog box for displaying errors to the user.
* @param loggingService - A service that provides logging capabilities.
* @param router - A service that provides navigation among views and URL manipulation capabilities.
*/
constructor(
private errorDialogService: ErrorDialogService,
private loggingService: LoggingService,
private router: Router
) {}
/**
* Intercepts HTTP requests.
*
* @param request - An outgoing HTTP request with an optional typed body.
* @param next - The next HTTP handler or the outgoing request dispatcher.
* @returns The error caught from the request.
*/
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((err: HttpErrorResponse) => {

View File

@@ -1,6 +1,8 @@
// Core imports
import { TestBed } from '@angular/core/testing';
import { HttpConfigInterceptor } from './http-config.interceptor';
// Application imports
import { HttpConfigInterceptor } from '@app/_interceptors/http-config.interceptor';
describe('HttpConfigInterceptor', () => {
beforeEach(() =>

View File

@@ -1,11 +1,23 @@
// Core imports
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
// Third party imports
import { Observable } from 'rxjs';
/** Intercepts and handles setting of configurations to outgoing HTTP request. */
@Injectable()
export class HttpConfigInterceptor implements HttpInterceptor {
/** Initialization of http config interceptor. */
constructor() {}
/**
* Intercepts HTTP requests.
*
* @param request - An outgoing HTTP request with an optional typed body.
* @param next - The next HTTP handler or the outgoing request dispatcher.
* @returns The forwarded request.
*/
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// const token: string = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));

View File

@@ -1,6 +1,8 @@
// Core imports
import { TestBed } from '@angular/core/testing';
import { LoggingInterceptor } from './logging.interceptor';
// Application imports
import { LoggingInterceptor } from '@app/_interceptors/logging.interceptor';
describe('LoggingInterceptor', () => {
beforeEach(() =>

View File

@@ -1,19 +1,37 @@
import { Injectable } from '@angular/core';
// Core imports
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
// Third party imports
import { Observable } from 'rxjs';
import { LoggingService } from '@app/_services/logging.service';
import { finalize, tap } from 'rxjs/operators';
// Application imports
import { LoggingService } from '@app/_services/logging.service';
/** Intercepts and handles of events from outgoing HTTP request. */
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
/**
* Initialization of the logging interceptor.
*
* @param loggingService - A service that provides logging capabilities.
*/
constructor(private loggingService: LoggingService) {}
/**
* Intercepts HTTP requests.
*
* @param request - An outgoing HTTP request with an optional typed body.
* @param next - The next HTTP handler or the outgoing request dispatcher.
* @returns The forwarded request.
*/
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request);
// this.loggingService.sendInfoLevelMessage(request);

View File

@@ -1,9 +1,16 @@
/** Account data interface */
interface AccountDetails {
date_registered: number;
gender: string;
/** Age of user */
age?: string;
type?: string;
/** Token balance on account */
balance?: number;
/** Business category of user. */
category?: string;
/** Account registration day */
date_registered: number;
/** User's gender */
gender: string;
/** Account identifiers */
identities: {
evm: {
'bloxberg:8996': string[];
@@ -12,13 +19,17 @@ interface AccountDetails {
latitude: number;
longitude: number;
};
/** User's location */
location: {
area?: string;
area_name: string;
area_type?: string;
};
/** Products or services provided by user. */
products: string[];
category?: string;
/** Type of account */
type?: string;
/** Personal identifying information of user */
vcard: {
email: [
{
@@ -51,24 +62,37 @@ interface AccountDetails {
};
}
/** Meta signature interface */
interface Signature {
/** Algorithm used */
algo: string;
/** Data that was signed. */
data: string;
/** Message digest */
digest: string;
/** Encryption engine used. */
engine: string;
}
/** Meta object interface */
interface Meta {
/** Account details */
data: AccountDetails;
/** Meta store id */
id: string;
/** Signature used during write. */
signature: Signature;
}
/** Meta response interface */
interface MetaResponse {
/** Meta store id */
id: string;
/** Meta object */
m: Meta;
}
/** Default account data object */
const defaultAccount: AccountDetails = {
date_registered: Date.now(),
gender: 'other',
@@ -116,4 +140,5 @@ const defaultAccount: AccountDetails = {
},
};
export { AccountDetails, Signature, Meta, MetaResponse, defaultAccount };
/** @exports */
export { AccountDetails, Meta, MetaResponse, Signature, defaultAccount };

View File

@@ -1,6 +1,6 @@
export * from '@app/_models/transaction';
export * from '@app/_models/settings';
export * from '@app/_models/account';
export * from '@app/_models/mappings';
export * from '@app/_models/settings';
export * from '@app/_models/staff';
export * from '@app/_models/token';
export * from '@app/_models/mappings';
export * from '@app/_models/transaction';

View File

@@ -1,24 +1,40 @@
/** Action object interface */
interface Action {
id: number;
user: string;
role: string;
/** Action performed */
action: string;
/** Action approval status. */
approval: boolean;
/** Action ID */
id: number;
/** Admin's role in the system */
role: string;
/** Admin who initialized the action. */
user: string;
}
interface Category {
/** Area name object interface */
interface AreaName {
/** Locations that map to that area name. */
locations: Array<string>;
/** Name of area */
name: string;
}
/** Area type object interface */
interface AreaType {
/** Areas that map to that area type. */
area: Array<string>;
/** Type of area */
name: string;
}
/** Category object interface */
interface Category {
/** Business category */
name: string;
/** Products that map to that category. */
products: Array<string>;
}
interface AreaName {
name: string;
locations: Array<string>;
}
interface AreaType {
name: string;
area: Array<string>;
}
export { Action, Category, AreaName, AreaType };
/** @exports */
export { Action, AreaName, AreaType, Category };

View File

@@ -1,20 +1,34 @@
/** Settings class */
class Settings {
/** CIC Registry instance */
registry: any;
/** A resource for searching through blocks on the blockchain network. */
scanFilter: any;
/** Transaction Helper instance */
txHelper: any;
/** Web3 Object */
w3: W3 = {
engine: undefined,
provider: undefined,
};
scanFilter: any;
registry: any;
txHelper: any;
/**
* Initialize the settings.
*
* @param scanFilter - A resource for searching through blocks on the blockchain network.
*/
constructor(scanFilter: any) {
this.scanFilter = scanFilter;
}
}
class W3 {
/** Web3 object interface */
interface W3 {
/** An active web3 instance connected to the blockchain network. */
engine: any;
/** The connection socket to the blockchain network. */
provider: any;
}
/** @exports */
export { Settings, W3 };

View File

@@ -1,9 +1,16 @@
/** Staff object interface */
interface Staff {
/** Comment made on the public key. */
comment: string;
/** Email used to create the public key. */
email: string;
/** Name of the owner of the public key */
name: string;
/** Tags added to the public key. */
tag: number;
/** User ID of the owner of the public key. */
userid: string;
}
/** @exports */
export { Staff };

View File

@@ -1,17 +1,27 @@
/** Token object interface */
interface Token {
name: string;
symbol: string;
/** Address of the deployed token contract. */
address: string;
supply: string;
/** Number of decimals to convert to smallest denomination of the token. */
decimals: string;
/** Name of the token. */
name: string;
/** Address of account that deployed token. */
owner?: string;
/** Token reserve to token minting ratio. */
reserveRatio?: string;
/** Token reserve information */
reserves?: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: {
weight: string;
balance: string;
};
};
reserveRatio?: string;
owner?: string;
/** Total token supply. */
supply: string;
/** The unique token symbol. */
symbol: string;
}
/** @exports */
export { Token };

View File

@@ -1,46 +1,67 @@
// Application imports
import { AccountDetails } from '@app/_models/account';
class BlocksBloom {
low: number;
blockFilter: string;
blocktxFilter: string;
alg: string;
filterRounds: number;
/** Conversion object interface */
interface Conversion {
/** Final transaction token information. */
destinationToken: TxToken;
/** Initial transaction token amount. */
fromValue: number;
/** Initial transaction token information. */
sourceToken: TxToken;
/** Final transaction token amount. */
toValue: number;
/** Address of the initiator of the conversion. */
trader: string;
/** Conversion mining information. */
tx: Tx;
/** Account information of the initiator of the conversion. */
user: AccountDetails;
}
class TxToken {
address: string;
name: string;
symbol: string;
/** Transaction object interface */
interface Transaction {
/** Address of the transaction sender. */
from: string;
/** Account information of the transaction recipient. */
recipient: AccountDetails;
/** Account information of the transaction sender. */
sender: AccountDetails;
/** Address of the transaction recipient. */
to: string;
/** Transaction token information. */
token: TxToken;
/** Transaction mining information. */
tx: Tx;
/** Type of transaction. */
type?: string;
/** Amount of tokens transacted. */
value: number;
}
class Tx {
/** Transaction data interface */
interface Tx {
/** Transaction block number. */
block: number;
/** Transaction mining status. */
success: boolean;
/** Time transaction was mined. */
timestamp: number;
/** Hash generated by transaction. */
txHash: string;
/** Index of transaction in block. */
txIndex: number;
}
class Transaction {
from: string;
sender: AccountDetails;
to: string;
recipient: AccountDetails;
token: TxToken;
tx: Tx;
value: number;
type?: string;
/** Transaction token object interface */
interface TxToken {
/** Address of the deployed token contract. */
address: string;
/** Name of the token. */
name: string;
/** The unique token symbol. */
symbol: string;
}
class Conversion {
destinationToken: TxToken;
fromValue: number;
sourceToken: TxToken;
toValue: number;
trader: string;
user: AccountDetails;
tx: Tx;
}
export { BlocksBloom, TxToken, Tx, Transaction, Conversion };
/** @exports */
export { Conversion, Transaction, Tx, TxToken };

View File

@@ -1,3 +1,4 @@
// Application imports
import { MutablePgpKeyStore } from '@app/_pgp/pgp-key-store';
describe('PgpKeyStore', () => {

View File

@@ -1,95 +1,188 @@
// Third party imports
import { KeyStore } from 'cic-client-meta';
// TODO should we put this on the mutable key store object
import * as openpgp from 'openpgp';
/** An openpgp Keyring instance. */
const keyring = new openpgp.Keyring();
/**
* Mutable Key store interface.
*
* @extends KeyStore
*/
interface MutableKeyStore extends KeyStore {
loadKeyring(): void;
importKeyPair(publicKey: any, privateKey: any): Promise<void>;
importPublicKey(publicKey: any): Promise<void>;
importPrivateKey(privateKey: any): Promise<void>;
getPublicKeys(): Array<any>;
getTrustedKeys(): Array<any>;
getTrustedActiveKeys(): Array<any>;
getEncryptKeys(): Array<any>;
getPrivateKeys(): Array<any>;
getPrivateKey(): any;
isValidKey(key: any): Promise<boolean>;
isEncryptedPrivateKey(privateKey: any): Promise<boolean>;
getFingerprint(): string;
getKeyId(key: any): string;
getPrivateKeyId(): string;
getKeysForId(keyId: string): Array<any>;
getPublicKeyForId(keyId: string): any;
getPrivateKeyForId(keyId: string): any;
getPublicKeyForSubkeyId(subkeyId: string): any;
getPublicKeysForAddress(address: string): Array<any>;
removeKeysForId(keyId: string): Array<any>;
removePublicKeyForId(keyId: string): any;
removePublicKey(publicKey: any): any;
/** Remove all keys from the keyring. */
clearKeysInKeyring(): void;
/**
* Get all the encryption keys.
* @returns An array of encryption keys.
* @remarks
* Current implementation doesn't include encryption keys.
* This is included to appease the implemented Keystore interface.
*/
getEncryptKeys(): Array<any>;
/**
* Get the first private key's fingerprint.
* @returns The first private key's fingerprint.
*/
getFingerprint(): string;
/**
* Get a key's keyId.
* @param key - The key to fetch the keyId from.
* @returns The key's keyId.
*/
getKeyId(key: any): string;
/**
* Get keys from the keyring using their keyId.
* @param keyId - The keyId of the keys to be fetched from the keyring.
* @returns An array of the keys with that keyId.
*/
getKeysForId(keyId: string): Array<any>;
/**
* Get the first private key.
* @returns The first private key.
*/
getPrivateKey(): any;
/**
* Get a private key from the keyring using it's keyId.
* @param keyId - The keyId of the private key to be fetched from the keyring.
* @returns The private key with that keyId.
*/
getPrivateKeyForId(keyId: string): any;
/**
* Get the first private key's keyID.
* @returns The first private key's keyId.
*/
getPrivateKeyId(): string;
/**
* Get all private keys.
* @returns An array of all private keys.
*/
getPrivateKeys(): Array<any>;
/**
* Get a public key from the keyring using it's keyId.
* @param keyId - The keyId of the public key to be fetched from the keyring.
* @returns The public key with that keyId.
*/
getPublicKeyForId(keyId: string): any;
/**
* Get a public key from the keyring using it's subkeyId.
* @param subkeyId - The subkeyId of the public key to be fetched from the keyring.
* @returns The public key with that subkeyId.
*/
getPublicKeyForSubkeyId(subkeyId: string): any;
/**
* Get all the public keys.
* @returns An array of public keys.
*/
getPublicKeys(): Array<any>;
/**
* Get public keys from the keyring using their address.
* @param address - The address of the public keys to be fetched from the keyring.
* @returns An array of the public keys with that address.
*/
getPublicKeysForAddress(address: string): Array<any>;
/**
* Get all the trusted active keys.
* @returns An array of trusted active keys.
*/
getTrustedActiveKeys(): Array<any>;
/**
* Get all the trusted keys.
* @returns An array of trusted keys.
*/
getTrustedKeys(): Array<any>;
/**
* Add a key pair to keyring.
* @async
* @param publicKey - The public key to be added to the keyring.
* @param privateKey - The private key to be added to the keyring.
* @throws Error
*/
importKeyPair(publicKey: any, privateKey: any): Promise<void>;
/**
* Add private key to keyring.
* @async
* @param privateKey - The private key to be added to the keyring.
* @throws Error
*/
importPrivateKey(privateKey: any): Promise<void>;
/**
* Add public key to keyring.
* @async
* @param publicKey - The public key to be added to the keyring.
* @throws Error
*/
importPublicKey(publicKey: any): Promise<void>;
/**
* Verify that a private key is encrypted.
* @async
* @param privateKey - The private key to verify.
* @returns true - If private key is encrypted.
*/
isEncryptedPrivateKey(privateKey: any): Promise<boolean>;
/**
* Test if the input is a valid key.
* @async
* @param key - The input to be validated.
* @returns true - If the input is a valid key.
*/
isValidKey(key: any): Promise<boolean>;
/**
* Instantiate the keyring in the keystore.
* @async
*/
loadKeyring(): void;
/**
* Remove a public key from the keyring using it's keyId.
* @param keyId - The keyId of the keys to be removed from the keyring.
* @returns An array of the removed keys.
*/
removeKeysForId(keyId: string): Array<any>;
/**
* Remove a public key from the keyring.
* @param publicKey - The public key to be removed from the keyring.
* @returns The removed public key.
*/
removePublicKey(publicKey: any): any;
/**
* Remove a public key from the keyring using it's keyId.
* @param keyId - The keyId of the public key to be removed from the keyring.
* @returns The removed public key.
*/
removePublicKeyForId(keyId: string): any;
/**
* Sign message using private key.
* @async
* @param plainText - The message to be signed.
* @returns The generated signature.
*/
sign(plainText: string): Promise<any>;
}
/** Provides a keyring for pgp keys. */
class MutablePgpKeyStore implements MutableKeyStore {
async loadKeyring(): Promise<void> {
await keyring.load();
await keyring.store();
}
async importKeyPair(publicKey: any, privateKey: any): Promise<void> {
await keyring.publicKeys.importKey(publicKey);
await keyring.privateKeys.importKey(privateKey);
}
async importPublicKey(publicKey: any): Promise<void> {
await keyring.publicKeys.importKey(publicKey);
}
async importPrivateKey(privateKey: any): Promise<void> {
await keyring.privateKeys.importKey(privateKey);
}
getPublicKeys(): Array<any> {
return keyring.publicKeys.keys;
}
getTrustedKeys(): Array<any> {
return keyring.publicKeys.keys;
}
getTrustedActiveKeys(): Array<any> {
return keyring.publicKeys.keys;
/** Remove all keys from the keyring. */
clearKeysInKeyring(): void {
keyring.clear();
}
/**
* Get all the encryption keys.
* @returns An array of encryption keys.
* @remarks
* Current implementation doesn't include encryption keys.
* This is included to appease the implemented Keystore interface.
*/
getEncryptKeys(): Array<any> {
return [];
}
getPrivateKeys(): Array<any> {
return keyring.privateKeys.keys;
}
getPrivateKey(): any {
return keyring.privateKeys && keyring.privateKeys.keys[0];
}
async isValidKey(key): Promise<boolean> {
// There is supposed to be an openpgp.readKey() method but I can't find it?
const testKey = await openpgp.key.readArmored(key);
return !testKey.err;
}
async isEncryptedPrivateKey(privateKey: any): Promise<boolean> {
const imported = await openpgp.key.readArmored(privateKey);
for (const key of imported.keys) {
if (key.isDecrypted()) {
return false;
}
}
return true;
}
/**
* Get the first private key's fingerprint.
* @returns The first private key's fingerprint.
*/
getFingerprint(): string {
// TODO Handle multiple keys
return (
@@ -100,10 +193,45 @@ class MutablePgpKeyStore implements MutableKeyStore {
);
}
/**
* Get a key's keyId.
* @param key - The key to fetch the keyId from.
* @returns The key's keyId.
*/
getKeyId(key: any): string {
return key.getKeyId().toHex();
}
/**
* Get keys from the keyring using their keyId.
* @param keyId - The keyId of the keys to be fetched from the keyring.
* @returns An array of the keys with that keyId.
*/
getKeysForId(keyId: string): Array<any> {
return keyring.getKeysForId(keyId);
}
/**
* Get the first private key.
* @returns The first private key.
*/
getPrivateKey(): any {
return keyring.privateKeys && keyring.privateKeys.keys[0];
}
/**
* Get a private key from the keyring using it's keyId.
* @param keyId - The keyId of the private key to be fetched from the keyring.
* @returns The private key with that keyId.
*/
getPrivateKeyForId(keyId): any {
return keyring.privateKeys && keyring.privateKeys.getForId(keyId);
}
/**
* Get the first private key's keyID.
* @returns The first private key's keyId.
*/
getPrivateKeyId(): string {
// TODO is there a library that comes with angular for doing this?
return (
@@ -113,43 +241,180 @@ class MutablePgpKeyStore implements MutableKeyStore {
);
}
getKeysForId(keyId: string): Array<any> {
return keyring.getKeysForId(keyId);
/**
* Get all private keys.
* @returns An array of all private keys.
*/
getPrivateKeys(): Array<any> {
return keyring.privateKeys && keyring.privateKeys.keys;
}
/**
* Get a public key from the keyring using it's keyId.
* @param keyId - The keyId of the public key to be fetched from the keyring.
* @returns The public key with that keyId.
*/
getPublicKeyForId(keyId): any {
return keyring.publicKeys.getForId(keyId);
}
getPrivateKeyForId(keyId): any {
return keyring.privateKeys.getForId(keyId);
return keyring.publicKeys && keyring.publicKeys.getForId(keyId);
}
/**
* Get a public key from the keyring using it's subkeyId.
* @param subkeyId - The subkeyId of the public key to be fetched from the keyring.
* @returns The public key with that subkeyId.
*/
getPublicKeyForSubkeyId(subkeyId): any {
return keyring.publicKeys.getForId(subkeyId, true);
return keyring.publicKeys && keyring.publicKeys.getForId(subkeyId, true);
}
/**
* Get all the public keys.
* @returns An array of public keys.
*/
getPublicKeys(): Array<any> {
return keyring.publicKeys && keyring.publicKeys.keys;
}
/**
* Get public keys from the keyring using their address.
* @param address - The address of the public keys to be fetched from the keyring.
* @returns An array of the public keys with that address.
*/
getPublicKeysForAddress(address): Array<any> {
return keyring.publicKeys.getForAddress(address);
return keyring.publicKeys && keyring.publicKeys.getForAddress(address);
}
/**
* Get all the trusted active keys.
* @returns An array of trusted active keys.
*/
getTrustedActiveKeys(): Array<any> {
return keyring.publicKeys && keyring.publicKeys.keys;
}
/**
* Get all the trusted keys.
* @returns An array of trusted keys.
*/
getTrustedKeys(): Array<any> {
return keyring.publicKeys && keyring.publicKeys.keys;
}
/**
* Add a key pair to keyring.
* @async
* @param publicKey - The public key to be added to the keyring.
* @param privateKey - The private key to be added to the keyring.
* @throws Error
*/
async importKeyPair(publicKey: any, privateKey: any): Promise<void> {
try {
await keyring.publicKeys.importKey(publicKey);
await keyring.privateKeys.importKey(privateKey);
} catch (error) {
throw error;
}
}
/**
* Add private key to keyring.
* @async
* @param privateKey - The private key to be added to the keyring.
* @throws Error
*/
async importPrivateKey(privateKey: any): Promise<void> {
try {
await keyring.privateKeys.importKey(privateKey);
} catch (error) {
throw error;
}
}
/**
* Add public key to keyring.
* @async
* @param publicKey - The public key to be added to the keyring.
* @throws Error
*/
async importPublicKey(publicKey: any): Promise<void> {
try {
await keyring.publicKeys.importKey(publicKey);
} catch (error) {
throw error;
}
}
/**
* Verify that a private key is encrypted.
* @async
* @param privateKey - The private key to verify.
* @returns true - If private key is encrypted.
*/
async isEncryptedPrivateKey(privateKey: any): Promise<boolean> {
const imported = await openpgp.key.readArmored(privateKey);
for (const key of imported.keys) {
if (key.isDecrypted()) {
return false;
}
}
return true;
}
/**
* Test if the input is a valid key.
* @async
* @param key - The input to be validated.
* @returns true - If the input is a valid key.
*/
async isValidKey(key): Promise<boolean> {
// There is supposed to be an openpgp.readKey() method but I can't find it?
const testKey = await openpgp.key.readArmored(key);
return !testKey.err;
}
/**
* Instantiate the keyring in the keystore.
* @async
*/
async loadKeyring(): Promise<void> {
await keyring.load();
await keyring.store();
}
/**
* Remove a public key from the keyring using it's keyId.
* @param keyId - The keyId of the keys to be removed from the keyring.
* @returns An array of the removed keys.
*/
removeKeysForId(keyId): Array<any> {
return keyring.removeKeysForId(keyId);
}
removePublicKeyForId(keyId): any {
return keyring.publicKeys.removeForId(keyId);
}
/**
* Remove a public key from the keyring.
* @param publicKey - The public key to be removed from the keyring.
* @returns The removed public key.
*/
removePublicKey(publicKey: any): any {
const keyId = publicKey.getKeyId().toHex();
return keyring.publicKeys.removeForId(keyId);
return keyring.publicKeys && keyring.publicKeys.removeForId(keyId);
}
clearKeysInKeyring(): void {
keyring.clear();
/**
* Remove a public key from the keyring using it's keyId.
* @param keyId - The keyId of the public key to be removed from the keyring.
* @returns The removed public key.
*/
removePublicKeyForId(keyId): any {
return keyring.publicKeys && keyring.publicKeys.removeForId(keyId);
}
/**
* Sign message using private key.
* @async
* @param plainText - The message to be signed.
* @returns The generated signature.
*/
async sign(plainText): Promise<any> {
const privateKey = this.getPrivateKey();
if (!privateKey.isDecrypted()) {
@@ -166,4 +431,5 @@ class MutablePgpKeyStore implements MutableKeyStore {
}
}
export { MutablePgpKeyStore, MutableKeyStore };
/** @exports */
export { MutableKeyStore, MutablePgpKeyStore };

View File

@@ -1,5 +1,7 @@
import { PGPSigner } from '@app/_pgp/pgp-signer';
// Application imports
import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp/pgp-key-store';
import { PGPSigner } from '@app/_pgp/pgp-signer';
const keystore: MutableKeyStore = new MutablePgpKeyStore();
describe('PgpSigner', () => {

View File

@@ -1,53 +1,146 @@
// Third party imports
import * as openpgp from 'openpgp';
// Application imports
import { MutableKeyStore } from '@app/_pgp/pgp-key-store';
import { LoggingService } from '@app/_services/logging.service';
const openpgp = require('openpgp');
/** Signable object interface */
interface Signable {
/** The message to be signed. */
digest(): string;
}
/** Signature object interface */
interface Signature {
engine: string;
/** Encryption algorithm used */
algo: string;
/** Data to be signed. */
data: string;
/** Message digest */
digest: string;
/** Encryption engine used. */
engine: string;
}
/** Signer interface */
interface Signer {
onsign(signature: Signature): void;
onverify(flag: boolean): void;
/**
* Get the private key fingerprint.
* @returns A private key fingerprint.
*/
fingerprint(): string;
/** Event triggered on successful signing of message. */
onsign(signature: Signature): void;
/** Event triggered on successful verification of a signature. */
onverify(flag: boolean): void;
/**
* Load the message digest.
* @param material - A signable object.
* @returns true - If digest has been loaded successfully.
*/
prepare(material: Signable): boolean;
verify(digest: string, signature: Signature): void;
/**
* Signs a message using a private key.
* @async
* @param digest - The message to be signed.
*/
sign(digest: string): Promise<void>;
/**
* Verify that signature is valid.
* @param digest - The message that was signed.
* @param signature - The generated signature.
*/
verify(digest: string, signature: Signature): void;
}
/** Provides functionality for signing and verifying signed messages. */
class PGPSigner implements Signer {
engine = 'pgp';
/** Encryption algorithm used */
algo = 'sha256';
/** Message digest */
dgst: string;
signature: Signature;
/** Encryption engine used. */
engine = 'pgp';
/** A keystore holding pgp keys. */
keyStore: MutableKeyStore;
onsign: (signature: Signature) => void;
onverify: (flag: boolean) => void;
/** A service that provides logging capabilities. */
loggingService: LoggingService;
/** Event triggered on successful signing of message. */
onsign: (signature: Signature) => void;
/** Event triggered on successful verification of a signature. */
onverify: (flag: boolean) => void;
/** Generated signature */
signature: Signature;
/**
* Initializing the Signer.
* @param keyStore - A keystore holding pgp keys.
*/
constructor(keyStore: MutableKeyStore) {
this.keyStore = keyStore;
this.onsign = (signature: Signature) => {};
this.onverify = (flag: boolean) => {};
}
/**
* Get the private key fingerprint.
* @returns A private key fingerprint.
*/
public fingerprint(): string {
return this.keyStore.getFingerprint();
}
/**
* Load the message digest.
* @param material - A signable object.
* @returns true - If digest has been loaded successfully.
*/
public prepare(material: Signable): boolean {
this.dgst = material.digest();
return true;
}
/**
* Signs a message using a private key.
* @async
* @param digest - The message to be signed.
*/
public async sign(digest: string): Promise<void> {
const m = openpgp.cleartext.fromText(digest);
const pk = this.keyStore.getPrivateKey();
if (!pk.isDecrypted()) {
const password = window.prompt('password');
await pk.decrypt(password);
}
const opts = {
message: m,
privateKeys: [pk],
detached: true,
};
openpgp
.sign(opts)
.then((s) => {
this.signature = {
engine: this.engine,
algo: this.algo,
data: s.signature,
// TODO: fix for browser later
digest,
};
this.onsign(this.signature);
})
.catch((e) => {
this.loggingService.sendErrorLevelMessage(e.message, this, { error: e });
this.onsign(undefined);
});
}
/**
* Verify that signature is valid.
* @param digest - The message that was signed.
* @param signature - The generated signature.
*/
public verify(digest: string, signature: Signature): void {
openpgp.signature
.readArmored(signature.data)
@@ -79,36 +172,7 @@ class PGPSigner implements Signer {
this.onverify(false);
});
}
public async sign(digest: string): Promise<void> {
const m = openpgp.cleartext.fromText(digest);
const pk = this.keyStore.getPrivateKey();
if (!pk.isDecrypted()) {
const password = window.prompt('password');
await pk.decrypt(password);
}
const opts = {
message: m,
privateKeys: [pk],
detached: true,
};
openpgp
.sign(opts)
.then((s) => {
this.signature = {
engine: this.engine,
algo: this.algo,
data: s.signature,
// TODO: fix for browser later
digest,
};
this.onsign(this.signature);
})
.catch((e) => {
this.loggingService.sendErrorLevelMessage(e.message, this, { error: e });
this.onsign(undefined);
});
}
}
export { Signable, Signature, Signer, PGPSigner };
/** @exports */
export { PGPSigner, Signable, Signature, Signer };

View File

@@ -131,6 +131,10 @@ export class AuthService {
this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
}
/**
* @throws
* @param privateKeyArmored - Private key.
*/
async setKey(privateKeyArmored): Promise<boolean> {
try {
const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored);

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Settings } from '@app/_models';
import { TransactionHelper } from 'cic-client';
import { TransactionHelper } from '@cicnet/cic-client';
import { first } from 'rxjs/operators';
import { TransactionService } from '@app/_services/transaction.service';
import { environment } from '@src/environments/environment';
@@ -39,10 +39,6 @@ export class BlockSyncService {
settings.txHelper.onconversion = async (transaction: any): Promise<any> => {
window.dispatchEvent(this.newEvent(transaction, 'cic_convert'));
};
// settings.registry.onload = (addressReturned: string): void => {
// this.loggingService.sendInfoLevelMessage(`Loaded network contracts ${addressReturned}`);
// this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit);
// };
this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit);
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { environment } from '@src/environments/environment';
import { CICRegistry, FileGetter } from 'cic-client';
import { CICRegistry, FileGetter } from '@cicnet/cic-client';
import { HttpGetter } from '@app/_helpers';
import { Web3Service } from '@app/_services/web3.service';

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { CICRegistry } from 'cic-client';
import { CICRegistry } from '@cicnet/cic-client';
import { TokenRegistry } from '@app/_eth';
import { HttpClient } from '@angular/common/http';
import { RegistryService } from '@app/_services/registry.service';
@@ -12,23 +12,21 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
export class TokenService {
registry: CICRegistry;
tokenRegistry: TokenRegistry;
onload: (status: boolean) => void;
tokens: Array<Token> = [];
private tokensList: BehaviorSubject<Array<Token>> = new BehaviorSubject<Array<Token>>(
this.tokens
);
tokensSubject: Observable<Array<Token>> = this.tokensList.asObservable();
load: BehaviorSubject<any> = new BehaviorSubject<any>(false);
constructor(private httpClient: HttpClient) {}
async init(): Promise<void> {
this.registry = await RegistryService.getRegistry();
this.registry.onload = async (address: string): Promise<void> => {
this.tokenRegistry = new TokenRegistry(
await this.registry.getContractAddressByName('TokenRegistry')
);
this.onload(this.tokenRegistry !== undefined);
};
this.tokenRegistry = new TokenRegistry(
await this.registry.getContractAddressByName('TokenRegistry')
);
this.load.next(true);
}
addToken(token: Token): void {
@@ -73,7 +71,17 @@ export class TokenService {
}
async getTokenBalance(address: string): Promise<(address: string) => Promise<number>> {
const sarafuToken = await this.registry.addToken(await this.tokenRegistry.entry(0));
return await sarafuToken.methods.balanceOf(address).call();
const token = await this.registry.addToken(await this.tokenRegistry.entry(0));
return await token.methods.balanceOf(address).call();
}
async getTokenName(): Promise<string> {
const token = await this.registry.addToken(await this.tokenRegistry.entry(0));
return await token.methods.name().call();
}
async getTokenSymbol(): Promise<string> {
const token = await this.registry.addToken(await this.tokenRegistry.entry(0));
return await token.methods.symbol().call();
}
}

View File

@@ -14,7 +14,7 @@ import { AuthService } from '@app/_services/auth.service';
import { defaultAccount } from '@app/_models';
import { LoggingService } from '@app/_services/logging.service';
import { HttpClient } from '@angular/common/http';
import { CICRegistry } from 'cic-client';
import { CICRegistry } from '@cicnet/cic-client';
import { RegistryService } from '@app/_services/registry.service';
import Web3 from 'web3';
import { Web3Service } from '@app/_services/web3.service';
@@ -149,44 +149,42 @@ export class TransactionService {
recipientAddress: string,
value: number
): Promise<any> {
this.registry.onload = async (addressReturned: string): Promise<void> => {
const transferAuthAddress = await this.registry.getContractAddressByName(
'TransferAuthorization'
);
const hashFunction = new Keccak(256);
hashFunction.update('createRequest(address,address,address,uint256)');
const hash = hashFunction.digest();
const methodSignature = hash.toString('hex').substring(0, 8);
const abiCoder = new utils.AbiCoder();
const abi = await abiCoder.encode(
['address', 'address', 'address', 'uint256'],
[senderAddress, recipientAddress, tokenAddress, value]
);
const data = fromHex(methodSignature + strip0x(abi));
const tx = new Tx(environment.bloxbergChainId);
tx.nonce = await this.web3.eth.getTransactionCount(senderAddress);
tx.gasPrice = Number(await this.web3.eth.getGasPrice());
tx.gasLimit = 8000000;
tx.to = fromHex(strip0x(transferAuthAddress));
tx.value = toValue(value);
tx.data = data;
const txMsg = tx.message();
const keystore = await KeystoreService.getKeystore();
const privateKey = keystore.getPrivateKey();
if (!privateKey.isDecrypted()) {
const password = window.prompt('password');
await privateKey.decrypt(password);
}
const signatureObject = secp256k1.ecdsaSign(txMsg, privateKey.keyPacket.privateParams.d);
const r = signatureObject.signature.slice(0, 32);
const s = signatureObject.signature.slice(32);
const v = signatureObject.recid;
tx.setSignature(r, s, v);
const txWire = add0x(toHex(tx.serializeRLP()));
const result = await this.web3.eth.sendSignedTransaction(txWire);
this.loggingService.sendInfoLevelMessage(`Result: ${result}`);
const transaction = await this.web3.eth.getTransaction(result.transactionHash);
this.loggingService.sendInfoLevelMessage(`Transaction: ${transaction}`);
};
const transferAuthAddress = await this.registry.getContractAddressByName(
'TransferAuthorization'
);
const hashFunction = new Keccak(256);
hashFunction.update('createRequest(address,address,address,uint256)');
const hash = hashFunction.digest();
const methodSignature = hash.toString('hex').substring(0, 8);
const abiCoder = new utils.AbiCoder();
const abi = await abiCoder.encode(
['address', 'address', 'address', 'uint256'],
[senderAddress, recipientAddress, tokenAddress, value]
);
const data = fromHex(methodSignature + strip0x(abi));
const tx = new Tx(environment.bloxbergChainId);
tx.nonce = await this.web3.eth.getTransactionCount(senderAddress);
tx.gasPrice = Number(await this.web3.eth.getGasPrice());
tx.gasLimit = 8000000;
tx.to = fromHex(strip0x(transferAuthAddress));
tx.value = toValue(value);
tx.data = data;
const txMsg = tx.message();
const keystore = await KeystoreService.getKeystore();
const privateKey = keystore.getPrivateKey();
if (!privateKey.isDecrypted()) {
const password = window.prompt('password');
await privateKey.decrypt(password);
}
const signatureObject = secp256k1.ecdsaSign(txMsg, privateKey.keyPacket.privateParams.d);
const r = signatureObject.signature.slice(0, 32);
const s = signatureObject.signature.slice(32);
const v = signatureObject.recid;
tx.setSignature(r, s, v);
const txWire = add0x(toHex(tx.serializeRLP()));
const result = await this.web3.eth.sendSignedTransaction(txWire);
this.loggingService.sendInfoLevelMessage(`Result: ${result}`);
const transaction = await this.web3.eth.getTransaction(result.transactionHash);
this.loggingService.sendInfoLevelMessage(`Transaction: ${transaction}`);
}
}

View File

@@ -22,26 +22,6 @@ describe('UserService', () => {
expect(service).toBeTruthy();
});
it('should return user for available id', () => {
expect(service.getAccountById(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.getAccountById(9999999999)).toBeUndefined();
});
it('should return action for available id', () => {
expect(service.getActionById('1')).toEqual({
id: 1,

View File

@@ -10,7 +10,7 @@ import { TokenService } from '@app/_services/token.service';
import { AccountIndex } from '@app/_eth';
import { MutableKeyStore, PGPSigner, Signer } from '@app/_pgp';
import { RegistryService } from '@app/_services/registry.service';
import { CICRegistry } from 'cic-client';
import { CICRegistry } from '@cicnet/cic-client';
import { AuthService } from '@app/_services/auth.service';
import { personValidation, vcardValidation } from '@app/_helpers';
import { add0x } from '@src/assets/js/ethtx/dist/hex';
@@ -202,11 +202,13 @@ export class UserService {
const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
const accountInfo = account.m.data;
await personValidation(accountInfo);
this.tokenService.onload = async (status: boolean): Promise<void> => {
accountInfo.balance = await this.tokenService.getTokenBalance(
accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]
);
};
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);

View File

@@ -1,6 +1,9 @@
import { PasswordToggleDirective } from '@app/auth/_directives/password-toggle.directive';
// Core imports
import { ElementRef, Renderer2 } from '@angular/core';
// Application imports
import { PasswordToggleDirective } from '@app/auth/_directives/password-toggle.directive';
// tslint:disable-next-line:prefer-const
let elementRef: ElementRef;
// tslint:disable-next-line:prefer-const

View File

@@ -1,21 +1,32 @@
// Core imports
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
/** Toggle password form field input visibility */
@Directive({
selector: '[appPasswordToggle]',
})
export class PasswordToggleDirective {
/** The password form field id */
@Input()
id: string;
/** The password form field icon id */
@Input()
iconId: string;
/**
* Handle click events on the html element.
*
* @param elementRef - A wrapper around a native element inside of a View.
* @param renderer - Extend this base class to implement custom rendering.
*/
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
this.renderer.listen(this.elementRef.nativeElement, 'click', () => {
this.togglePasswordVisibility();
});
}
/** Toggle the visibility of the password input field value and accompanying icon. */
togglePasswordVisibility(): void {
const password: HTMLElement = document.getElementById(this.id);
const icon: HTMLElement = document.getElementById(this.iconId);

View File

@@ -29,6 +29,9 @@ export class AuthComponent implements OnInit {
this.keyForm = this.formBuilder.group({
key: ['', Validators.required],
});
if (this.authService.getPrivateKey()) {
this.authService.loginView();
}
}
get keyFormStub(): any {

View File

@@ -33,7 +33,7 @@
<h3>
<strong> {{account?.vcard?.fn[0].value}} </strong>
</h3>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
<span class="ml-2"><strong>Created:</strong> {{account?.date_registered | unixDate}}</span>
<span class="ml-2"><strong>Address:</strong>
<a href="{{bloxbergLink}}" target="_blank"> {{accountAddress}} </a>
@@ -218,7 +218,7 @@
<tbody>
<tr>
<td>{{account?.vcard?.fn[0].value}}</td>
<td>{{account?.balance | tokenRatio}}</td>
<td>{{account?.balance | tokenRatio}} {{ tokenSymbol | uppercase }}</td>
<td>{{account?.date_registered | unixDate}}</td>
<td>
<span class="badge badge-success badge-pill">
@@ -274,8 +274,8 @@
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Value </th>
<td mat-cell *matCellDef="let transaction">
<span *ngIf="transaction.type == 'transaction'">{{transaction?.value | tokenRatio}}</span>
<span *ngIf="transaction.type == 'conversion'">{{transaction?.toValue | tokenRatio}}</span>
<span *ngIf="transaction.type == 'transaction'">{{transaction?.value | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
<span *ngIf="transaction.type == 'conversion'">{{transaction?.toValue | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
</td>
</ng-container>
@@ -348,7 +348,7 @@
<ng-container matColumnDef="balance">
<mat-header-cell *matHeaderCellDef mat-sort-header> BALANCE </mat-header-cell>
<mat-cell *matCellDef="let user"> {{user?.balance}} </mat-cell>
<mat-cell *matCellDef="let user"> {{user?.balance | tokenRatio}} {{tokenSymbol | uppercase}} </mat-cell>
</ng-container>
<ng-container matColumnDef="location">

View File

@@ -64,6 +64,7 @@ export class AccountDetailsComponent implements OnInit {
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
submitted: boolean = false;
bloxbergLink: string;
tokenSymbol: string;
constructor(
private formBuilder: FormBuilder,
@@ -113,7 +114,7 @@ export class AccountDetailsComponent implements OnInit {
this.loggingService.sendInfoLevelMessage(this.account);
const fullName = this.account.vcard?.fn[0].value.split(' ');
this.accountInfoForm.patchValue({
firstName: fullName[0],
firstName: fullName[0].split(',')[0],
lastName: fullName.slice(1).join(' '),
phoneNumber: this.account.vcard?.tel[0].value,
age: this.account.age,
@@ -189,6 +190,11 @@ export class AccountDetailsComponent implements OnInit {
.getGenders()
.pipe(first())
.subscribe((res) => (this.genders = res));
this.tokenService.load.subscribe(async (status: boolean) => {
if (status) {
this.tokenSymbol = await this.tokenService.getTokenSymbol();
}
});
}
doTransactionFilter(value: string): void {
@@ -220,7 +226,7 @@ export class AccountDetailsComponent implements OnInit {
}
const accountKey = await this.userService.changeAccountInfo(
this.accountAddress,
this.accountInfoFormStub.firstName.value + ' ' + this.accountInfoFormStub.lastName.value,
this.accountInfoFormStub.firstName.value + ', ' + this.accountInfoFormStub.lastName.value,
this.accountInfoFormStub.phoneNumber.value,
this.accountInfoFormStub.age.value,
this.accountInfoFormStub.type.value,

View File

@@ -29,9 +29,11 @@ export class TokensComponent implements OnInit {
async ngOnInit(): Promise<void> {
await this.tokenService.init();
this.tokenService.onload = async (status: boolean): Promise<void> => {
await this.tokenService.getTokens();
};
this.tokenService.load.subscribe(async (status: boolean) => {
if (status) {
await this.tokenService.getTokens();
}
});
this.tokenService.tokensSubject.subscribe((tokens) => {
this.loggingService.sendInfoLevelMessage(tokens);
this.dataSource = new MatTableDataSource(tokens);

View File

@@ -30,7 +30,7 @@
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewRecipient()">View Recipient</button>
</li>
<li class="list-group-item">
<span>Amount: SRF {{transaction.value | tokenRatio}}</span>
<span>Amount: {{transaction.value | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
</li>
</ul>
<h4 class="mt-2">Token: </h4>
@@ -43,10 +43,10 @@
</span>
</li>
<li class="list-group-item">
<span>Name: Sarafu Token</span>
<span>Name: {{ tokenName }}</span>
</li>
<li class="list-group-item">
<span>Symbol: SRF</span>
<span>Symbol: {{ tokenSymbol | uppercase }}</span>
</li>
</ul>
</div>
@@ -109,10 +109,10 @@
<span>Name: {{transaction.sourceToken.name}}</span>
</li>
<li class="list-group-item">
<span>Symbol: {{transaction.sourceToken.symbol}}</span>
<span>Symbol: {{transaction.sourceToken.symbol | uppercase}}</span>
</li>
<li class="list-group-item">
<span>Amount: {{transaction.sourceToken.symbol + ' ' + transaction.fromValue}}</span>
<span>Amount: {{transaction.fromValue}} {{transaction.sourceToken.symbol | uppercase}}</span>
</li>
</ul>
</div>
@@ -130,10 +130,10 @@
<span>Name: {{transaction.destinationToken.name}}</span>
</li>
<li class="list-group-item">
<span>Symbol: {{transaction.destinationToken.symbol}}</span>
<span>Symbol: {{transaction.destinationToken.symbol | uppercase}}</span>
</li>
<li class="list-group-item">
<span>Amount: {{transaction.destinationToken.symbol + ' ' + transaction.toValue}}</span>
<span>Amount: {{transaction.toValue}} {{transaction.destinationToken.symbol | uppercase}}</span>
</li>
</ul>
</div>

View File

@@ -7,7 +7,7 @@ import {
Output,
} from '@angular/core';
import { Router } from '@angular/router';
import { TransactionService } from '@app/_services';
import { TokenService, TransactionService } from '@app/_services';
import { copyToClipboard } from '@app/_helpers';
import { MatSnackBar } from '@angular/material/snack-bar';
import { strip0x } from '@src/assets/js/ethtx/dist/hex';
@@ -26,15 +26,19 @@ export class TransactionDetailsComponent implements OnInit {
senderBloxbergLink: string;
recipientBloxbergLink: string;
traderBloxbergLink: string;
tokenName: string;
tokenSymbol: string;
constructor(
private router: Router,
private transactionService: TransactionService,
private snackBar: MatSnackBar
private snackBar: MatSnackBar,
private tokenService: TokenService
) {}
async ngOnInit(): Promise<void> {
await this.transactionService.init();
await this.tokenService.init();
if (this.transaction?.type === 'conversion') {
this.traderBloxbergLink =
'https://blockexplorer.bloxberg.org/address/' + this.transaction?.trader + '/transactions';
@@ -44,6 +48,12 @@ export class TransactionDetailsComponent implements OnInit {
this.recipientBloxbergLink =
'https://blockexplorer.bloxberg.org/address/' + this.transaction?.to + '/transactions';
}
this.tokenService.load.subscribe(async (status: boolean) => {
if (status) {
this.tokenSymbol = await this.tokenService.getTokenSymbol();
this.tokenName = await this.tokenService.getTokenName();
}
});
}
async viewSender(): Promise<void> {

View File

@@ -59,8 +59,8 @@
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Value </th>
<td mat-cell *matCellDef="let transaction">
<span *ngIf="transaction.type == 'transaction'">{{transaction?.value | tokenRatio}}</span>
<span *ngIf="transaction.type == 'conversion'">{{transaction?.toValue | tokenRatio}}</span>
<span *ngIf="transaction.type == 'transaction'">{{transaction?.value | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
<span *ngIf="transaction.type == 'conversion'">{{transaction?.toValue | tokenRatio}} {{ tokenSymbol | uppercase }}</span>
</td>
</ng-container>

View File

@@ -5,7 +5,7 @@ import {
OnInit,
ViewChild,
} from '@angular/core';
import { BlockSyncService, TransactionService, UserService } from '@app/_services';
import { BlockSyncService, TokenService, TransactionService, UserService } from '@app/_services';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
@@ -28,6 +28,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
transaction: Transaction;
transactionsType: string = 'all';
transactionsTypes: Array<string>;
tokenSymbol: string;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@@ -35,7 +36,8 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
constructor(
private blockSyncService: BlockSyncService,
private transactionService: TransactionService,
private userService: UserService
private userService: UserService,
private tokenService: TokenService
) {}
async ngOnInit(): Promise<void> {
@@ -46,6 +48,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
this.transactions = transactions;
});
await this.blockSyncService.init();
await this.tokenService.init();
await this.transactionService.init();
await this.userService.init();
await this.blockSyncService.blockSync();
@@ -53,6 +56,11 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
.getTransactionTypes()
.pipe(first())
.subscribe((res) => (this.transactionsTypes = res));
this.tokenService.load.subscribe(async (status: boolean) => {
if (status) {
this.tokenSymbol = await this.tokenService.getTokenSymbol();
}
});
}
viewTransaction(transaction): void {

View File

@@ -1,6 +1,9 @@
import { MenuSelectionDirective } from '@app/shared/_directives/menu-selection.directive';
// Core imports
import { ElementRef, Renderer2 } from '@angular/core';
// Application imports
import { MenuSelectionDirective } from '@app/shared/_directives/menu-selection.directive';
describe('MenuSelectionDirective', () => {
// tslint:disable-next-line:prefer-const
let elementRef: ElementRef;

View File

@@ -1,9 +1,17 @@
// Core imports
import { Directive, ElementRef, Renderer2 } from '@angular/core';
/** Toggle availability of sidebar on menu item selection. */
@Directive({
selector: '[appMenuSelection]',
})
export class MenuSelectionDirective {
/**
* Handle click events on the html element.
*
* @param elementRef - A wrapper around a native element inside of a View.
* @param renderer - Extend this base class to implement custom rendering.
*/
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
this.renderer.listen(this.elementRef.nativeElement, 'click', () => {
const mediaQuery = window.matchMedia('(max-width: 768px)');
@@ -13,6 +21,7 @@ export class MenuSelectionDirective {
});
}
/** Toggle the availability of the sidebar. */
onMenuSelect(): void {
const sidebar: HTMLElement = document.getElementById('sidebar');
if (!sidebar?.classList.contains('active')) {

View File

@@ -1,6 +1,9 @@
import { MenuToggleDirective } from '@app/shared/_directives/menu-toggle.directive';
// Core imports
import { ElementRef, Renderer2 } from '@angular/core';
// Application imports
import { MenuToggleDirective } from '@app/shared/_directives/menu-toggle.directive';
describe('MenuToggleDirective', () => {
// tslint:disable-next-line:prefer-const
let elementRef: ElementRef;

View File

@@ -1,16 +1,24 @@
// Core imports
import { Directive, ElementRef, Renderer2 } from '@angular/core';
/** Toggle availability of sidebar on menu toggle click. */
@Directive({
selector: '[appMenuToggle]',
})
export class MenuToggleDirective {
/**
* Handle click events on the html element.
*
* @param elementRef - A wrapper around a native element inside of a View.
* @param renderer - Extend this base class to implement custom rendering.
*/
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
this.renderer.listen(this.elementRef.nativeElement, 'click', () => {
this.onMenuToggle();
});
}
// Menu Trigger
/** Toggle the availability of the sidebar. */
onMenuToggle(): void {
const sidebar: HTMLElement = document.getElementById('sidebar');
sidebar?.classList.toggle('active');

View File

@@ -1,8 +1,12 @@
import { SafePipe } from './safe.pipe';
import { DomSanitizer } from '@angular/platform-browser';
// tslint:disable-next-line:prefer-const
let sanitizer: DomSanitizer;
describe('SafePipe', () => {
it('create an instance', () => {
const pipe = new SafePipe();
const pipe = new SafePipe(sanitizer);
expect(pipe).toBeTruthy();
});
});

View File

@@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'tokenRatio' })
export class TokenRatioPipe implements PipeTransform {
transform(value: any, ...args): any {
transform(value: any = 0, ...args): any {
return Number(value) / Math.pow(10, 6);
}
}

View File

@@ -50,6 +50,13 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "_holder", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",