diff --git a/packages/bitcore-node/src/models/block.ts b/packages/bitcore-node/src/models/block.ts index a599936749a..47d170eee05 100644 --- a/packages/bitcore-node/src/models/block.ts +++ b/packages/bitcore-node/src/models/block.ts @@ -5,25 +5,9 @@ import { LoggifyClass } from '../decorators/Loggify'; import { Bitcoin } from '../types/namespaces/Bitcoin'; import { BaseModel, MongoBound } from './base'; import logger from '../logger'; +import { IBlock } from '../types/Block'; -export type IBlock = { - chain: string; - network: string; - height: number; - hash: string; - version: number; - merkleRoot: string; - time: Date; - timeNormalized: Date; - nonce: number; - previousBlockHash: string; - nextBlockHash: string; - transactionCount: number; - size: number; - bits: number; - reward: number; - processed: boolean; -}; +export { IBlock }; @LoggifyClass export class Block extends BaseModel { diff --git a/packages/bitcore-node/src/services/storage.ts b/packages/bitcore-node/src/services/storage.ts index e8669b364e4..0e68c8bbc8d 100644 --- a/packages/bitcore-node/src/services/storage.ts +++ b/packages/bitcore-node/src/services/storage.ts @@ -8,14 +8,10 @@ import { ObjectID } from 'bson'; import { MongoClient, Db, Cursor } from 'mongodb'; import { MongoBound } from '../models/base'; import '../models'; +import { StreamingFindOptions } from '../types/Query'; + +export { StreamingFindOptions }; -export type StreamingFindOptions = Partial<{ - paging: keyof T; - since: T[keyof T]; - sort: any; - direction: 1 | -1; - limit: number; -}>; @LoggifyClass export class StorageService { client?: MongoClient; @@ -88,8 +84,8 @@ export class StorageService { typecastedValue = new Date(oldValue) as any; break; } - // TODO: Micah check this! - } else if (modelKey == "_id") { + // TODO: Micah check this! + } else if (modelKey == '_id') { typecastedValue = new ObjectID(oldValue) as any; } } @@ -123,17 +119,13 @@ export class StorageService { } getFindOptions(model: TransformableModel, originalOptions: StreamingFindOptions) { let options: StreamingFindOptions = {}; - let query: any = {}, since: any; - if ( originalOptions.paging && - this.validPagingProperty(model, originalOptions.paging) - ) { - - + let query: any = {}, + since: any; + if (originalOptions.paging && this.validPagingProperty(model, originalOptions.paging)) { if (originalOptions.since !== undefined) { since = this.typecastForDb(model, originalOptions.paging, originalOptions.since); } - if (originalOptions.direction && Number(originalOptions.direction) === 1) { if (since) { query[originalOptions.paging] = { $gt: since }; diff --git a/packages/bitcore-node/src/types/Block.ts b/packages/bitcore-node/src/types/Block.ts new file mode 100644 index 00000000000..551c866caa2 --- /dev/null +++ b/packages/bitcore-node/src/types/Block.ts @@ -0,0 +1,18 @@ +export type IBlock = { + chain: string; + network: string; + height: number; + hash: string; + version: number; + merkleRoot: string; + time: Date; + timeNormalized: Date; + nonce: number; + previousBlockHash: string; + nextBlockHash: string; + transactionCount: number; + size: number; + bits: number; + reward: number; + processed: boolean; +}; diff --git a/packages/bitcore-node/src/types/Query.ts b/packages/bitcore-node/src/types/Query.ts new file mode 100644 index 00000000000..ccc3f54cdbd --- /dev/null +++ b/packages/bitcore-node/src/types/Query.ts @@ -0,0 +1,12 @@ +const enum Direction { + ascending = 1, + descending = -1 +} + +export type StreamingFindOptions = Partial<{ + paging: keyof T; + since: T[keyof T]; + sort: any; + direction: Direction; + limit: number; +}>; diff --git a/packages/insight/angular.json b/packages/insight/angular.json index 80cbc3fc6c7..1f502349fc6 100644 --- a/packages/insight/angular.json +++ b/packages/insight/angular.json @@ -9,7 +9,11 @@ "sourceRoot": "src", "projectType": "application", "prefix": "app", - "schematics": {}, + "schematics": { + "@schematics/angular:component": { + "styleext": "scss" + } + }, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", diff --git a/packages/insight/package-lock.json b/packages/insight/package-lock.json index 572c0454152..15045e33970 100644 --- a/packages/insight/package-lock.json +++ b/packages/insight/package-lock.json @@ -1294,6 +1294,14 @@ "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.3.0", "uri-js": "^3.0.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + } } }, "ajv-errors": { @@ -3984,10 +3992,9 @@ "dev": true }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -5028,6 +5035,14 @@ "fast-deep-equal": "^1.0.0", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.3.0" + }, + "dependencies": { + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + } } } } @@ -6078,6 +6093,14 @@ "fast-deep-equal": "^1.0.0", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.3.0" + }, + "dependencies": { + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + } } }, "schema-utils": { @@ -7291,6 +7314,15 @@ "fast-deep-equal": "^1.0.0", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.3.0" + }, + "dependencies": { + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true, + "optional": true + } } }, "ansi-styles": { diff --git a/packages/insight/package.json b/packages/insight/package.json index 3ff7713e7f0..6ce07704d8c 100644 --- a/packages/insight/package.json +++ b/packages/insight/package.json @@ -53,6 +53,7 @@ "@ionic/angular": "^4.0.0-beta.12", "backoff-rxjs": "0.0.10", "core-js": "^2.5.3", + "fast-deep-equal": "^2.0.1", "ngx-logger": "^3.3.2", "rxjs": "~6.3.3", "zone.js": "^0.8.26" diff --git a/packages/insight/src/app/app.component.html b/packages/insight/src/app/app.component.html index ea28e809a67..6fb0efb5e30 100644 --- a/packages/insight/src/app/app.component.html +++ b/packages/insight/src/app/app.component.html @@ -1,18 +1,25 @@ - - - - - - - - {{ p.title }} - - - - - - - - - \ No newline at end of file + + + + + + + {{ p.title }} + + + + + + + + diff --git a/packages/insight/src/app/app.component.scss b/packages/insight/src/app/app.component.scss new file mode 100644 index 00000000000..a7faad2b7d9 --- /dev/null +++ b/packages/insight/src/app/app.component.scss @@ -0,0 +1,5 @@ +.dark-theme { + --ion-background-color: #3d3d4d; + --ion-item-background-color-active: lighten(--ion-background-color, 15%); + --ion-text-color: rgba(255, 255, 255, 0.9); +} diff --git a/packages/insight/src/app/app.component.ts b/packages/insight/src/app/app.component.ts index d2033f19e47..30befae96d1 100644 --- a/packages/insight/src/app/app.component.ts +++ b/packages/insight/src/app/app.component.ts @@ -6,7 +6,8 @@ import { Platform } from '@ionic/angular'; @Component({ selector: 'app-root', - templateUrl: 'app.component.html' + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'] }) export class AppComponent { public appPages = [ diff --git a/packages/insight/src/app/blocks/block-list/block-list.component.html b/packages/insight/src/app/blocks/block-list/block-list.component.html index 358de8526d3..ea994cc5407 100644 --- a/packages/insight/src/app/blocks/block-list/block-list.component.html +++ b/packages/insight/src/app/blocks/block-list/block-list.component.html @@ -1,3 +1,6 @@ -

- block-list works! -

+ diff --git a/packages/insight/src/app/blocks/block-list/block-list.component.scss b/packages/insight/src/app/blocks/block-list/block-list.component.scss index e69de29bb2d..3cfb953ebaa 100644 --- a/packages/insight/src/app/blocks/block-list/block-list.component.scss +++ b/packages/insight/src/app/blocks/block-list/block-list.component.scss @@ -0,0 +1,3 @@ +app-block { + cursor: pointer; +} diff --git a/packages/insight/src/app/blocks/block-list/block-list.component.ts b/packages/insight/src/app/blocks/block-list/block-list.component.ts index 612480ec1dd..68d1578ba01 100644 --- a/packages/insight/src/app/blocks/block-list/block-list.component.ts +++ b/packages/insight/src/app/blocks/block-list/block-list.component.ts @@ -1,29 +1,42 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit +} from '@angular/core'; +import * as equal from 'fast-deep-equal'; +import { combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; import { ApiService } from '../../services/api/api.service'; -import { IBlock } from '../../types/bitcore-node'; -import { Network, Ticker } from '../../types/configuration'; +import { IBlock, StreamingFindOptions } from '../../types/bitcore-node'; +import { Chain } from '../../types/configuration'; @Component({ selector: 'app-block-list', templateUrl: './block-list.component.html', - styleUrls: ['./block-list.component.scss'] + styleUrls: ['./block-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class BlockListComponent { +export class BlockListComponent implements OnInit { @Input() - public ticker: Ticker; + chain: Observable; @Input() - public network: Network; + query: Observable>; @Input() - public blocks: number; - @Input() - public paging: keyof IBlock = 'height'; - - public blockStream = this.apiService.streamBlocks( - { ticker: this.ticker, network: this.network }, - { - paging: this.paging - } - ); + displayValueIn = 'BCH'; + block$: Observable; constructor(private apiService: ApiService) {} + ngOnInit() { + this.block$ = combineLatest(this.chain, this.query).pipe( + distinctUntilChanged((x, y) => equal(x, y)), + switchMap(([chain, query]) => this.apiService.streamBlocks(chain, query)), + distinctUntilChanged((x, y) => equal(x, y)) + ); + } + + goToBlock(block: IBlock) { + // tslint:disable-next-line:no-console + console.log('TODO: navigate to block', block.hash); + } } diff --git a/packages/insight/src/app/blocks/block/block.component.html b/packages/insight/src/app/blocks/block/block.component.html new file mode 100644 index 00000000000..a449b844832 --- /dev/null +++ b/packages/insight/src/app/blocks/block/block.component.html @@ -0,0 +1,59 @@ + + +
+
+
+
+ {{block.height}} +
+ + Tip + +
+ + +
+
+
+
Block Hash
+
{{block.hash}}
+
+
+ Transaction Count + + {{block.transactionCount}} + +
+
+ Reward + + + +
+
+
+ [show details] +
+
diff --git a/packages/insight/src/app/blocks/block/block.component.scss b/packages/insight/src/app/blocks/block/block.component.scss new file mode 100644 index 00000000000..8a1600a2e97 --- /dev/null +++ b/packages/insight/src/app/blocks/block/block.component.scss @@ -0,0 +1,72 @@ +.card { + border-radius: 3px; + max-width: 800px; + margin: 1em auto; + background-color: #fff; + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.08); +} +.section { + padding: 1em; + & + .section { + border-top: 1px solid #f1f2f5; + } +} + +.header, +.identifiers { + display: flex; + align-items: center; +} + +.identifiers { + width: 150px; +} + +.height { + font-size: 1.3em; + color: var(--ion-color-primary); +} + +.tip-tag { + text-transform: uppercase; + color: #fff; + background-color: #6eb542; + border-radius: 3px; + font-size: 0.7em; + display: inline-block; + padding: 0.1em 0.5em; + margin-left: 0.5em; +} + +.time { + flex-grow: 1; + text-align: right; +} + +.time, +.value { + color: #aaa; +} + +.key, +.property { + margin-bottom: 0.3em; +} + +.time, +.key, +.value { + font-size: 0.9em; +} + +.key { + margin-right: 0.3em; +} + +.link { + color: var(--ion-color-primary); +} + +.reward { + display: inline; +} diff --git a/packages/insight/src/app/blocks/block/block.component.spec.ts b/packages/insight/src/app/blocks/block/block.component.spec.ts new file mode 100644 index 00000000000..cdd4437197d --- /dev/null +++ b/packages/insight/src/app/blocks/block/block.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BlockComponent } from './block.component'; + +describe('BlockComponent', () => { + let component: BlockComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BlockComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BlockComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/insight/src/app/blocks/block/block.component.ts b/packages/insight/src/app/blocks/block/block.component.ts new file mode 100644 index 00000000000..7c691e0f3bf --- /dev/null +++ b/packages/insight/src/app/blocks/block/block.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; +import { IBlock } from '../../types/bitcore-node'; + +@Component({ + selector: 'app-block', + templateUrl: './block.component.html', + styleUrls: ['./block.component.scss'] +}) +export class BlockComponent { + @Input() + block: IBlock; + + /** + * The unit in which to display value – can be either a valid denomination for + * this chain or an alternative currency in which to estimate value. + */ + @Input() + displayValueIn = 'BCH'; + + @Input() + summary = true; + + listTransactionsInBlock(block: IBlock) { + // tslint:disable-next-line:no-console + console.log('TODO: jump to transaction listing for block:', block.hash); + } +} diff --git a/packages/insight/src/app/blocks/blocks.module.ts b/packages/insight/src/app/blocks/blocks.module.ts index 80300c9881a..5ee9d745f62 100644 --- a/packages/insight/src/app/blocks/blocks.module.ts +++ b/packages/insight/src/app/blocks/blocks.module.ts @@ -4,7 +4,9 @@ import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { IonicModule } from '@ionic/angular'; +import { SharedModule } from '../shared/shared.module'; import { BlockListComponent } from './block-list/block-list.component'; +import { BlockComponent } from './block/block.component'; import { BlocksPage } from './blocks.page'; @NgModule({ @@ -17,8 +19,9 @@ import { BlocksPage } from './blocks.page'; path: '', component: BlocksPage } - ]) + ]), + SharedModule ], - declarations: [BlocksPage, BlockListComponent] + declarations: [BlocksPage, BlockListComponent, BlockComponent] }) export class BlocksPageModule {} diff --git a/packages/insight/src/app/blocks/blocks.page.html b/packages/insight/src/app/blocks/blocks.page.html index 0201b2a0dd0..b0ae85a823b 100644 --- a/packages/insight/src/app/blocks/blocks.page.html +++ b/packages/insight/src/app/blocks/blocks.page.html @@ -1,14 +1,15 @@ - - - - - - Blocks - - + + + + + Blocks + - - \ No newline at end of file + + diff --git a/packages/insight/src/app/blocks/blocks.page.ts b/packages/insight/src/app/blocks/blocks.page.ts index dbf8152d122..586272c755b 100644 --- a/packages/insight/src/app/blocks/blocks.page.ts +++ b/packages/insight/src/app/blocks/blocks.page.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from '@angular/core'; +import { BehaviorSubject, interval } from 'rxjs'; +import { take } from 'rxjs/operators'; @Component({ selector: 'app-blocks', @@ -6,7 +8,23 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['blocks.page.scss'] }) export class BlocksPage implements OnInit { - constructor() {} + chain$ = new BehaviorSubject({ ticker: 'BTC', network: 'mainnet' }); + query$ = new BehaviorSubject({}); - ngOnInit() {} + constructor() { + this.chain$.subscribe(value => + // tslint:disable-next-line:no-console + console.log(`Chain value: ${JSON.stringify(value)}`) + ); + } + + ngOnInit() { + interval(1 * 1000) + .pipe(take(1)) + .subscribe(() => { + // tslint:disable-next-line:no-console + console.log('Simulating chain switch to BCH:'); + this.chain$.next({ ticker: 'BCH', network: 'mainnet' }); + }); + } } diff --git a/packages/insight/src/app/services/api/api.service.ts b/packages/insight/src/app/services/api/api.service.ts index 399b556fdac..900970a2c44 100644 --- a/packages/insight/src/app/services/api/api.service.ts +++ b/packages/insight/src/app/services/api/api.service.ts @@ -1,13 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { retryBackoff } from 'backoff-rxjs'; -import { BehaviorSubject, interval } from 'rxjs'; +import { BehaviorSubject, timer } from 'rxjs'; import { finalize, switchMap } from 'rxjs/operators'; import { IBlock, StreamingFindOptions } from '../../types/bitcore-node'; import { Chain } from '../../types/configuration'; import { ConfigService } from '../config/config.service'; -const streamPollingInterval = interval(15 * 1000); +// TODO: expose this setting as a BehaviorSubject, tone it down when the window isn't active? +const streamPollingPeriod = timer(0, 15 * 1000); /** * Convert any object into an object with only elements of type `string`. @@ -27,11 +28,13 @@ const stringifyForQuery = (object: any) => export class ApiService { constructor(private config: ConfigService, private http: HttpClient) {} + private _bitcoreAvailable = new BehaviorSubject(true); + // TODO: service down notification (if NetworkService is up but the service is unreachable) /** * Emits when availability of the service appears to change, e.g. when an API * call doesn't return, or when a retry is successful. */ - serviceAvailable = new BehaviorSubject(true); + public bitcoreAvailable = this._bitcoreAvailable.asObservable(); /** * Everything before the API route itself, without the trailing `/`. @@ -47,20 +50,25 @@ export class ApiService { }); streamBlocks = (chain: Chain, params: StreamingFindOptions) => - streamPollingInterval.pipe( + streamPollingPeriod.pipe( switchMap(() => this.getBlocks(chain, params)), retryBackoff({ initialInterval: 5 * 1000, shouldRetry: error => { // TODO: retry only if ERR_CONNECTION_REFUSED or 5xx error code if (!!error) { - this.serviceAvailable.next(false); + this._bitcoreAvailable.next(false); return true; } } }), finalize(() => { - this.serviceAvailable.next(true); + if (this._bitcoreAvailable.getValue() !== true) { + this._bitcoreAvailable.next(true); + } }) ); + + // TODO: rates API: + // consumers subscribe to an observable – only if a consumer is listening, poll this.config.ratesApi for the rates object. If everyone unsubscribes, polling should stop, and the last rates object thrown away (we don't want to use old rates on a resubscription). If someone subscribes again, start from scratch. } diff --git a/packages/insight/src/app/services/config/config.service.ts b/packages/insight/src/app/services/config/config.service.ts index 8b1e7ab3af9..7838d7bd47e 100644 --- a/packages/insight/src/app/services/config/config.service.ts +++ b/packages/insight/src/app/services/config/config.service.ts @@ -15,6 +15,7 @@ export class ConfigService { environment.expectedNetworks ); public apiPrefix = new BehaviorSubject(environment.apiPrefix); + public ratesApi = new BehaviorSubject(environment.ratesApi); constructor(private logger: NGXLogger) { this.logger.debug( `ConfigService initialized in ${ diff --git a/packages/insight/src/app/services/network/network.service.spec.ts b/packages/insight/src/app/services/network/network.service.spec.ts new file mode 100644 index 00000000000..097db559e86 --- /dev/null +++ b/packages/insight/src/app/services/network/network.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { NetworkService } from './network.service'; + +describe('NetworkService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: NetworkService = TestBed.get(NetworkService); + expect(service).toBeTruthy(); + }); +}); diff --git a/packages/insight/src/app/services/network/network.service.ts b/packages/insight/src/app/services/network/network.service.ts new file mode 100644 index 00000000000..3904f0d1348 --- /dev/null +++ b/packages/insight/src/app/services/network/network.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, fromEvent } from 'rxjs'; + +// TODO: network connected/disconnected notification +@Injectable({ + providedIn: 'root' +}) +export class NetworkService { + private _isOnline = new BehaviorSubject(navigator.onLine); + public isOnline = this._isOnline.asObservable(); + + constructor() { + const connect = fromEvent(window, 'online'); + const disconnect = fromEvent(window, 'offline'); + + connect.subscribe(() => this._isOnline.next(true)); + disconnect.subscribe(() => this._isOnline.next(false)); + } +} diff --git a/packages/insight/src/app/shared/currency-value/currency-value.component.html b/packages/insight/src/app/shared/currency-value/currency-value.component.html new file mode 100644 index 00000000000..4a9917e8051 --- /dev/null +++ b/packages/insight/src/app/shared/currency-value/currency-value.component.html @@ -0,0 +1,3 @@ + + currency-value: inAtomicUnits: {{inAtomicUnits}}, ticker: {{ticker}}, displayAs: {{displayAs}} + diff --git a/packages/insight/src/app/shared/currency-value/currency-value.component.scss b/packages/insight/src/app/shared/currency-value/currency-value.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/insight/src/app/shared/currency-value/currency-value.component.spec.ts b/packages/insight/src/app/shared/currency-value/currency-value.component.spec.ts new file mode 100644 index 00000000000..84c278bcfc9 --- /dev/null +++ b/packages/insight/src/app/shared/currency-value/currency-value.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CurrencyValueComponent } from './currency-value.component'; + +describe('CurrencyUnitsComponent', () => { + let component: CurrencyValueComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CurrencyValueComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CurrencyValueComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/insight/src/app/shared/currency-value/currency-value.component.ts b/packages/insight/src/app/shared/currency-value/currency-value.component.ts new file mode 100644 index 00000000000..ce677756708 --- /dev/null +++ b/packages/insight/src/app/shared/currency-value/currency-value.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { ApiService } from '../../services/api/api.service'; + +@Component({ + selector: 'app-currency-value', + templateUrl: './currency-value.component.html', + styleUrls: ['./currency-value.component.scss'] +}) +export class CurrencyValueComponent implements OnChanges { + /** + * The currency value in the smallest unit accounted for by Insight. (E.g. + * for BCH, the atomic unit is satoshis.) + */ + @Input() + inAtomicUnits: number; + /** + * The currency code/ticker symbol of the value. (E.g. when displaying BCH + * values, this should always be `BCH`, even if values will be displayed in an + * alternative currency.) + */ + @Input() + ticker: string; + /** + * Either the alternative ticker/currency code, or the unit (e.g. satoshis, + * bits, BCH) in which to display the value. + * + * If an alternative ticker is provided, the value will be estimated using + * market exchange rates. + */ + @Input() + displayAs: string; + + constructor(private apiService: ApiService) {} + ngOnChanges() {} +} diff --git a/packages/insight/src/app/shared/date-time/date-time.component.html b/packages/insight/src/app/shared/date-time/date-time.component.html new file mode 100644 index 00000000000..37155aab13a --- /dev/null +++ b/packages/insight/src/app/shared/date-time/date-time.component.html @@ -0,0 +1,4 @@ + + {{value | date:'shortTime'}} + {{value | date:'short'}} + diff --git a/packages/insight/src/app/shared/date-time/date-time.component.scss b/packages/insight/src/app/shared/date-time/date-time.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/insight/src/app/shared/date-time/date-time.component.spec.ts b/packages/insight/src/app/shared/date-time/date-time.component.spec.ts new file mode 100644 index 00000000000..ef377ce7fdb --- /dev/null +++ b/packages/insight/src/app/shared/date-time/date-time.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DateTimeComponent } from './date-time.component'; + +describe('DateTimeComponent', () => { + let component: DateTimeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DateTimeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DateTimeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/insight/src/app/shared/date-time/date-time.component.ts b/packages/insight/src/app/shared/date-time/date-time.component.ts new file mode 100644 index 00000000000..431f48c703c --- /dev/null +++ b/packages/insight/src/app/shared/date-time/date-time.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-date-time', + templateUrl: './date-time.component.html', + styleUrls: ['./date-time.component.scss'] +}) +export class DateTimeComponent implements OnChanges { + @Input() + value: any; + inThePastDay: boolean; + + ngOnChanges() { + const twentyFourHoursBeforeNow = new Date(Date.now() - 1000 * 60 * 60 * 24); + this.inThePastDay = twentyFourHoursBeforeNow < new Date(this.value); + } +} diff --git a/packages/insight/src/app/shared/shared.module.spec.ts b/packages/insight/src/app/shared/shared.module.spec.ts new file mode 100644 index 00000000000..3ecb62606e2 --- /dev/null +++ b/packages/insight/src/app/shared/shared.module.spec.ts @@ -0,0 +1,13 @@ +import { SharedModule } from './shared.module'; + +describe('SharedModule', () => { + let sharedModule: SharedModule; + + beforeEach(() => { + sharedModule = new SharedModule(); + }); + + it('should create an instance', () => { + expect(sharedModule).toBeTruthy(); + }); +}); diff --git a/packages/insight/src/app/shared/shared.module.ts b/packages/insight/src/app/shared/shared.module.ts new file mode 100644 index 00000000000..c369cc9a5cf --- /dev/null +++ b/packages/insight/src/app/shared/shared.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { CurrencyValueComponent } from './currency-value/currency-value.component'; +import { DateTimeComponent } from './date-time/date-time.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [DateTimeComponent, CurrencyValueComponent], + exports: [DateTimeComponent, CurrencyValueComponent] +}) +export class SharedModule {} diff --git a/packages/insight/src/app/types/bitcore-node.ts b/packages/insight/src/app/types/bitcore-node.ts index 1c0d31f3711..2b5cc9871bb 100644 --- a/packages/insight/src/app/types/bitcore-node.ts +++ b/packages/insight/src/app/types/bitcore-node.ts @@ -1,6 +1,4 @@ // TODO: a separate package of well-documented types shared between projects -export { IBlock } from '../../../../bitcore-node/src/models/block'; -export { - StreamingFindOptions -} from '../../../../bitcore-node/src/services/storage'; +export { IBlock } from '../../../../bitcore-node/src/types/Block'; +export { StreamingFindOptions } from '../../../../bitcore-node/src/types/Query'; diff --git a/packages/insight/src/assets/icon/favicon.png b/packages/insight/src/assets/icon/favicon.png index 51888a7bbdb..40eea5da855 100644 Binary files a/packages/insight/src/assets/icon/favicon.png and b/packages/insight/src/assets/icon/favicon.png differ diff --git a/packages/insight/src/assets/icon/favicon@2x.png b/packages/insight/src/assets/icon/favicon@2x.png new file mode 100644 index 00000000000..c279c3d007e Binary files /dev/null and b/packages/insight/src/assets/icon/favicon@2x.png differ diff --git a/packages/insight/src/assets/icon/favicon@4x.png b/packages/insight/src/assets/icon/favicon@4x.png new file mode 100644 index 00000000000..eef589b4de6 Binary files /dev/null and b/packages/insight/src/assets/icon/favicon@4x.png differ diff --git a/packages/insight/src/assets/icon/logo.svg b/packages/insight/src/assets/icon/logo.svg new file mode 100644 index 00000000000..ad6c2dc08c2 --- /dev/null +++ b/packages/insight/src/assets/icon/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/packages/insight/src/environments/environment.prod.ts b/packages/insight/src/environments/environment.prod.ts index 701788d7e8f..6a50e731d00 100644 --- a/packages/insight/src/environments/environment.prod.ts +++ b/packages/insight/src/environments/environment.prod.ts @@ -30,6 +30,7 @@ const expectedNetworks: Chain[] = [ export const environment = { apiPrefix: 'https://api.bitcore.io/api', + ratesApi: 'https://bitpay.com/api/rates/bch', production: true, loggingSettings, initialNetwork, diff --git a/packages/insight/src/index.html b/packages/insight/src/index.html index fe55652acc6..789029cacdc 100644 --- a/packages/insight/src/index.html +++ b/packages/insight/src/index.html @@ -1,23 +1,41 @@ - - - Ionic App + + + Insight Block Explorer - + - - - + + + - + + + + - - - + + + - - - - + + + + diff --git a/packages/insight/src/theme/variables.scss b/packages/insight/src/theme/variables.scss index 4b39b39fd68..24f825340ba 100644 --- a/packages/insight/src/theme/variables.scss +++ b/packages/insight/src/theme/variables.scss @@ -3,21 +3,38 @@ /** Ionic CSS Variables **/ :root { + --ion-font-family: 'Roboto', 'Helvetica Neue', sans-serif; + --ion-text-color: #3d3d4d; + --ion-background-color: #f8f8f9; + /** primary **/ - --ion-color-primary: #3880ff; - --ion-color-primary-rgb: 56, 128, 255; + // --ion-color-primary: #3880ff; + // --ion-color-primary-rgb: 56, 128, 255; + // --ion-color-primary-contrast: #ffffff; + // --ion-color-primary-contrast-rgb: 255, 255, 255; + // --ion-color-primary-shade: #3171e0; + // --ion-color-primary-tint: #4c8dff; + + --ion-color-primary: #2a3387; + --ion-color-primary-rgb: 61, 210, 240; --ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast-rgb: 255, 255, 255; - --ion-color-primary-shade: #3171e0; - --ion-color-primary-tint: #4c8dff; + --ion-color-primary-shade: darken(--ion-color-primary, 10%); + --ion-color-primary-tint: lighten(--ion-color-primary, 10%); /** secondary **/ - --ion-color-secondary: #0cd1e8; - --ion-color-secondary-rgb: 12, 209, 232; + // --ion-color-secondary: #0cd1e8; + // --ion-color-secondary-rgb: 12, 209, 232; + // --ion-color-secondary-contrast: #ffffff; + // --ion-color-secondary-contrast-rgb: 255, 255, 255; + // --ion-color-secondary-shade: #0bb8cc; + // --ion-color-secondary-tint: #24d6ea; + --ion-color-secondary: #3dd2f0; + --ion-color-secondary-rgb: 61, 210, 240; --ion-color-secondary-contrast: #ffffff; --ion-color-secondary-contrast-rgb: 255, 255, 255; - --ion-color-secondary-shade: #0bb8cc; - --ion-color-secondary-tint: #24d6ea; + --ion-color-secondary-shade: #02a1cb; + --ion-color-secondary-tint: #57e8fd; /** tertiary **/ --ion-color-tertiary: #7044ff;