40 Commits

Author SHA1 Message Date
Spencer Ofwiti
7e82930282 Merge branch 'master' into spencer/electron 2021-06-09 08:49:05 +03:00
Spencer Ofwiti
1dc751f15f Merge branch 'spencer/fix-account-index' into 'master'
Fix account index out of index error.

See merge request grassrootseconomics/cic-staff-client!23
2021-06-09 05:20:44 +00:00
Spencer Ofwiti
9363532284 Remove forceful parsing to JSON format. 2021-06-04 15:16:57 +03:00
Spencer Ofwiti
3831694d3e Refactor token list to use Observables for tracking changes. 2021-06-04 11:01:20 +03:00
Spencer Ofwiti
631c546c7b Bypass checking for registry load in blocksync service. 2021-06-04 09:35:34 +03:00
Spencer Ofwiti
d94f7e8e15 Add a dedicated get token by address method. 2021-06-04 09:23:05 +03:00
Spencer Ofwiti
701605be31 Refactor getTokenBySymbol method. 2021-06-02 13:09:06 +03:00
Spencer Ofwiti
06326aa0bf Refactor patching of location and category information to autofil from bio and user location. 2021-06-02 12:59:37 +03:00
Spencer Ofwiti
6904127876 Add firstname and lastname input fields to account details form. 2021-06-02 12:10:28 +03:00
Spencer Ofwiti
4fba6a8383 Refactor trusted users list. 2021-06-02 10:48:02 +03:00
Spencer Ofwiti
81620600e3 Refactor token details into a presentation component. 2021-06-02 10:04:42 +03:00
Spencer Ofwiti
8de8e6a30b Fix token list. 2021-06-01 20:34:41 +03:00
Spencer Ofwiti
384071d3db Add account status to account info. 2021-05-26 19:02:06 +03:00
Spencer Ofwiti
e5d555577e Add optional template filling for admin user info. 2021-05-26 18:34:03 +03:00
Spencer Ofwiti
cfe558cccf Move online status tracker to sidebar. 2021-05-26 13:06:18 +03:00
Spencer Ofwiti
228bca564e Refactor footer. 2021-05-26 12:51:54 +03:00
Spencer Ofwiti
9fed6fcaf3 Add close event to transaction details presentation component. 2021-05-26 12:21:25 +03:00
Spencer Ofwiti
48ef7cd58d Refactor USSD url. 2021-05-26 11:53:44 +03:00
Spencer Ofwiti
16af939af7 Refactor transactions table to show address before loading account meta. 2021-05-26 11:30:38 +03:00
Spencer Ofwiti
9782c919d5 Refactor calls to ussd service. 2021-05-26 10:26:11 +03:00
Spencer Ofwiti
cef7b8dc04 Refactor ussd service url. 2021-05-26 10:19:14 +03:00
Spencer Ofwiti
3a6d3bb8c2 Refactor file management functions. 2021-05-26 10:00:08 +03:00
Spencer Ofwiti
7ddca00871 Wrap web app into electron. 2021-05-26 09:54:55 +03:00
Spencer Ofwiti
c60d28a053 Add web3 singleton service. 2021-05-20 21:27:06 +03:00
Spencer Ofwiti
a4c0e26be9 Clean up constructors. 2021-05-19 19:57:10 +03:00
Spencer Ofwiti
5baffa5fef Fix timestam. 2021-05-19 15:35:28 +03:00
Spencer Ofwiti
4e06b42cc6 Fix user traded accounts list. 2021-05-19 10:56:39 +03:00
Spencer Ofwiti
8ad736de20 Fix async pipe. 2021-05-19 09:32:35 +03:00
Spencer Ofwiti
872bf65786 Refactor services to handle data from meta service. 2021-05-18 22:10:08 +03:00
Spencer Ofwiti
7a2321f444 Parse incoming data from meta into JSON format. 2021-05-18 19:06:57 +03:00
Spencer Ofwiti
7b83dbecd3 Merge branch 'master' into spencer/fix-account-index 2021-05-18 18:13:32 +03:00
Spencer Ofwiti
39af716b15 Merge branch 'master' into spencer/fix-account-index 2021-05-18 18:07:35 +03:00
Spencer Ofwiti
be3d389078 Refactor transaction events generator into one interface. 2021-05-18 16:17:35 +03:00
Spencer Ofwiti
a003bd7124 Add check for transaction in current transactions list. 2021-05-18 16:08:25 +03:00
Spencer Ofwiti
9235c969fa Use limit to query account index. 2021-05-18 15:37:30 +03:00
Spencer Ofwiti
ac231dc03e Add token service load event. 2021-05-18 13:24:41 +03:00
Spencer Ofwiti
8ae6436460 Refactor registry service imports. 2021-05-18 12:38:27 +03:00
Spencer Ofwiti
405199cfe3 Refactor loop to avoid passing index limit. 2021-05-18 10:42:24 +03:00
Spencer Ofwiti
08fda5390a Fix ABI query using http getter. 2021-05-18 10:38:19 +03:00
Spencer Ofwiti
506be2eb5b Add private key user info to settings page. 2021-05-18 10:33:38 +03:00
61 changed files with 8664 additions and 639 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/dist /dist
/tmp /tmp
/out-tsc /out-tsc
/out
# Only exists if Bazel was run # Only exists if Bazel was run
/bazel-out /bazel-out

7860
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
{ {
"name": "cic-staff-client", "name": "cic-staff-client",
"version": "0.0.0", "version": "0.0.1",
"author": "Spencer Ofwiti <maxspencer56@gmail.com>",
"description": "A fully featured admin client for managing users and transactions in the CIC network.",
"main": "dist/main.js",
"license": "GPL-3.0-or-later",
"scripts": { "scripts": {
"config:dev": "ts-node set-env.ts --environment=dev", "config:dev": "ts-node set-env.ts --environment=dev",
"config:prod": "ts-node set-env.ts --environment=prod", "config:prod": "ts-node set-env.ts --environment=prod",
@@ -18,7 +22,17 @@
"e2e": "ng e2e", "e2e": "ng e2e",
"precommit": "npm run format:fix && npm run lint", "precommit": "npm run format:fix && npm run lint",
"postinstall": "node patch-webpack.js", "postinstall": "node patch-webpack.js",
"prepare": "husky install" "prepare": "husky install",
"electron": "ng build --base-href ./ && electron .",
"electron-tsc": "ng build --base-href ./ && tsc --p src-backend --outDir dist && electron .",
"electron:build:dev": "ng build -c dev --base-href ./ && tsc --lib ES2018,DOM --target ES5 src-backend/main.ts --outDir dist && electron .",
"electron:package": "npx electron-forge import && npm run make",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"elec:package": "npm run build:dev && electron-packager . CICADA --out out --overwrite --asar --icon=dist/cic-staff-client/icons/manifest-icon-512.ico --ignore=^e2e$ --ignore=^src$ --ignore=^src-backend$ --ignore=^.editorconfig$ --ignore=^.gitignore$ --ignore=^angular.json$ --ignore=^browserslist$ --ignore=^karma.conf.js$ --ignore=^package-lock.json$ --ignore=^README.md$ --ignore=^tslint --ignore=^tsconfig --all",
"clean": "rimraf dist",
"prebuild": "npm run clean"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -38,6 +52,7 @@
"bootstrap": "^4.5.3", "bootstrap": "^4.5.3",
"cic-client": "0.1.4", "cic-client": "0.1.4",
"cic-client-meta": "0.0.7-alpha.6", "cic-client-meta": "0.0.7-alpha.6",
"electron-squirrel-startup": "^1.0.0",
"ethers": "^5.0.31", "ethers": "^5.0.31",
"http-server": "^0.12.3", "http-server": "^0.12.3",
"jquery": "^3.5.1", "jquery": "^3.5.1",
@@ -53,13 +68,21 @@
"@angular-devkit/build-angular": "~0.1002.0", "@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0", "@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.0", "@angular/compiler-cli": "~10.2.0",
"@electron-forge/cli": "^6.0.0-beta.55",
"@electron-forge/maker-deb": "^6.0.0-beta.55",
"@electron-forge/maker-rpm": "^6.0.0-beta.55",
"@electron-forge/maker-squirrel": "^6.0.0-beta.55",
"@electron-forge/maker-zip": "^6.0.0-beta.55",
"@types/datatables.net": "^1.10.19", "@types/datatables.net": "^1.10.19",
"@types/electron": "^1.6.10",
"@types/jasmine": "~3.5.0", "@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/jquery": "^3.5.4", "@types/jquery": "^3.5.4",
"@types/node": "^12.20.6", "@types/node": "^15.6.1",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"electron": "^12.0.9",
"electron-packager": "^15.2.0",
"husky": "^6.0.0", "husky": "^6.0.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
@@ -72,6 +95,7 @@
"prettier": "^2.3.0", "prettier": "^2.3.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"rimraf": "^3.0.2",
"secp256k1": "^4.0.2", "secp256k1": "^4.0.2",
"ts-node": "~8.3.0", "ts-node": "~8.3.0",
"tslint": "~6.1.0", "tslint": "~6.1.0",
@@ -85,5 +109,32 @@
"hooks": { "hooks": {
"pre-commit": "pretty-quick --staged & ng lint" "pre-commit": "pretty-quick --staged & ng lint"
} }
},
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "cic_staff_client"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
} }
} }

139
src-backend/main.ts Normal file
View File

@@ -0,0 +1,139 @@
import { BrowserWindow, Menu, Tray, app, ipcMain } from 'electron';
import * as path from 'path';
import * as url from 'url';
import * as fs from 'fs';
let mainWindow: BrowserWindow;
async function createWindow(): Promise<void> {
// Create the browser window.
mainWindow = new BrowserWindow({
fullscreen: false,
width: 1000,
height: 800,
icon: path.join(__dirname, '/cic-staff-client/assets/icons/manifest-icon-512.png'),
// Allows IPC and other APIs
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
nodeIntegrationInWorker: true,
}
});
// load the dist folder from Angular
await mainWindow.loadURL(
url.format({
pathname: path.join(__dirname, '/cic-staff-client/index.html'),
protocol: 'file',
slashes: true
})
);
// Open the DevTools.
// mainWindow.webContents.openDevTools();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
async function openModal(): Promise<void> {
const modal = new BrowserWindow({
parent: mainWindow,
modal: true,
show: false,
});
await modal.loadURL('https://dashboard.sarafu.network/');
modal.once('ready-to-show', () => {
modal.show();
});
}
function isRoot(): boolean {
return path.parse(process.cwd()).root === process.cwd();
}
function getImages(): void {
const cwd = process.cwd();
fs.readdir('.', { withFileTypes: true }, ((err, files) => {
if (!err) {
const re = /(?:\.([^.]+))?$/;
const images = files.filter(file => file.isFile() && ['jpg', 'png'].includes(re.exec(file.name)[1])).map(file => `file://${cwd}/${file.name}`);
mainWindow.webContents.send('getImagesResponse', images);
}
}));
}
function getDirectory(): void {
fs.readdir('.', { withFileTypes: true }, (err, files) => {
if (!err) {
const directories = files.filter(file => file.isDirectory()).map(file => file.name);
if (!isRoot()) {
directories.unshift('..');
}
mainWindow.webContents.send('getDirectoryResponse', directories);
}
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
let tray = null;
tray = new Tray(path.join(__dirname, '/cic-staff-client/assets/icons/manifest-icon-512.png'));
const contextMenu = Menu.buildFromTemplate([
{ label: 'Item1', type: 'radio' },
{ label: 'Item2', type: 'radio' },
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Exit', type: 'normal', click: () => { app.quit(); } },
]);
tray.setToolTip('CIC Administration Dashboard.');
// Overrides 'right-click' event.
tray.setContextMenu(contextMenu);
tray.on('click', (event, arg) => {
console.log('Systray was left-clicked.', event, arg);
});
tray.on('double-click', (event, arg) => {
console.log('Systray was double-clicked.', event, arg);
});
await createWindow();
});
app.on('activate', async () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
await createWindow();
}
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.on('my-custom-signal', (event, arg) => {
console.log('Print to the main process terminal (STDOUT) when signal received from renderer process.');
console.log(event);
console.log(arg);
mainWindow.webContents.send('other-custom-signal', 'message from the backend process');
});
ipcMain.on('navigateDirectory', (event, arg) => {
process.chdir(arg);
getImages();
getDirectory();
});
ipcMain.on('getFiles', (event, arg) => {
const files = fs.readdirSync(__dirname);
mainWindow.webContents.send('getFilesResponse', files);
});
ipcMain.on('openModal', async (event, arg) => {
await openModal();
});

View File

@@ -0,0 +1,9 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist",
"module": "commonjs",
"skipLibCheck": true
}
}

View File

@@ -1,8 +1,8 @@
import { environment } from '@src/environments/environment';
import Web3 from 'web3'; import Web3 from 'web3';
import { Web3Service } from '@app/_services/web3.service';
const abi: Array<any> = require('@src/assets/js/block-sync/data/AccountsIndex.json'); const abi: Array<any> = require('@src/assets/js/block-sync/data/AccountsIndex.json');
const web3: Web3 = new Web3(environment.web3Provider); const web3: Web3 = Web3Service.getInstance();
export class AccountIndex { export class AccountIndex {
contractAddress: string; contractAddress: string;

View File

@@ -1,8 +1,8 @@
import Web3 from 'web3'; import Web3 from 'web3';
import { environment } from '@src/environments/environment'; import { Web3Service } from '@app/_services/web3.service';
const abi: Array<any> = require('@src/assets/js/block-sync/data/TokenUniqueSymbolIndex.json'); const abi: Array<any> = require('@src/assets/js/block-sync/data/TokenUniqueSymbolIndex.json');
const web3: Web3 = new Web3(environment.web3Provider); const web3: Web3 = Web3Service.getInstance();
export class TokenRegistry { export class TokenRegistry {
contractAddress: string; contractAddress: string;

View File

@@ -1151,7 +1151,7 @@ export class MockBackendInterceptor implements HttpInterceptor {
const queriedCategory: Category = categories.find((category) => const queriedCategory: Category = categories.find((category) =>
category.products.includes(stringFromUrl()) category.products.includes(stringFromUrl())
); );
return ok(queriedCategory.name); return ok(queriedCategory.name || 'other');
} }
function getAreaNames(): Observable<HttpResponse<any>> { function getAreaNames(): Observable<HttpResponse<any>> {
@@ -1163,7 +1163,7 @@ export class MockBackendInterceptor implements HttpInterceptor {
const queriedAreaName: AreaName = areaNames.find((areaName) => const queriedAreaName: AreaName = areaNames.find((areaName) =>
areaName.locations.includes(stringFromUrl()) areaName.locations.includes(stringFromUrl())
); );
return ok(queriedAreaName.name); return ok(queriedAreaName.name || 'other');
} }
function getAreaTypes(): Observable<HttpResponse<any>> { function getAreaTypes(): Observable<HttpResponse<any>> {
@@ -1175,7 +1175,7 @@ export class MockBackendInterceptor implements HttpInterceptor {
const queriedAreaType: AreaType = areaTypes.find((areaType) => const queriedAreaType: AreaType = areaTypes.find((areaType) =>
areaType.area.includes(stringFromUrl()) areaType.area.includes(stringFromUrl())
); );
return ok(queriedAreaType.name); return ok(queriedAreaType.name || 'other');
} }
function getAccountTypes(): Observable<HttpResponse<any>> { function getAccountTypes(): Observable<HttpResponse<any>> {

View File

@@ -4,7 +4,7 @@ async function personValidation(person: any): Promise<void> {
const personValidationErrors: any = await validatePerson(person); const personValidationErrors: any = await validatePerson(person);
if (personValidationErrors) { if (personValidationErrors) {
personValidationErrors.map((error) => console.error(`${error.message}`)); personValidationErrors.map((error) => console.error(`${error.message}`, person, error));
} }
} }
@@ -12,7 +12,7 @@ async function vcardValidation(vcard: any): Promise<void> {
const vcardValidationErrors: any = await validateVcard(vcard); const vcardValidationErrors: any = await validateVcard(vcard);
if (vcardValidationErrors) { if (vcardValidationErrors) {
vcardValidationErrors.map((error) => console.error(`${error.message}`)); vcardValidationErrors.map((error) => console.error(`${error.message}`, vcard, error));
} }
} }

View File

@@ -4,7 +4,7 @@ interface Token {
address: string; address: string;
supply: string; supply: string;
decimals: string; decimals: string;
reserves: { reserves?: {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: { '0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: {
weight: string; weight: string;
balance: string; balance: string;

View File

@@ -6,7 +6,7 @@ const keyring = new openpgp.Keyring();
interface MutableKeyStore extends KeyStore { interface MutableKeyStore extends KeyStore {
loadKeyring(): void; loadKeyring(): void;
importKeyPair(publicKey: any, privateKey: any): Promise<void>; importKeyPair(publicKey: any, privateKey: any): Promise<void>;
importPublicKey(publicKey: any): void; importPublicKey(publicKey: any): Promise<void>;
importPrivateKey(privateKey: any): Promise<void>; importPrivateKey(privateKey: any): Promise<void>;
getPublicKeys(): Array<any>; getPublicKeys(): Array<any>;
getTrustedKeys(): Array<any>; getTrustedKeys(): Array<any>;
@@ -28,7 +28,7 @@ interface MutableKeyStore extends KeyStore {
removePublicKeyForId(keyId: string): any; removePublicKeyForId(keyId: string): any;
removePublicKey(publicKey: any): any; removePublicKey(publicKey: any): any;
clearKeysInKeyring(): void; clearKeysInKeyring(): void;
sign(plainText: string, passphrase: string): Promise<any>; sign(plainText: string): Promise<any>;
} }
class MutablePgpKeyStore implements MutableKeyStore { class MutablePgpKeyStore implements MutableKeyStore {
@@ -42,8 +42,8 @@ class MutablePgpKeyStore implements MutableKeyStore {
await keyring.privateKeys.importKey(privateKey); await keyring.privateKeys.importKey(privateKey);
} }
importPublicKey(publicKey: any): void { async importPublicKey(publicKey: any): Promise<void> {
keyring.publicKeys.importKey(publicKey); await keyring.publicKeys.importKey(publicKey);
} }
async importPrivateKey(privateKey: any): Promise<void> { async importPrivateKey(privateKey: any): Promise<void> {
@@ -150,10 +150,11 @@ class MutablePgpKeyStore implements MutableKeyStore {
keyring.clear(); keyring.clear();
} }
async sign(plainText: string, passphrase: string): Promise<any> { async sign(plainText): Promise<any> {
const privateKey = this.getPrivateKey(); const privateKey = this.getPrivateKey();
if (!privateKey.isDecrypted()) { if (!privateKey.isDecrypted()) {
await privateKey.decrypt(passphrase); const password = window.prompt('password');
await privateKey.decrypt(password);
} }
const opts = { const opts = {
message: openpgp.message.fromText(plainText), message: openpgp.message.fromText(plainText),

View File

@@ -7,14 +7,20 @@ import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp';
import { ErrorDialogService } from '@app/_services/error-dialog.service'; import { ErrorDialogService } from '@app/_services/error-dialog.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { HttpError, rejectBody } from '@app/_helpers/global-error-handler'; import { HttpError, rejectBody } from '@app/_helpers/global-error-handler';
import { Staff } from '@app/_models';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthService { export class AuthService {
sessionToken: any; sessionToken: any;
privateKey: any;
mutableKeyStore: MutableKeyStore; mutableKeyStore: MutableKeyStore;
trustedUsers: Array<Staff> = [];
private trustedUsersList: BehaviorSubject<Array<Staff>> = new BehaviorSubject<Array<Staff>>(
this.trustedUsers
);
trustedUsersSubject: Observable<Array<Staff>> = this.trustedUsersList.asObservable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
@@ -31,7 +37,6 @@ export class AuthService {
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
} }
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))); await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
} }
} }
@@ -96,32 +101,21 @@ export class AuthService {
}); });
} }
async passwordLogin(password: string): Promise<boolean> {
try {
const o = await this.getChallenge();
await this.loginResponse(o, password);
return true;
} catch (error) {
this.loggingService.sendErrorLevelMessage(
`Login challenge failed: Error ${error.status} - ${error.statusText}`,
this,
{ error }
);
}
return false;
}
async login(): Promise<boolean> { async login(): Promise<boolean> {
if (this.sessionToken !== undefined) { if (this.sessionToken !== undefined) {
try { try {
this.getWithToken(); const response: boolean = await this.getWithToken();
return true; return response === true;
} catch (error) { } catch (e) {
this.loggingService.sendErrorLevelMessage( this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e });
`Login token failed: Error ${error.status} - ${error.statusText}`, }
this, } else {
{ error } try {
); const o = await this.getChallenge();
const response: boolean = await this.loginResponse(o);
return response === true;
} catch (e) {
this.loggingService.sendErrorLevelMessage('Login challenge failed', this, { error: e });
} }
} }
return false; return false;
@@ -162,18 +156,23 @@ export class AuthService {
}); });
} }
loginView(): void {
document.getElementById('one').style.display = 'none';
document.getElementById('two').style.display = 'block';
this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
}
async setKey(privateKeyArmored): Promise<boolean> { async setKey(privateKeyArmored): Promise<boolean> {
try { try {
const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored); const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored);
if (!isValidKeyCheck) { if (!isValidKeyCheck) {
throw Error('The private key is invalid'); throw Error('The private key is invalid');
} }
const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey( // TODO leaving this out for now.
privateKeyArmored // const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
); // if (!isEncryptedKeyCheck) {
if (!isEncryptedKeyCheck) { // throw Error('The private key doesn\'t have a password!');
throw Error('The private key does not have a password!'); // }
}
const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored); const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
} catch (err) { } catch (err) {
@@ -187,6 +186,7 @@ export class AuthService {
}); });
return false; return false;
} }
this.loginView();
return true; return true;
} }
@@ -197,10 +197,22 @@ export class AuthService {
window.location.reload(); window.location.reload();
} }
getTrustedUsers(): any { addTrustedUser(user: Staff): void {
const trustedUsers: Array<any> = []; const savedIndex = this.trustedUsers.findIndex((staff) => staff.userid === user.userid);
this.mutableKeyStore.getPublicKeys().forEach((key) => trustedUsers.push(key.users[0].userId)); if (savedIndex === 0) {
return trustedUsers; return;
}
if (savedIndex > 0) {
this.trustedUsers.splice(savedIndex, 1);
}
this.trustedUsers.unshift(user);
this.trustedUsersList.next(this.trustedUsers);
}
getTrustedUsers(): void {
this.mutableKeyStore.getPublicKeys().forEach((key) => {
this.addTrustedUser(key.users[0].userId);
});
} }
async getPublicKeys(): Promise<any> { async getPublicKeys(): Promise<any> {
@@ -218,4 +230,8 @@ export class AuthService {
getPrivateKey(): any { getPrivateKey(): any {
return this.mutableKeyStore.getPrivateKey(); return this.mutableKeyStore.getPrivateKey();
} }
getPrivateKeyInfo(): any {
return this.getPrivateKey().users[0].userId;
}
} }

View File

@@ -6,6 +6,7 @@ import { TransactionService } from '@app/_services/transaction.service';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { LoggingService } from '@app/_services/logging.service'; import { LoggingService } from '@app/_services/logging.service';
import { RegistryService } from '@app/_services/registry.service'; import { RegistryService } from '@app/_services/registry.service';
import { Web3Service } from '@app/_services/web3.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -16,31 +17,33 @@ export class BlockSyncService {
constructor( constructor(
private transactionService: TransactionService, private transactionService: TransactionService,
private loggingService: LoggingService, private loggingService: LoggingService
private registryService: RegistryService
) {} ) {}
blockSync(address: string = null, offset: number = 0, limit: number = 100): void { async init(): Promise<void> {
await this.transactionService.init();
}
async blockSync(address: string = null, offset: number = 0, limit: number = 100): Promise<void> {
this.transactionService.resetTransactionsList(); this.transactionService.resetTransactionsList();
const settings: Settings = new Settings(this.scan); const settings: Settings = new Settings(this.scan);
const readyStateElements: { network: number } = { network: 2 }; const readyStateElements: { network: number } = { network: 2 };
settings.w3.provider = environment.web3Provider; settings.w3.provider = environment.web3Provider;
settings.w3.engine = this.registryService.getWeb3(); settings.w3.engine = Web3Service.getInstance();
settings.registry = this.registryService.getRegistry(); settings.registry = await RegistryService.getRegistry();
settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry); settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry);
settings.txHelper.ontransfer = async (transaction: any): Promise<void> => { settings.txHelper.ontransfer = async (transaction: any): Promise<void> => {
window.dispatchEvent(this.newTransferEvent(transaction)); window.dispatchEvent(this.newEvent(transaction, 'cic_transfer'));
}; };
settings.txHelper.onconversion = async (transaction: any): Promise<any> => { settings.txHelper.onconversion = async (transaction: any): Promise<any> => {
window.dispatchEvent(this.newConversionEvent(transaction)); window.dispatchEvent(this.newEvent(transaction, 'cic_convert'));
}; };
settings.registry.onload = (addressReturned: number): void => { // settings.registry.onload = (addressReturned: string): void => {
this.loggingService.sendInfoLevelMessage(`Loaded network contracts ${addressReturned}`); // this.loggingService.sendInfoLevelMessage(`Loaded network contracts ${addressReturned}`);
this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit); // this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit);
}; // };
this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit);
settings.registry.load();
} }
readyStateProcessor( readyStateProcessor(
@@ -78,16 +81,8 @@ export class BlockSyncService {
} }
} }
newTransferEvent(tx: any): any { newEvent(tx: any, eventType: string): any {
return new CustomEvent('cic_transfer', { return new CustomEvent(eventType, {
detail: {
tx,
},
});
}
newConversionEvent(tx: any): any {
return new CustomEvent('cic_convert', {
detail: { detail: {
tx, tx,
}, },

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ElectronService } from './electron.service';
describe('ElectronService', () => {
let service: ElectronService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ElectronService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { IpcRenderer } from 'electron';
import {BehaviorSubject} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ElectronService {
private ipc: IpcRenderer;
images = new BehaviorSubject<string[]>([]);
directory = new BehaviorSubject<string[]>([]);
constructor() {
if ((window as any).require) {
console.log('Pass');
try {
console.log('supported');
this.ipc = (window as any).require('electron').ipcRenderer;
} catch (error) {
console.error(error);
throw error;
}
} else {
console.warn('Could not load electron ipc');
}
}
testIpc(): void {
this.ipc.on('my-custom-signal', (event, arg) => {
console.log('Received acknowledged from backend about receipt of our signal.');
console.log(event);
console.log(arg);
});
this.ipc.on('getImageResponse', (event, images) => {
this.images.next(images);
});
this.ipc.on('getDirectoryResponse', (event, directory) => {
this.directory.next(directory);
});
console.log('Sending message to backend.');
this.ipc.send('my-custom-signal', 'hello, are you there?');
}
navigateDirectory(path): any {
this.ipc.send('navigateDirectory', path);
}
async getFiles(): Promise<string[]> {
console.log('Starting');
return new Promise<string[]>((resolve, reject) => {
this.ipc.once('getFilesResponse', (event, arg) => {
resolve(arg);
});
this.ipc.send('getFiles');
});
}
openModal(): void {
console.log('Open a modal.');
this.ipc.send('openModal');
}
}

View File

@@ -6,3 +6,4 @@ export * from '@app/_services/block-sync.service';
export * from '@app/_services/location.service'; export * from '@app/_services/location.service';
export * from '@app/_services/logging.service'; export * from '@app/_services/logging.service';
export * from '@app/_services/error-dialog.service'; export * from '@app/_services/error-dialog.service';
export * from '@app/_services/web3.service';

View File

@@ -1,33 +1,30 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import Web3 from 'web3';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { CICRegistry, FileGetter } from 'cic-client'; import { CICRegistry, FileGetter } from 'cic-client';
import { HttpGetter } from '@app/_helpers'; import { HttpGetter } from '@app/_helpers';
import { Web3Service } from '@app/_services/web3.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class RegistryService { export class RegistryService {
web3: Web3 = new Web3(environment.web3Provider); static fileGetter: FileGetter = new HttpGetter();
fileGetter: FileGetter = new HttpGetter(); private static registry: CICRegistry;
registry: CICRegistry = new CICRegistry(
this.web3,
environment.registryAddress,
'Registry',
this.fileGetter,
['../../assets/js/block-sync/data']
);
constructor() { constructor() {}
this.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
this.registry.load();
}
getRegistry(): any { public static async getRegistry(): Promise<CICRegistry> {
return this.registry; if (!RegistryService.registry) {
} RegistryService.registry = new CICRegistry(
Web3Service.getInstance(),
getWeb3(): any { environment.registryAddress,
return this.web3; 'Registry',
RegistryService.fileGetter,
['../../assets/js/block-sync/data']
);
RegistryService.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
await RegistryService.registry.load();
}
return RegistryService.registry;
} }
} }

View File

@@ -1,10 +1,10 @@
import { EventEmitter, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from '@src/environments/environment';
import { BehaviorSubject, Observable } from 'rxjs';
import { CICRegistry } from 'cic-client'; import { CICRegistry } from 'cic-client';
import { TokenRegistry } from '@app/_eth'; import { TokenRegistry } from '@app/_eth';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { RegistryService } from '@app/_services/registry.service'; import { RegistryService } from '@app/_services/registry.service';
import { Token } from '@app/_models';
import {BehaviorSubject, Observable, Subject} from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -12,29 +12,65 @@ import { RegistryService } from '@app/_services/registry.service';
export class TokenService { export class TokenService {
registry: CICRegistry; registry: CICRegistry;
tokenRegistry: TokenRegistry; tokenRegistry: TokenRegistry;
LoadEvent: EventEmitter<number> = new EventEmitter<number>(); 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();
constructor(private httpClient: HttpClient, private registryService: RegistryService) { constructor(private httpClient: HttpClient) {}
this.registry = registryService.getRegistry();
this.registry.load(); async init(): Promise<void> {
this.registry = await RegistryService.getRegistry();
this.registry.onload = async (address: string): Promise<void> => { this.registry.onload = async (address: string): Promise<void> => {
this.tokenRegistry = new TokenRegistry( this.tokenRegistry = new TokenRegistry(
await this.registry.getContractAddressByName('TokenRegistry') await this.registry.getContractAddressByName('TokenRegistry')
); );
this.LoadEvent.next(Date.now()); this.onload(this.tokenRegistry !== undefined);
}; };
} }
async getTokens(): Promise<Array<Promise<string>>> { addToken(token: Token): void {
const savedIndex = this.tokens.findIndex((tk) => tk.address === token.address);
if (savedIndex === 0) {
return;
}
if (savedIndex > 0) {
this.tokens.splice(savedIndex, 1);
}
this.tokens.unshift(token);
this.tokensList.next(this.tokens);
}
async getTokens(): Promise<void> {
const count: number = await this.tokenRegistry.totalTokens(); const count: number = await this.tokenRegistry.totalTokens();
return Array.from({ length: count }, async (v, i) => await this.tokenRegistry.entry(i)); for (let i = 0; i < count; i++) {
const token: Token = await this.getTokenByAddress(await this.tokenRegistry.entry(i));
this.addToken(token);
}
} }
getTokenBySymbol(symbol: string): Observable<any> { async getTokenByAddress(address: string): Promise<Token> {
return this.httpClient.get(`${environment.cicCacheUrl}/tokens/${symbol}`); const token: any = {};
const tokenContract = await this.registry.addToken(address);
token.address = address;
token.name = await tokenContract.methods.name().call();
token.symbol = await tokenContract.methods.symbol().call();
token.supply = await tokenContract.methods.totalSupply().call();
token.decimals = await tokenContract.methods.decimals().call();
return token;
} }
async getTokenBalance(address: string): Promise<number> { async getTokenBySymbol(symbol: string): Promise<Observable<Token>> {
const tokenSubject: Subject<Token> = new Subject<Token>();
await this.getTokens();
this.tokensSubject.subscribe((tokens) => {
const queriedToken = tokens.find((token) => token.symbol === symbol);
tokenSubject.next(queriedToken);
});
return tokenSubject.asObservable();
}
async getTokenBalance(address: string): Promise<(address: string) => Promise<number>> {
const sarafuToken = await this.registry.addToken(await this.tokenRegistry.entry(0)); const sarafuToken = await this.registry.addToken(await this.tokenRegistry.entry(0));
return await sarafuToken.methods.balanceOf(address).call(); return await sarafuToken.methods.balanceOf(address).call();
} }

View File

@@ -17,6 +17,7 @@ import { HttpClient } from '@angular/common/http';
import { CICRegistry } from 'cic-client'; import { CICRegistry } from 'cic-client';
import { RegistryService } from '@app/_services/registry.service'; import { RegistryService } from '@app/_services/registry.service';
import Web3 from 'web3'; import Web3 from 'web3';
import { Web3Service } from '@app/_services/web3.service';
const vCard = require('vcard-parser'); const vCard = require('vcard-parser');
@Injectable({ @Injectable({
@@ -34,12 +35,15 @@ export class TransactionService {
private httpClient: HttpClient, private httpClient: HttpClient,
private authService: AuthService, private authService: AuthService,
private userService: UserService, private userService: UserService,
private loggingService: LoggingService, private loggingService: LoggingService
private registryService: RegistryService
) { ) {
this.web3 = this.registryService.getWeb3(); this.web3 = Web3Service.getInstance();
this.registry = registryService.getRegistry(); }
this.registry.load();
async init(): Promise<void> {
await this.authService.init();
await this.userService.init();
this.registry = await RegistryService.getRegistry();
} }
getAllTransactions(offset: number, limit: number): Observable<any> { getAllTransactions(offset: number, limit: number): Observable<any> {
@@ -47,7 +51,7 @@ export class TransactionService {
} }
getAddressTransactions(address: string, offset: number, limit: number): Observable<any> { getAddressTransactions(address: string, offset: number, limit: number): Observable<any> {
return this.httpClient.get(`${environment.cicCacheUrl}/tx/${address}/${offset}/${limit}`); return this.httpClient.get(`${environment.cicCacheUrl}/tx/user/${address}/${offset}/${limit}`);
} }
async setTransaction(transaction, cacheSize: number): Promise<void> { async setTransaction(transaction, cacheSize: number): Promise<void> {
@@ -62,10 +66,11 @@ export class TransactionService {
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(res) => { (res) => {
transaction.sender = this.getAccountInfo(res.body); transaction.sender = this.getAccountInfo(res, cacheSize);
}, },
(error) => { (error) => {
transaction.sender = defaultAccount; transaction.sender = defaultAccount;
this.userService.addAccount(defaultAccount, cacheSize);
} }
); );
this.userService this.userService
@@ -73,10 +78,11 @@ export class TransactionService {
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(res) => { (res) => {
transaction.recipient = this.getAccountInfo(res.body); transaction.recipient = this.getAccountInfo(res, cacheSize);
}, },
(error) => { (error) => {
transaction.recipient = defaultAccount; transaction.recipient = defaultAccount;
this.userService.addAccount(defaultAccount, cacheSize);
} }
); );
} finally { } finally {
@@ -97,10 +103,11 @@ export class TransactionService {
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(res) => { (res) => {
conversion.sender = conversion.recipient = this.getAccountInfo(res.body); conversion.sender = conversion.recipient = this.getAccountInfo(res);
}, },
(error) => { (error) => {
conversion.sender = conversion.recipient = defaultAccount; conversion.sender = conversion.recipient = defaultAccount;
this.userService.addAccount(defaultAccount, cacheSize);
} }
); );
} finally { } finally {
@@ -109,9 +116,16 @@ export class TransactionService {
} }
addTransaction(transaction, cacheSize: number): void { addTransaction(transaction, cacheSize: number): void {
const savedIndex = this.transactions.findIndex((tx) => tx.tx.txHash === transaction.tx.txHash);
if (savedIndex === 0) {
return;
}
if (savedIndex > 0) {
this.transactions.splice(savedIndex, 1);
}
this.transactions.unshift(transaction); this.transactions.unshift(transaction);
if (this.transactions.length > cacheSize) { if (this.transactions.length > cacheSize) {
this.transactions.length = cacheSize; this.transactions.length = Math.min(this.transactions.length, cacheSize);
} }
this.transactionList.next(this.transactions); this.transactionList.next(this.transactions);
} }
@@ -121,9 +135,10 @@ export class TransactionService {
this.transactionList.next(this.transactions); this.transactionList.next(this.transactions);
} }
getAccountInfo(account: string): any { getAccountInfo(account: string, cacheSize: number = 100): any {
const accountInfo = Envelope.fromJSON(JSON.stringify(account)).unwrap().m.data; const accountInfo = Envelope.fromJSON(JSON.stringify(account)).unwrap().m.data;
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard)); accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
this.userService.addAccount(accountInfo, cacheSize);
return accountInfo; return accountInfo;
} }
@@ -133,41 +148,43 @@ export class TransactionService {
recipientAddress: string, recipientAddress: string,
value: number value: number
): Promise<any> { ): Promise<any> {
const transferAuthAddress = await this.registry.getContractAddressByName( this.registry.onload = async (addressReturned: string): Promise<void> => {
'TransferAuthorization' const transferAuthAddress = await this.registry.getContractAddressByName(
); 'TransferAuthorization'
const hashFunction = new Keccak(256); );
hashFunction.update('createRequest(address,address,address,uint256)'); const hashFunction = new Keccak(256);
const hash = hashFunction.digest(); hashFunction.update('createRequest(address,address,address,uint256)');
const methodSignature = hash.toString('hex').substring(0, 8); const hash = hashFunction.digest();
const abiCoder = new utils.AbiCoder(); const methodSignature = hash.toString('hex').substring(0, 8);
const abi = await abiCoder.encode( const abiCoder = new utils.AbiCoder();
['address', 'address', 'address', 'uint256'], const abi = await abiCoder.encode(
[senderAddress, recipientAddress, tokenAddress, value] ['address', 'address', 'address', 'uint256'],
); [senderAddress, recipientAddress, tokenAddress, value]
const data = fromHex(methodSignature + strip0x(abi)); );
const tx = new Tx(environment.bloxbergChainId); const data = fromHex(methodSignature + strip0x(abi));
tx.nonce = await this.web3.eth.getTransactionCount(senderAddress); const tx = new Tx(environment.bloxbergChainId);
tx.gasPrice = Number(await this.web3.eth.getGasPrice()); tx.nonce = await this.web3.eth.getTransactionCount(senderAddress);
tx.gasLimit = 8000000; tx.gasPrice = Number(await this.web3.eth.getGasPrice());
tx.to = fromHex(strip0x(transferAuthAddress)); tx.gasLimit = 8000000;
tx.value = toValue(value); tx.to = fromHex(strip0x(transferAuthAddress));
tx.data = data; tx.value = toValue(value);
const txMsg = tx.message(); tx.data = data;
const privateKey = this.authService.mutableKeyStore.getPrivateKey(); const txMsg = tx.message();
if (!privateKey.isDecrypted()) { const privateKey = this.authService.mutableKeyStore.getPrivateKey();
const password = window.prompt('password'); if (!privateKey.isDecrypted()) {
await privateKey.decrypt(password); 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 signatureObject = secp256k1.ecdsaSign(txMsg, privateKey.keyPacket.privateParams.d);
const s = signatureObject.signature.slice(32); const r = signatureObject.signature.slice(0, 32);
const v = signatureObject.recid; const s = signatureObject.signature.slice(32);
tx.setSignature(r, s, v); const v = signatureObject.recid;
const txWire = add0x(toHex(tx.serializeRLP())); tx.setSignature(r, s, v);
const result = await this.web3.eth.sendSignedTransaction(txWire); const txWire = add0x(toHex(tx.serializeRLP()));
this.loggingService.sendInfoLevelMessage(`Result: ${result}`); const result = await this.web3.eth.sendSignedTransaction(txWire);
const transaction = await this.web3.eth.getTransaction(result.transactionHash); this.loggingService.sendInfoLevelMessage(`Result: ${result}`);
this.loggingService.sendInfoLevelMessage(`Transaction: ${transaction}`); const transaction = await this.web3.eth.getTransaction(result.transactionHash);
this.loggingService.sendInfoLevelMessage(`Transaction: ${transaction}`);
};
} }
} }

View File

@@ -39,20 +39,20 @@ export class UserService {
private httpClient: HttpClient, private httpClient: HttpClient,
private loggingService: LoggingService, private loggingService: LoggingService,
private tokenService: TokenService, private tokenService: TokenService,
private registryService: RegistryService,
private authService: AuthService private authService: AuthService
) { ) {}
this.authService.init().then(() => {
this.keystore = authService.mutableKeyStore; async init(): Promise<void> {
this.signer = new PGPSigner(this.keystore); await this.authService.init();
}); await this.tokenService.init();
this.registry = registryService.getRegistry(); this.keystore = this.authService.mutableKeyStore;
this.registry.load(); this.signer = new PGPSigner(this.keystore);
this.registry = await RegistryService.getRegistry();
} }
resetPin(phone: string): Observable<any> { resetPin(phone: string): Observable<any> {
const params: HttpParams = new HttpParams().set('phoneNumber', phone); const params: HttpParams = new HttpParams().set('phoneNumber', phone);
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params }); return this.httpClient.put(`${environment.cicUssdUrl}/pin`, { params });
} }
getAccountStatus(phone: string): Observable<any> { getAccountStatus(phone: string): Observable<any> {
@@ -183,9 +183,7 @@ export class UserService {
'AccountRegistry' 'AccountRegistry'
); );
const accountIndexQuery = new AccountIndex(accountIndexAddress); const accountIndexQuery = new AccountIndex(accountIndexAddress);
const accountAddresses: Array<string> = await accountIndexQuery.last( const accountAddresses: Array<string> = await accountIndexQuery.last(limit);
await accountIndexQuery.totalAccounts()
);
this.loggingService.sendInfoLevelMessage(accountAddresses); this.loggingService.sendInfoLevelMessage(accountAddresses);
for (const accountAddress of accountAddresses.slice(offset, offset + limit)) { for (const accountAddress of accountAddresses.slice(offset, offset + limit)) {
await this.getAccountByAddress(accountAddress, limit); await this.getAccountByAddress(accountAddress, limit);
@@ -203,16 +201,14 @@ export class UserService {
const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
const accountInfo = account.m.data; const accountInfo = account.m.data;
await personValidation(accountInfo); await personValidation(accountInfo);
accountInfo.balance = await this.tokenService.getTokenBalance( this.tokenService.onload = async (status: boolean): Promise<void> => {
accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] accountInfo.balance = await this.tokenService.getTokenBalance(
); accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]
);
};
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard)); accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
await vcardValidation(accountInfo.vcard); await vcardValidation(accountInfo.vcard);
this.accounts.unshift(accountInfo); this.addAccount(accountInfo, limit);
if (this.accounts.length > limit) {
this.accounts.length = limit;
}
this.accountsList.next(this.accounts);
accountSubject.next(accountInfo); accountSubject.next(accountInfo);
}); });
return accountSubject.asObservable(); return accountSubject.asObservable();
@@ -264,4 +260,23 @@ export class UserService {
getGenders(): Observable<any> { getGenders(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/genders`); return this.httpClient.get(`${environment.cicMetaUrl}/genders`);
} }
addAccount(account: AccountDetails, cacheSize: number): void {
const savedIndex = this.accounts.findIndex(
(acc) =>
acc.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0] ===
account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]
);
if (savedIndex === 0) {
return;
}
if (savedIndex > 0) {
this.accounts.splice(savedIndex, 1);
}
this.accounts.unshift(account);
if (this.accounts.length > cacheSize) {
this.accounts.length = Math.min(this.accounts.length, cacheSize);
}
this.accountsList.next(this.accounts);
}
} }

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { Web3Service } from './web3.service';
describe('Web3Service', () => {
let service: Web3Service;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(Web3Service);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import Web3 from 'web3';
import { environment } from '@src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class Web3Service {
private static web3: Web3;
constructor() {}
public static getInstance(): Web3 {
if (!Web3Service.web3) {
Web3Service.web3 = new Web3(environment.web3Provider);
}
return Web3Service.web3;
}
}

View File

@@ -1,2 +1 @@
<app-network-status></app-network-status>
<router-outlet (activate)="onResize(mediaQuery)"></router-outlet> <router-outlet (activate)="onResize(mediaQuery)"></router-outlet>

View File

@@ -27,28 +27,23 @@ export class AppComponent implements OnInit {
private errorDialogService: ErrorDialogService, private errorDialogService: ErrorDialogService,
private swUpdate: SwUpdate private swUpdate: SwUpdate
) { ) {
(async () => {
try {
await this.authService.init();
// this.authService.getPublicKeys()
// .pipe(catchError(async (error) => {
// this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
// this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
// })).subscribe(this.authService.mutableKeyStore.importPublicKey);
const publicKeys = await this.authService.getPublicKeys();
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
} catch (error) {
this.errorDialogService.openDialog({
message: 'Trusted keys endpoint cannot be reached. Please try again later.',
});
// TODO do something to halt user progress...show a sad cicada page 🦗?
}
})();
this.mediaQuery.addEventListener('change', this.onResize); this.mediaQuery.addEventListener('change', this.onResize);
this.onResize(this.mediaQuery); this.onResize(this.mediaQuery);
} }
ngOnInit(): void { async ngOnInit(): Promise<void> {
await this.authService.init();
await this.transactionService.init();
try {
const publicKeys = await this.authService.getPublicKeys();
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
this.authService.getTrustedUsers();
} catch (error) {
this.errorDialogService.openDialog({
message: 'Trusted keys endpoint cannot be reached. Please try again later.',
});
// TODO do something to halt user progress...show a sad cicada page 🦗?
}
if (!this.swUpdate.isEnabled) { if (!this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => { this.swUpdate.available.subscribe(() => {
if (confirm('New Version available. Load New Version?')) { if (confirm('New Version available. Load New Version?')) {

View File

@@ -1,13 +1,14 @@
<app-network-status></app-network-status>
<div class="container"> <div class="container">
<div class="row justify-content-center mt-5 mb-5"> <div class="row justify-content-center mt-5 mb-5">
<div class="col-lg-6 col-md-8 col-sm-10"> <div class="col-lg-6 col-md-8 col-sm-10">
<div class="card"> <div class="card">
<mat-card-title class="card-header pt-4 pb-4 text-center bg-dark"> <mat-card-title class="card-header pt-4 pb-4 text-center background-dark">
<a routerLink="/"> <a routerLink="/">
<h1 class="text-white">CICADA</h1> <h1 class="text-white">CICADA</h1>
</a> </a>
</mat-card-title> </mat-card-title>
<div id="one" style="display: block" class="card-body p-4 align-items-center"> <div id="one" style="display: block" class="card-body p-4">
<div class="text-center w-75 m-auto"> <div class="text-center w-75 m-auto">
<h4 class="text-dark-50 text-center font-weight-bold">Add Private Key</h4> <h4 class="text-dark-50 text-center font-weight-bold">Add Private Key</h4>
@@ -19,13 +20,13 @@
<mat-label>Private Key</mat-label> <mat-label>Private Key</mat-label>
<textarea matInput style="height: 30rem" formControlName="key" placeholder="Enter your private key..." <textarea matInput style="height: 30rem" formControlName="key" placeholder="Enter your private key..."
[errorStateMatcher]="matcher"></textarea> [errorStateMatcher]="matcher"></textarea>
<div *ngIf="keyFormSubmitted && keyFormStub.key.errors" class="invalid-feedback"> <div *ngIf="submitted && keyFormStub.key.errors" class="invalid-feedback">
<mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error> <mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error>
</div> </div>
</mat-form-field> </mat-form-field>
<button mat-raised-button matRipple color="primary" type="submit" [disabled]="keyFormLoading"> <button mat-raised-button matRipple color="primary" type="submit" [disabled]="loading">
<span *ngIf="keyFormLoading" class="spinner-border spinner-border-sm mr-1"></span> <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Add Key Add Key
</button> </button>
@@ -33,43 +34,6 @@
</div> </div>
<div id="two" style="display: none" class="card-body p-4 align-items-center"> <div id="two" style="display: none" class="card-body p-4 align-items-center">
<div class="text-center w-75 m-auto">
<h4 id="passwordState" class="text-dark-50 text-center font-weight-bold"></h4>
</div>
<div class="center-items">
<form [formGroup]="passwordForm" (ngSubmit)="onPasswordInput()">
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" placeholder="Enter your private key password..."
[errorStateMatcher]="matcher">
<div *ngIf="passwordForm && passwordFormStub.password.errors" class="invalid-feedback">
<mat-error *ngIf="passwordFormStub.password.errors.required">Private Key password is required.</mat-error>
</div>
</mat-form-field>
<button id="passwordLoginButton" mat-raised-button matRipple color="primary" type="submit" class="ml-3" [disabled]="passwordFormLoading">
<span *ngIf="passwordFormLoading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
</form>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<p class="text-muted">Change private key?
<a (click)="keyInput()" class="text-muted ml-1">
<b>Enter private key</b>
</a>
</p>
</div> <!-- end col-->
</div>
<!-- end row -->
</div>
<div id="three" style="display: none" class="card-body p-4 align-items-center">
<div class="text-center w-75 m-auto"> <div class="text-center w-75 m-auto">
<h4 id="state" class="text-dark-50 text-center font-weight-bold"></h4> <h4 id="state" class="text-dark-50 text-center font-weight-bold"></h4>
<button mat-raised-button matRipple color="primary" type="submit" (click)="login()"> Login </button> <button mat-raised-button matRipple color="primary" type="submit" (click)="login()"> Login </button>
@@ -77,11 +41,7 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 text-center"> <div class="col-12 text-center">
<p class="text-muted">Change private key? <p class="text-muted">Change private key? <a (click)="switchWindows()" class="text-muted ml-1"><b>Enter private key</b></a></p>
<a (click)="keyInput()" class="text-muted ml-1">
<b>Enter private key</b>
</a>
</p>
</div> <!-- end col--> </div> <!-- end col-->
</div> </div>
<!-- end row --> <!-- end row -->

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CustomErrorStateMatcher } from '@app/_helpers'; import { CustomErrorStateMatcher } from '@app/_helpers';
import { AuthService } from '@app/_services'; import { AuthService } from '@app/_services';
@@ -12,126 +12,68 @@ import { Router } from '@angular/router';
}) })
export class AuthComponent implements OnInit { export class AuthComponent implements OnInit {
keyForm: FormGroup; keyForm: FormGroup;
submitted: boolean = false;
loading: boolean = false;
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher(); matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
keyFormSubmitted: boolean = false;
keyFormLoading: boolean = false;
passwordForm: FormGroup;
passwordFormSubmitted: boolean = false;
passwordFormLoading: boolean = false;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private router: Router, private router: Router
private cdr: ChangeDetectorRef
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.keyForm = this.formBuilder.group({ this.keyForm = this.formBuilder.group({
key: ['', Validators.required], key: ['', Validators.required],
}); });
this.passwordForm = this.formBuilder.group({ await this.authService.init();
password: ['', Validators.required], // if (this.authService.privateKey !== undefined) {
}); // const setKey = await this.authService.setKey(this.authService.privateKey);
if (this.authService.privateKey !== undefined) { // }
const setKey = await this.authService.setKey(this.authService.privateKey); // }
if (setKey) {
this.passwordInput();
}
if (setKey && this.authService.sessionToken !== undefined) {
this.loginView();
}
}
} }
get keyFormStub(): any { get keyFormStub(): any {
return this.keyForm.controls; return this.keyForm.controls;
} }
get passwordFormStub(): any {
return this.passwordForm.controls;
}
async onSubmit(): Promise<void> { async onSubmit(): Promise<void> {
this.keyFormSubmitted = true; this.submitted = true;
if (this.keyForm.invalid) { if (this.keyForm.invalid) {
return; return;
} }
this.keyFormLoading = true; this.loading = true;
const keySetup = await this.authService.setKey(this.keyFormStub.key.value); await this.authService.setKey(this.keyFormStub.key.value);
if (keySetup) { this.loading = false;
this.passwordInput();
}
this.keyFormLoading = false;
this.cdr.detectChanges();
}
async onPasswordInput(): Promise<void> {
this.passwordFormSubmitted = true;
if (this.passwordForm.invalid) {
return;
}
this.passwordFormLoading = true;
const passwordLogin = await this.authService.passwordLogin(
this.passwordFormStub.password.value
);
if (passwordLogin) {
this.loginView();
}
this.passwordFormLoading = false;
this.cdr.detectChanges();
} }
login(): void { login(): void {
if (this.authService.sessionToken === undefined) { // TODO check if we have privatekey
this.passwordInput(); // Send us to home if we have a private key
} // talk to meta somehow
const loginStatus = this.authService.login(); // in the error interceptor if 401/403 handle it
if (loginStatus) { // if 200 go /home
if (this.authService.getPrivateKey()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} }
} }
keyInput(): void { switchWindows(): void {
this.authService.sessionToken = undefined; this.authService.sessionToken = undefined;
this.switchWindows(true, false, false); const divOne: HTMLElement = document.getElementById('one');
const divTwo: HTMLElement = document.getElementById('two');
this.toggleDisplay(divOne);
this.toggleDisplay(divTwo);
} }
passwordInput(): void { toggleDisplay(element: any): void {
this.authService.sessionToken = undefined;
this.switchWindows(false, true, false);
this.setPasswordState(
'Enter Password to log in with PGP key ' + this.authService.mutableKeyStore.getPrivateKeyId()
);
}
loginView(): void {
this.switchWindows(false, false, true);
this.authService.setState('Click button to log in');
}
switchWindows(divOneStatus: boolean, divTwoStatus: boolean, divThreeStatus: boolean): void {
const divOne = document.getElementById('one');
const divTwo = document.getElementById('two');
const divThree = document.getElementById('three');
this.toggleDisplay(divOne, divOneStatus);
this.toggleDisplay(divTwo, divTwoStatus);
this.toggleDisplay(divThree, divThreeStatus);
}
toggleDisplay(element: any, active: boolean): void {
const style: string = window.getComputedStyle(element).display; const style: string = window.getComputedStyle(element).display;
if (active) { if (style === 'block') {
element.style.display = 'block';
} else {
element.style.display = 'none'; element.style.display = 'none';
} else {
element.style.display = 'block';
} }
} }
setPasswordState(s): void {
document.getElementById('passwordState').innerHTML = s;
}
} }

View File

@@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
import { SharedModule } from '@app/shared/shared.module';
@NgModule({ @NgModule({
declarations: [AuthComponent, PasswordToggleDirective], declarations: [AuthComponent, PasswordToggleDirective],
@@ -22,6 +23,7 @@ import { MatRippleModule } from '@angular/material/core';
MatInputModule, MatInputModule,
MatButtonModule, MatButtonModule,
MatRippleModule, MatRippleModule,
SharedModule,
], ],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -34,7 +34,7 @@
<strong> {{account?.vcard?.fn[0].value}} </strong> <strong> {{account?.vcard?.fn[0].value}} </strong>
</h3> </h3>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span> <span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span>
<span class="ml-2"><strong>Created:</strong> {{account?.date_registered | date}}</span> <span class="ml-2"><strong>Created:</strong> {{account?.date_registered | unixDate}}</span>
<span class="ml-2"><strong>Address:</strong> <span class="ml-2"><strong>Address:</strong>
<a href="{{bloxbergLink}}" target="_blank"> {{accountAddress}} </a> <a href="{{bloxbergLink}}" target="_blank"> {{accountAddress}} </a>
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress()" alt="Copy"> <img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress()" alt="Copy">
@@ -48,10 +48,19 @@
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Name(s): *</mat-label> <mat-label>First Name: *</mat-label>
<input matInput type="text" id="givenNames" placeholder="{{account?.vcard?.fn[0].value}}" <input matInput type="text" id="firstName" placeholder="{{account?.vcard?.fn[0].value.split(' ')[0]}}"
value="{{account?.vcard?.fn[0].value}}" formControlName="name" [errorStateMatcher]="matcher"> value="{{account?.vcard?.fn[0].value.split(' ')[0]}}" formControlName="firstName" [errorStateMatcher]="matcher">
<mat-error *ngIf="submitted && accountInfoFormStub.name.errors">Name is required.</mat-error> <mat-error *ngIf="submitted && accountInfoFormStub.firstName.errors">First Name is required.</mat-error>
</mat-form-field>
</div>
<div class="col-md-6 col-lg-4">
<mat-form-field appearance="outline">
<mat-label>Last Name(s): *</mat-label>
<input matInput type="text" id="lastName" placeholder="{{account?.vcard?.fn[0].value.split(' ').slice(1).join(' ')}}"
value="{{account?.vcard?.fn[0].value.split(' ').slice(1).join(' ')}}" formControlName="lastName" [errorStateMatcher]="matcher">
<mat-error *ngIf="submitted && accountInfoFormStub.lastName.errors">Last Name is required.</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
@@ -210,12 +219,9 @@
<tr> <tr>
<td>{{account?.vcard?.fn[0].value}}</td> <td>{{account?.vcard?.fn[0].value}}</td>
<td>{{account?.balance | tokenRatio}}</td> <td>{{account?.balance | tokenRatio}}</td>
<td>{{account?.date_registered | date}}</td> <td>{{account?.date_registered | unixDate}}</td>
<td> <td>
<span *ngIf="accountStatus === 'active'" class="badge badge-success badge-pill"> <span class="badge badge-success badge-pill">
{{accountStatus}}
</span>
<span *ngIf="accountStatus === 'blocked'" class="badge badge-danger badge-pill">
{{accountStatus}} {{accountStatus}}
</span> </span>
</td> </td>
@@ -228,7 +234,7 @@
<mat-tab-group *ngIf="account" dynamicHeight mat-align-tabs="start"> <mat-tab-group *ngIf="account" dynamicHeight mat-align-tabs="start">
<mat-tab label="Transactions"> <mat-tab label="Transactions">
<app-transaction-details [transaction]="transaction"></app-transaction-details> <app-transaction-details [transaction]="transaction" (closeWindow)="transaction = $event"></app-transaction-details>
<div class="card mt-1"> <div class="card mt-1">
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
@@ -252,17 +258,17 @@
<mat-icon matSuffix>search</mat-icon> <mat-icon matSuffix>search</mat-icon>
</mat-form-field> </mat-form-field>
<mat-table class="mat-elevation-z10" [dataSource]="transactionsDataSource" matSort matSortActive="created" <table mat-table class="mat-elevation-z10" [dataSource]="transactionsDataSource" matSort matSortActive="created"
#TransactionTableSort="matSort" matSortDirection="asc" matSortDisableClear> #TransactionTableSort="matSort" matSortDirection="asc" matSortDisableClear>
<ng-container matColumnDef="sender"> <ng-container matColumnDef="sender">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Sender </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Sender </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.sender?.vcard.fn[0].value}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.sender?.vcard.fn[0].value || transaction.from}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="recipient"> <ng-container matColumnDef="recipient">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Recipient </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Recipient </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.recipient?.vcard.fn[0].value}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.recipient?.vcard.fn[0].value || transaction.to}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
@@ -275,7 +281,7 @@
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Created </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Created </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.tx.timestamp | date}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.tx.timestamp | unixDate}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
@@ -285,10 +291,10 @@
</td> </td>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="transactionsDisplayedColumns"></mat-header-row> <tr mat-header-row *matHeaderRowDef="transactionsDisplayedColumns"></tr>
<mat-row *matRowDef="let transaction; columns: transactionsDisplayedColumns" matRipple <tr mat-row *matRowDef="let transaction; columns: transactionsDisplayedColumns" matRipple
(click)="viewTransaction(transaction)"></mat-row> (click)="viewTransaction(transaction)"></tr>
</mat-table> </table>
<mat-paginator #TransactionTablePaginator="matPaginator" [pageSize]="transactionsDefaultPageSize" <mat-paginator #TransactionTablePaginator="matPaginator" [pageSize]="transactionsDefaultPageSize"
[pageSizeOptions]="transactionsPageSizeOptions" showFirstLastButtons></mat-paginator> [pageSizeOptions]="transactionsPageSizeOptions" showFirstLastButtons></mat-paginator>
@@ -337,7 +343,7 @@
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell>
<mat-cell *matCellDef="let user"> {{user?.date_registered | date}} </mat-cell> <mat-cell *matCellDef="let user"> {{user?.date_registered | unixDate}} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="balance"> <ng-container matColumnDef="balance">

View File

@@ -78,8 +78,17 @@ export class AccountDetailsComponent implements OnInit {
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) { ) {
this.route.paramMap.subscribe((params: Params) => {
this.accountAddress = add0x(params.get('id'));
this.bloxbergLink =
'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions';
});
}
async ngOnInit(): Promise<void> {
this.accountInfoForm = this.formBuilder.group({ this.accountInfoForm = this.formBuilder.group({
name: ['', Validators.required], firstName: ['', Validators.required],
lastName: ['', Validators.required],
phoneNumber: ['', Validators.required], phoneNumber: ['', Validators.required],
age: ['', Validators.required], age: ['', Validators.required],
type: ['', Validators.required], type: ['', Validators.required],
@@ -90,36 +99,71 @@ export class AccountDetailsComponent implements OnInit {
location: ['', Validators.required], location: ['', Validators.required],
locationType: ['', Validators.required], locationType: ['', Validators.required],
}); });
this.route.paramMap.subscribe(async (params: Params) => { await this.blockSyncService.init();
this.accountAddress = add0x(params.get('id')); await this.tokenService.init();
this.bloxbergLink = await this.transactionService.init();
'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions'; await this.userService.init();
(await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe( await this.blockSyncService.blockSync(this.accountAddress);
async (res) => { this.userService.resetAccountsList();
if (res !== undefined) { (await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe(
this.account = res; async (res) => {
this.cdr.detectChanges(); if (res !== undefined) {
this.loggingService.sendInfoLevelMessage(this.account); this.account = res;
// this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first()) this.cdr.detectChanges();
// .subscribe(response => this.accountStatus = response); this.loggingService.sendInfoLevelMessage(this.account);
this.accountInfoForm.patchValue({ const fullName = this.account.vcard?.fn[0].value.split(' ');
name: this.account.vcard?.fn[0].value, this.accountInfoForm.patchValue({
phoneNumber: this.account.vcard?.tel[0].value, firstName: fullName[0],
age: this.account.age, lastName: fullName.slice(1).join(' '),
type: this.account.type, phoneNumber: this.account.vcard?.tel[0].value,
bio: this.account.products, age: this.account.age,
gender: this.account.gender, type: this.account.type,
businessCategory: this.account.category, bio: this.account.products,
userLocation: this.account.location.area_name, gender: this.account.gender,
location: this.account.location.area, businessCategory:
locationType: this.account.location.area_type, this.account.category ||
}); this.userService.getCategoryByProduct(this.account.products[0]),
} else { userLocation: this.account.location.area_name,
alert('Account not found!'); location:
} this.account.location.area ||
this.locationService
.getAreaNameByLocation(this.account.location.area_name)
.pipe(first())
.subscribe((response) => {
return response;
}),
locationType:
this.account.location.area_type ||
this.locationService
.getAreaTypeByArea(this.accountInfoFormStub.location.value)
.pipe(first())
.subscribe((response) => {
return response;
}),
});
this.userService
.getAccountStatus(this.account.vcard?.tel[0].value)
.pipe(first())
.subscribe((response) => (this.accountStatus = response.status));
} else {
alert('Account not found!');
} }
); }
this.blockSyncService.blockSync(this.accountAddress); );
this.userService.accountsSubject.subscribe((accounts) => {
this.userDataSource = new MatTableDataSource<any>(accounts);
this.userDataSource.paginator = this.userTablePaginator;
this.userDataSource.sort = this.userTableSort;
this.accounts = accounts;
this.cdr.detectChanges();
});
this.transactionService.transactionsSubject.subscribe((transactions) => {
this.transactionsDataSource = new MatTableDataSource<any>(transactions);
this.transactionsDataSource.paginator = this.transactionTablePaginator;
this.transactionsDataSource.sort = this.transactionTableSort;
this.transactions = transactions;
this.cdr.detectChanges();
}); });
this.userService this.userService
.getCategories() .getCategories()
@@ -147,22 +191,6 @@ export class AccountDetailsComponent implements OnInit {
.subscribe((res) => (this.genders = res)); .subscribe((res) => (this.genders = res));
} }
ngOnInit(): void {
this.userService.accountsSubject.subscribe((accounts) => {
this.userDataSource = new MatTableDataSource<any>(accounts);
this.userDataSource.paginator = this.userTablePaginator;
this.userDataSource.sort = this.userTableSort;
this.accounts = accounts;
});
this.transactionService.transactionsSubject.subscribe((transactions) => {
this.transactionsDataSource = new MatTableDataSource<any>(transactions);
this.transactionsDataSource.paginator = this.transactionTablePaginator;
this.transactionsDataSource.sort = this.transactionTableSort;
this.transactions = transactions;
});
}
doTransactionFilter(value: string): void { doTransactionFilter(value: string): void {
this.transactionsDataSource.filter = value.trim().toLocaleLowerCase(); this.transactionsDataSource.filter = value.trim().toLocaleLowerCase();
} }
@@ -192,7 +220,7 @@ export class AccountDetailsComponent implements OnInit {
} }
const accountKey = await this.userService.changeAccountInfo( const accountKey = await this.userService.changeAccountInfo(
this.accountAddress, this.accountAddress,
this.accountInfoFormStub.name.value, this.accountInfoFormStub.firstName.value + ' ' + this.accountInfoFormStub.lastName.value,
this.accountInfoFormStub.phoneNumber.value, this.accountInfoFormStub.phoneNumber.value,
this.accountInfoFormStub.age.value, this.accountInfoFormStub.age.value,
this.accountInfoFormStub.type.value, this.accountInfoFormStub.type.value,

View File

@@ -30,7 +30,8 @@ export class AccountSearchComponent implements OnInit {
private router: Router private router: Router
) {} ) {}
ngOnInit(): void { async ngOnInit(): Promise<void> {
await this.userService.init();
this.nameSearchForm = this.formBuilder.group({ this.nameSearchForm = this.formBuilder.group({
name: ['', Validators.required], name: ['', Validators.required],
}); });

View File

@@ -56,7 +56,7 @@
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell>
<mat-cell *matCellDef="let user"> {{user?.date_registered | date}} </mat-cell> <mat-cell *matCellDef="let user"> {{user?.date_registered | unixDate}} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="balance"> <ng-container matColumnDef="balance">

View File

@@ -32,28 +32,26 @@ export class AccountsComponent implements OnInit {
private userService: UserService, private userService: UserService,
private loggingService: LoggingService, private loggingService: LoggingService,
private router: Router private router: Router
) { ) {}
(async () => {
try {
// TODO it feels like this should be in the onInit handler
await this.userService.loadAccounts(100);
} catch (error) {
this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, { error });
}
})();
this.userService
.getAccountTypes()
.pipe(first())
.subscribe((res) => (this.accountTypes = res));
}
ngOnInit(): void { async ngOnInit(): Promise<void> {
await this.userService.init();
try {
// TODO it feels like this should be in the onInit handler
await this.userService.loadAccounts(100);
} catch (error) {
this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, { error });
}
this.userService.accountsSubject.subscribe((accounts) => { this.userService.accountsSubject.subscribe((accounts) => {
this.dataSource = new MatTableDataSource<any>(accounts); this.dataSource = new MatTableDataSource<any>(accounts);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.accounts = accounts; this.accounts = accounts;
}); });
this.userService
.getAccountTypes()
.pipe(first())
.subscribe((res) => (this.accountTypes = res));
} }
doFilter(value: string): void { doFilter(value: string): void {

View File

@@ -26,7 +26,8 @@ export class CreateAccountComponent implements OnInit {
private userService: UserService private userService: UserService
) {} ) {}
ngOnInit(): void { async ngOnInit(): Promise<void> {
await this.userService.init();
this.createForm = this.formBuilder.group({ this.createForm = this.formBuilder.group({
accountType: ['', Validators.required], accountType: ['', Validators.required],
idNumber: ['', Validators.required], idNumber: ['', Validators.required],

View File

@@ -30,7 +30,10 @@ export class AdminComponent implements OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor(private userService: UserService, private loggingService: LoggingService) { constructor(private userService: UserService, private loggingService: LoggingService) {}
async ngOnInit(): Promise<void> {
await this.userService.init();
this.userService.getActions(); this.userService.getActions();
this.userService.actionsSubject.subscribe((actions) => { this.userService.actionsSubject.subscribe((actions) => {
this.dataSource = new MatTableDataSource<any>(actions); this.dataSource = new MatTableDataSource<any>(actions);
@@ -40,8 +43,6 @@ export class AdminComponent implements OnInit {
}); });
} }
ngOnInit(): void {}
doFilter(value: string): void { doFilter(value: string): void {
this.dataSource.filter = value.trim().toLocaleLowerCase(); this.dataSource.filter = value.trim().toLocaleLowerCase();
} }

View File

@@ -23,9 +23,10 @@
SETTINGS SETTINGS
</mat-card-title> </mat-card-title>
<div class="card-body"> <div class="card-body">
<h4>Kobo Toolbox Credentials</h4> <h4>CICADA Admin Credentials</h4>
<span><strong>Username: </strong> admin_reserve </span><br> <span><strong>UserId: </strong> {{ userInfo?.userid }} </span><br>
<span><strong>Password: </strong> ******** </span> <span><strong>Username: </strong> {{ userInfo?.name }} </span><br>
<span><strong>Email: </strong> {{ userInfo?.email }} </span>
</div> </div>
<hr> <hr>
<div class="card-body"> <div class="card-body">

View File

@@ -17,19 +17,22 @@ export class SettingsComponent implements OnInit {
dataSource: MatTableDataSource<any>; dataSource: MatTableDataSource<any>;
displayedColumns: Array<string> = ['name', 'email', 'userId']; displayedColumns: Array<string> = ['name', 'email', 'userId'];
trustedUsers: Array<Staff>; trustedUsers: Array<Staff>;
userInfo: Staff;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
ngOnInit(): void { async ngOnInit(): Promise<void> {
const d = new Date(); await this.authService.init();
this.date = `${d.getDate()}/${d.getMonth()}/${d.getFullYear()}`; this.authService.trustedUsersSubject.subscribe((users) => {
this.trustedUsers = this.authService.getTrustedUsers(); this.dataSource = new MatTableDataSource<any>(users);
this.dataSource = new MatTableDataSource<any>(this.trustedUsers); this.dataSource.paginator = this.paginator;
this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort;
this.dataSource.sort = this.sort; this.trustedUsers = users;
});
this.userInfo = this.authService.getPrivateKeyInfo();
} }
doFilter(value: string): void { doFilter(value: string): void {

View File

@@ -1,60 +1,36 @@
<!-- Begin page --> <div *ngIf="token" class="mb-3 mt-1">
<div class="wrapper"> <div class="card text-center">
<app-sidebar></app-sidebar> <mat-card-title class="card-header">
<div class="row">
<!-- ============================================================== --> TOKEN DETAILS
<!-- Start Page Content here --> <button mat-raised-button type="button" class="btn btn-outline-secondary ml-auto mr-2" (click)="close()"> CLOSE </button>
<!-- ============================================================== --> </div>
</mat-card-title>
<div id="content"> <div class="card-body">
<app-topbar></app-topbar> <div>
<!-- Start Content--> <span><strong>Name:</strong> {{token?.name}}</span>
<div class="container-fluid text-center" appMenuSelection> </div>
<nav aria-label="breadcrumb"> <div>
<ol class="breadcrumb"> <span><strong>Symbol:</strong> {{token?.symbol}}</span>
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li> </div>
<li class="breadcrumb-item"><a routerLink="/tokens">Tokens</a></li> <div>
<li class="breadcrumb-item active" aria-current="page">{{token.name}}</li> <span><strong>Address:</strong> {{token?.address}}</span>
</ol> </div>
</nav> <div>
<div class="col-md-6 center-body"> <span><strong>Details:</strong> A community inclusive currency for trading among lower to middle income societies.</span>
<div class="card"> </div>
<mat-card-title class="card-header"> <div>
Token <span><strong>Supply:</strong> {{token?.supply | tokenRatio}}</span>
</mat-card-title> </div><br>
<div class="card-body"> <div>
<div> <h2>Reserve</h2>
<span><strong>Name:</strong> {{token.name}}</span> <div>
</div> <span><strong>Weight:</strong> {{token?.reserveRatio}}</span>
<div> </div>
<span><strong>Symbol:</strong> {{token.symbol}}</span> <div>
</div> <span><strong>Owner:</strong> {{token?.owner}}</span>
<div>
<span><strong>Address:</strong> {{token.address}}</span>
</div>
<div>
<span><strong>Details:</strong> A community inclusive currency for trading among lower to middle income societies.</span>
</div>
<div>
<span><strong>Supply:</strong> {{token.supply | tokenRatio}}</span>
</div><br>
<div>
<h2>Reserve</h2>
<div>
<span><strong>Weight:</strong> {{token.reserveRatio}}</span>
</div>
<div>
<span><strong>Owner:</strong> {{token.owner}}</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<app-footer appMenuSelection></app-footer>
</div> </div>
<!-- ============================================================== -->
<!-- End Page content -->
<!-- ============================================================== -->
</div> </div>

View File

@@ -1,8 +1,12 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import {
import { ActivatedRoute, Params } from '@angular/router'; ChangeDetectionStrategy,
import { TokenService } from '@app/_services'; Component,
import { first } from 'rxjs/operators'; EventEmitter,
import { Token } from '../../../_models'; Input,
OnInit,
Output,
} from '@angular/core';
import { Token } from '@app/_models';
@Component({ @Component({
selector: 'app-token-details', selector: 'app-token-details',
@@ -11,18 +15,16 @@ import { Token } from '../../../_models';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TokenDetailsComponent implements OnInit { export class TokenDetailsComponent implements OnInit {
token: Token; @Input() token: Token;
constructor(private route: ActivatedRoute, private tokenService: TokenService) { @Output() closeWindow: EventEmitter<any> = new EventEmitter<any>();
this.route.paramMap.subscribe((params: Params) => {
this.tokenService constructor() {}
.getTokenBySymbol(params.get('id'))
.pipe(first())
.subscribe((res) => {
this.token = res;
});
});
}
ngOnInit(): void {} ngOnInit(): void {}
close(): void {
this.token = null;
this.closeWindow.emit(this.token);
}
} }

View File

@@ -24,6 +24,9 @@
</div> </div>
</mat-card-title> </mat-card-title>
<div class="card-body"> <div class="card-body">
<app-token-details [token]="token" (closeWindow)="token = $event"></app-token-details>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label> Filter </mat-label> <mat-label> Filter </mat-label>
<input matInput type="text" (keyup)="doFilter($event.target.value)" placeholder="Filter"> <input matInput type="text" (keyup)="doFilter($event.target.value)" placeholder="Filter">

View File

@@ -5,8 +5,7 @@ import { LoggingService, TokenService } from '@app/_services';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { exportCsv } from '@app/_helpers'; import { exportCsv } from '@app/_helpers';
import { TokenRegistry } from '../../_eth'; import { Token } from '@app/_models';
import { Token } from '../../_models';
@Component({ @Component({
selector: 'app-tokens', selector: 'app-tokens',
@@ -19,7 +18,8 @@ export class TokensComponent implements OnInit {
columnsToDisplay: Array<string> = ['name', 'symbol', 'address', 'supply']; columnsToDisplay: Array<string> = ['name', 'symbol', 'address', 'supply'];
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
tokens: Array<Promise<string>>; tokens: Array<Token>;
token: Token;
constructor( constructor(
private tokenService: TokenService, private tokenService: TokenService,
@@ -28,22 +28,25 @@ export class TokensComponent implements OnInit {
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.tokenService.LoadEvent.subscribe(async () => { await this.tokenService.init();
this.tokens = await this.tokenService.getTokens(); this.tokenService.onload = async (status: boolean): Promise<void> => {
await this.tokenService.getTokens();
};
this.tokenService.tokensSubject.subscribe((tokens) => {
this.loggingService.sendInfoLevelMessage(tokens);
this.dataSource = new MatTableDataSource(tokens);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.tokens = tokens;
}); });
this.tokens = await this.tokenService.getTokens();
this.loggingService.sendInfoLevelMessage(this.tokens);
this.dataSource = new MatTableDataSource(this.tokens);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
} }
doFilter(value: string): void { doFilter(value: string): void {
this.dataSource.filter = value.trim().toLocaleLowerCase(); this.dataSource.filter = value.trim().toLocaleLowerCase();
} }
async viewToken(token): Promise<void> { viewToken(token): void {
await this.router.navigateByUrl(`/tokens/${token.symbol}`); this.token = token;
} }
downloadCsv(): void { downloadCsv(): void {

View File

@@ -1,9 +1,9 @@
<div *ngIf="transaction | async" class="mb-3 mt-1"> <div *ngIf="transaction" class="mb-3 mt-1">
<div class="card text-center"> <div class="card text-center">
<mat-card-title class="card-header"> <mat-card-title class="card-header">
<div class="row"> <div class="row">
TRANSACTION DETAILS TRANSACTION DETAILS
<button mat-raised-button type="button" class="btn btn-outline-secondary ml-auto mr-2" (click)="transaction = null"> CLOSE </button> <button mat-raised-button type="button" class="btn btn-outline-secondary ml-auto mr-2" (click)="close()"> CLOSE </button>
</div> </div>
</mat-card-title> </mat-card-title>
<div *ngIf="transaction.type == 'transaction'" class="card-body"> <div *ngIf="transaction.type == 'transaction'" class="card-body">
@@ -66,7 +66,7 @@
<span>Success: {{transaction.tx.success}}</span> <span>Success: {{transaction.tx.success}}</span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Timestamp: {{transaction.tx.timestamp | date}}</span> <span>Timestamp: {{transaction.tx.timestamp | unixDate}}</span>
</li> </li>
</ul><br> </ul><br>
<div class="mb-3"> <div class="mb-3">

View File

@@ -1,4 +1,11 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TransactionService } from '@app/_services'; import { TransactionService } from '@app/_services';
import { copyToClipboard } from '@app/_helpers'; import { copyToClipboard } from '@app/_helpers';
@@ -13,6 +20,9 @@ import { strip0x } from '@src/assets/js/ethtx/dist/hex';
}) })
export class TransactionDetailsComponent implements OnInit { export class TransactionDetailsComponent implements OnInit {
@Input() transaction; @Input() transaction;
@Output() closeWindow: EventEmitter<any> = new EventEmitter<any>();
senderBloxbergLink: string; senderBloxbergLink: string;
recipientBloxbergLink: string; recipientBloxbergLink: string;
traderBloxbergLink: string; traderBloxbergLink: string;
@@ -23,7 +33,8 @@ export class TransactionDetailsComponent implements OnInit {
private snackBar: MatSnackBar private snackBar: MatSnackBar
) {} ) {}
ngOnInit(): void { async ngOnInit(): Promise<void> {
await this.transactionService.init();
if (this.transaction?.type === 'conversion') { if (this.transaction?.type === 'conversion') {
this.traderBloxbergLink = this.traderBloxbergLink =
'https://blockexplorer.bloxberg.org/address/' + this.transaction?.trader + '/transactions'; 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.trader + '/transactions';
@@ -61,4 +72,9 @@ export class TransactionDetailsComponent implements OnInit {
this.snackBar.open(address + ' copied successfully!', 'Close', { duration: 3000 }); this.snackBar.open(address + ' copied successfully!', 'Close', { duration: 3000 });
} }
} }
close(): void {
this.transaction = null;
this.closeWindow.emit(this.transaction);
}
} }

View File

@@ -22,7 +22,7 @@
</mat-card-title> </mat-card-title>
<div class="card-body"> <div class="card-body">
<app-transaction-details [transaction]="transaction"></app-transaction-details> <app-transaction-details [transaction]="transaction" (closeWindow)="transaction = $event"></app-transaction-details>
<div class="row card-header"> <div class="row card-header">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -48,12 +48,12 @@
<ng-container matColumnDef="sender"> <ng-container matColumnDef="sender">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Sender </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Sender </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.sender?.vcard.fn[0].value}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.sender?.vcard.fn[0].value || transaction.from}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="recipient"> <ng-container matColumnDef="recipient">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Recipient </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Recipient </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.recipient?.vcard.fn[0].value}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.recipient?.vcard.fn[0].value || transaction.to}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
@@ -66,7 +66,7 @@
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Created </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Created </th>
<td mat-cell *matCellDef="let transaction"> {{transaction?.tx.timestamp | date}} </td> <td mat-cell *matCellDef="let transaction"> {{transaction?.tx.timestamp | unixDate}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">

View File

@@ -36,17 +36,19 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
private blockSyncService: BlockSyncService, private blockSyncService: BlockSyncService,
private transactionService: TransactionService, private transactionService: TransactionService,
private userService: UserService private userService: UserService
) { ) {}
this.blockSyncService.blockSync();
}
ngOnInit(): void { async ngOnInit(): Promise<void> {
this.transactionService.transactionsSubject.subscribe((transactions) => { this.transactionService.transactionsSubject.subscribe((transactions) => {
this.transactionDataSource = new MatTableDataSource<any>(transactions); this.transactionDataSource = new MatTableDataSource<any>(transactions);
this.transactionDataSource.paginator = this.paginator; this.transactionDataSource.paginator = this.paginator;
this.transactionDataSource.sort = this.sort; this.transactionDataSource.sort = this.sort;
this.transactions = transactions; this.transactions = transactions;
}); });
await this.blockSyncService.init();
await this.transactionService.init();
await this.userService.init();
await this.blockSyncService.blockSync();
this.userService this.userService
.getTransactionTypes() .getTransactionTypes()
.pipe(first()) .pipe(first())

View File

@@ -0,0 +1,8 @@
import { UnixDatePipe } from './unix-date.pipe';
describe('UnixDatePipe', () => {
it('create an instance', () => {
const pipe = new UnixDatePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'unixDate',
})
export class UnixDatePipe implements PipeTransform {
transform(timestamp: number, ...args: unknown[]): any {
return new Date(timestamp * 1000).toLocaleDateString('en-GB');
}
}

View File

@@ -1,5 +1,8 @@
<!-- Footer Start --> <!-- Footer Start -->
<footer class="footer"> <footer class="footer">
2020 © Grassroots Economics <a target="blank" title="GPL-3" href="https://www.gnu.org/licenses/gpl-3.0.en.html"> Copyleft </a> 🄯.
{{ currentYear }}
<a target="blank" title="Gitlab@GrassrootsEconomics" href="https://gitlab.com/grassrootseconomics"><u> Grassroots Economics </u></a>
</footer> </footer>
<!-- end Footer --> <!-- end Footer -->

View File

@@ -7,6 +7,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FooterComponent implements OnInit { export class FooterComponent implements OnInit {
currentYear = new Date().getFullYear();
constructor() {} constructor() {}
ngOnInit(): void {} ngOnInit(): void {}

View File

@@ -1,4 +1,4 @@
<nav class="navbar navbar-dark bg-dark"> <nav class="navbar navbar-dark background-dark">
<h1 class="navbar-brand"> <h1 class="navbar-brand">
<div *ngIf="noInternetConnection; then offlineBlock else onlineBlock"></div> <div *ngIf="noInternetConnection; then offlineBlock else onlineBlock"></div>
<ng-template #offlineBlock> <ng-template #offlineBlock>

View File

@@ -12,6 +12,7 @@ import { ErrorDialogComponent } from '@app/shared/error-dialog/error-dialog.comp
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { SafePipe } from '@app/shared/_pipes/safe.pipe'; import { SafePipe } from '@app/shared/_pipes/safe.pipe';
import { NetworkStatusComponent } from './network-status/network-status.component'; import { NetworkStatusComponent } from './network-status/network-status.component';
import { UnixDatePipe } from './_pipes/unix-date.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -24,6 +25,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
ErrorDialogComponent, ErrorDialogComponent,
SafePipe, SafePipe,
NetworkStatusComponent, NetworkStatusComponent,
UnixDatePipe,
], ],
exports: [ exports: [
TopbarComponent, TopbarComponent,
@@ -33,6 +35,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
TokenRatioPipe, TokenRatioPipe,
SafePipe, SafePipe,
NetworkStatusComponent, NetworkStatusComponent,
UnixDatePipe,
], ],
imports: [CommonModule, RouterModule, MatIconModule, MatDialogModule], imports: [CommonModule, RouterModule, MatIconModule, MatDialogModule],
}) })

View File

@@ -1,5 +1,6 @@
<!-- ========== Left Sidebar Start ========== --> <!-- ========== Left Sidebar Start ========== -->
<div id="sidebar"> <div id="sidebar">
<app-network-status></app-network-status>
<nav> <nav>
<div class="sidebar-header"> <div class="sidebar-header">

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -42,7 +42,7 @@ Driver.prototype.sync = function (n) {
const processor = async (b, t) => { const processor = async (b, t) => {
return await self.process(b, t); return await self.process(b, t);
}; };
self.syncer(self, self.lo, self.hi, self.filters[0], self.filters[1], countGetter, processor); self.syncer(self.lo, self.hi, self.filters[0], self.filters[1], countGetter, processor);
}; };
Driver.prototype.process = function (b, t) { Driver.prototype.process = function (b, t) {

View File

@@ -2,7 +2,7 @@ import { hobaResult, hobaToSign } from '@src/assets/js/hoba.js';
const alg = '969'; const alg = '969';
export async function signChallenge(challenge, realm, origin, keyStore, password) { export async function signChallenge(challenge, realm, origin, keyStore) {
const fingerprint = keyStore.getFingerprint(); const fingerprint = keyStore.getFingerprint();
const nonce_array = new Uint8Array(32); const nonce_array = new Uint8Array(32);
crypto.getRandomValues(nonce_array); crypto.getRandomValues(nonce_array);
@@ -14,7 +14,7 @@ export async function signChallenge(challenge, realm, origin, keyStore, password
const a_challenge = btoa(challenge); const a_challenge = btoa(challenge);
const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg); const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg);
const signature = await keyStore.sign(message, password); const signature = await keyStore.sign(message);
const a_signature = btoa(signature); const a_signature = btoa(signature);
const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature); const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature);

View File

@@ -10,7 +10,7 @@ export const environment = {
publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/',
cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net',
web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',
cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net', cicUssdUrl: 'https://user.dev.grassrootseconomics.net',
registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161', registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161',
trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C', trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C',
}; };

View File

@@ -10,7 +10,7 @@ export const environment = {
publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/',
cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net',
web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',
cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net', cicUssdUrl: 'https://user.dev.grassrootseconomics.net',
registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161', registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161',
trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C', trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C',
}; };

View File

@@ -10,7 +10,7 @@ export const environment = {
publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/', publicKeysUrl: 'https://dev.grassrootseconomics.net/.well-known/publickeys/',
cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net',
web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',
cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net', cicUssdUrl: 'https://user.dev.grassrootseconomics.net',
registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161', registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161',
trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C', trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C',
}; };

View File

@@ -3,10 +3,11 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>CICADA</title> <title>CICADA</title>
<base href="/"> <base href="./">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta content="A fully featured admin client for managing users and transactions in the CIC network." name="description"/> <meta content="A fully featured admin client for managing users and transactions in the CIC network." name="description"/>
<meta content="Spencer Ofwiti" name="author"/> <meta content="Spencer Ofwiti" name="author"/>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<link rel="icon" type="image/x-icon" href="assets/icons/manifest-icon-512.png"> <link rel="icon" type="image/x-icon" href="assets/icons/manifest-icon-512.png">
<link rel="apple-touch-icon" href="assets/icons/apple-icon-180.png"> <link rel="apple-touch-icon" href="assets/icons/apple-icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@@ -18,8 +18,8 @@ body {
background: #fafafa; background: #fafafa;
} }
.bg-dark { .background-dark {
background: #313a46; background: #313a46 !important;
} }
p { p {
@@ -65,12 +65,6 @@ footer.footer { color: black; }
left: -9999px; left: -9999px;
} }
.center-items {
display: flex;
justify-content: center;
align-items: center;
}
#sidebar { #sidebar {
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
@@ -213,11 +207,6 @@ a[data-toggle="collapse"] { position: relative; }
.mat-column-select { overflow: initial; } .mat-column-select { overflow: initial; }
#passwordLoginButton {
height: 3.0rem;
padding-top: 0.5rem;
}
button { height: 2.5rem; } button { height: 2.5rem; }
.badge-pill { width: 5rem; } .badge-pill { width: 5rem; }

View File

@@ -14,9 +14,11 @@
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true, "importHelpers": true,
"target": "es2015", "target": "es5",
"module": "es2020", "module": "es2020",
"typeRoots": ["node_modules/@types"],
"lib": [ "lib": [
"es2018", "es2018",
"dom" "dom"