├── src ├── assets │ ├── .gitkeep │ └── external-link.png ├── app │ ├── app.component.scss │ ├── app-routing.module.ts │ ├── app.module.ts │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.component.html ├── components │ └── external-explorer-link │ │ ├── external-explorer-link.component.scss │ │ ├── external-explorer-link.component.ts │ │ ├── external-explorer-link.component.html │ │ └── external-explorer-link.component.spec.ts ├── favicon.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── main.ts ├── index.html ├── variables.scss ├── test.ts ├── polyfills.ts └── styles.scss ├── CODEOWNERS ├── Caddyfile ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.json └── protractor.conf.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── README.md ├── .browserslistrc ├── .gitignore ├── tsconfig.json ├── Dockerfile ├── LICENSE ├── karma.conf.js ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/external-explorer-link/external-explorer-link.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # For now, tag everyone who wants to participate 2 | * @deso-protocol/reviewers 3 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boss-0000/blockchain-explorer/HEAD/src/favicon.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/external-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boss-0000/blockchain-explorer/HEAD/src/assets/external-link.png -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | admin off 3 | auto_https off 4 | } 5 | 6 | :80 { 7 | file_server 8 | try_files {path} index.html 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The DeSo Block Explorer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/external-explorer-link/external-explorer-link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: 'app-external-explorer-link', 5 | templateUrl: './external-explorer-link.component.html', 6 | styleUrls: ['./external-explorer-link.component.scss'] 7 | }) 8 | export class ExternalExplorerLinkComponent implements OnInit { 9 | @Input() path!: string; 10 | 11 | constructor() { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/external-explorer-link/external-explorer-link.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | $dark-color: white; 2 | $light-color: rgb(30, 50, 77); 3 | $light-hover-color: #5a7190; 4 | $dark-in-between-color: #5a7190; 5 | $separating-line-color: #5a7190; 6 | $hardcore-text-color: #5a7190; 7 | $important-text-color: rgb(30, 50, 77); 8 | $text-color: $light-color; 9 | $error-color: #d37676; 10 | $bad-color: #983c3c; 11 | $good-color: #91C8A4; 12 | $yellow-color: #91C8A4; 13 | 14 | $translucent-modal-background: #979797a6; 15 | 16 | $button-font-size: 1rem; 17 | $button-height: 3rem; 18 | 19 | $normal-text-size: 1.25rem; 20 | $big-text-size: 1.5rem; 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Explorer 2 | 3 | The DeSo block explorer 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('explorer app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import {FormsModule} from '@angular/forms'; 7 | import {HttpClientModule} from '@angular/common/http'; 8 | import { ExternalExplorerLinkComponent } from "../components/external-explorer-link/external-explorer-link.component"; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent, 13 | ExternalExplorerLinkComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | AppRoutingModule, 18 | FormsModule, 19 | HttpClientModule, 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent] 23 | }) 24 | export class AppModule { } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/components/external-explorer-link/external-explorer-link.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExternalExplorerLinkComponent } from './external-explorer-link.component'; 4 | 5 | describe('ExternalExplorerLinkComponent', () => { 6 | let component: ExternalExplorerLinkComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ExternalExplorerLinkComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ExternalExplorerLinkComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2015", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15.5-alpine3.13 AS explorer 2 | 3 | WORKDIR /explorer 4 | 5 | COPY ./package.json . 6 | COPY ./package-lock.json . 7 | 8 | # install frontend dependencies before copying the frontend code 9 | # into the container so we get docker cache benefits 10 | RUN npm install 11 | 12 | # running ngcc before build_prod lets us utilize the docker 13 | # cache and significantly speeds up builds without requiring us 14 | # to import/export the node_modules folder from the container 15 | RUN npm run ngcc 16 | 17 | COPY ./angular.json . 18 | COPY ./tsconfig.json . 19 | COPY ./tsconfig.app.json . 20 | COPY ./tslint.json . 21 | COPY ./src ./src 22 | 23 | RUN npm run build_prod 24 | 25 | # build minified version of frontend, served using caddy 26 | FROM caddy:2.3.0-alpine 27 | 28 | WORKDIR /explorer 29 | 30 | COPY ./Caddyfile . 31 | COPY --from=explorer /explorer/dist/explorer . 32 | 33 | ENTRYPOINT ["caddy", "run"] 34 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DeSo Community Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'explorer'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('explorer'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('explorer app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/explorer'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "explorer", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "./node_modules/.bin/ng", 6 | "start": "./node_modules/.bin/ng serve", 7 | "build": "./node_modules/.bin/ng build", 8 | "test": "./node_modules/.bin/ng test", 9 | "lint": "./node_modules/.bin/ng lint", 10 | "e2e": "./node_modules/.bin/ng e2e", 11 | "build_prod": "./node_modules/.bin/ng build --prod --base-href / --deploy-url /", 12 | "ngcc": "./node_modules/.bin/ngcc --properties es2015" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~11.1.1", 17 | "@angular/common": "~11.1.1", 18 | "@angular/compiler": "~11.1.1", 19 | "@angular/core": "~11.1.1", 20 | "@angular/forms": "~11.1.1", 21 | "@angular/platform-browser": "~11.1.1", 22 | "@angular/platform-browser-dynamic": "~11.1.1", 23 | "@angular/router": "~11.1.1", 24 | "rxjs": "~6.6.0", 25 | "tslib": "^2.0.0", 26 | "zone.js": "~0.11.3" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "~0.1101.2", 30 | "@angular/cli": "~11.1.2", 31 | "@angular/compiler-cli": "~11.1.1", 32 | "@types/jasmine": "~3.6.0", 33 | "@types/node": "^12.11.1", 34 | "codelyzer": "^6.0.0", 35 | "jasmine-core": "~3.6.0", 36 | "jasmine-spec-reporter": "~5.0.0", 37 | "karma": "~5.2.0", 38 | "karma-chrome-launcher": "~3.1.0", 39 | "karma-coverage": "~2.0.3", 40 | "karma-jasmine": "~4.0.0", 41 | "karma-jasmine-html-reporter": "^1.5.0", 42 | "protractor": "~7.0.0", 43 | "ts-node": "~8.3.0", 44 | "tslint": "~6.1.0", 45 | "typescript": "~4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "explorer": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/explorer", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "aot": true, 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "500kb", 57 | "maximumError": "1mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "2kb", 62 | "maximumError": "4kb" 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "options": { 71 | "browserTarget": "explorer:build" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "explorer:build:production" 76 | } 77 | } 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "explorer:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "src/styles.scss" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": [ 111 | "**/node_modules/**" 112 | ] 113 | } 114 | }, 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "explorer:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "explorer:serve:production" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "defaultProject": "explorer" 131 | } 132 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | body { 4 | background-color: $dark-color; 5 | color: $light-color; 6 | } 7 | 8 | #content-desktop {display: block;} 9 | #content-mobile {display: none;} 10 | 11 | @media screen and (max-width: 768px) { 12 | 13 | #content-desktop {display: none;} 14 | #content-mobile {display: block;} 15 | 16 | } 17 | 18 | .btn.btn-light { 19 | cursor: pointer; 20 | font-size: $button-font-size; 21 | height: $button-height; 22 | 23 | transition: 0s; 24 | background-color: $light-color; 25 | color: $dark-color; 26 | border-radius: 5px; 27 | border: 2px solid $light-color; 28 | &:hover { 29 | background-color: $light-hover-color; 30 | border: 2px solid $light-hover-color; 31 | } 32 | &:active { 33 | color: $dark-color; 34 | } 35 | } 36 | 37 | .btn.btn-light.selected { 38 | background-color: $dark-color; 39 | color: $light-color; 40 | cursor: default; 41 | &:hover { 42 | background-color: $dark-color; 43 | border: 2px solid $light-color; 44 | } 45 | &:active { 46 | color: $light-color; 47 | } 48 | } 49 | 50 | .divtext { 51 | width: 100%; 52 | height: 100%; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | } 57 | 58 | input { 59 | background-color: $dark-color; 60 | border: 1px solid $separating-line-color; 61 | color: $important-text-color; 62 | padding-left: .5rem; 63 | padding-right: .5rem; 64 | height: 2.5rem; 65 | &:focus { 66 | outline: none; 67 | } 68 | } 69 | 70 | .desktop {display: block;} 71 | .mobile {display: none;} 72 | 73 | @media screen and (max-width: 768px) { 74 | 75 | .desktop {display: none;} 76 | .mobile {display: block;} 77 | 78 | } 79 | 80 | .explorer-container { 81 | width: 90%; 82 | padding-left: 5%; 83 | padding-right: 5%; 84 | } 85 | 86 | .little-button { 87 | height: 2.5rem; 88 | font-weight: bold; 89 | 90 | } 91 | 92 | .container-outer { 93 | width: 100%; 94 | min-height: calc(100vh - 15rem); 95 | display: flex; 96 | justify-content: center; 97 | } 98 | .container-inner { 99 | text-align: center; 100 | } 101 | .welcome-top { 102 | margin-top: 1.5rem; 103 | font-size: 2rem; 104 | } 105 | .welcome { 106 | font-size: 1.5rem; 107 | } 108 | .signup { 109 | margin-top: 1rem; 110 | font-size: 1.5rem; 111 | color: white; 112 | } 113 | .title-logo-container { 114 | width: 100%; 115 | display: flex; 116 | justify-content: center; 117 | margin-top: 1rem; 118 | } 119 | 120 | 121 | a { 122 | color: $light-color; 123 | text-decoration: underline; 124 | } 125 | 126 | 127 | .btn.btn-light.yes { 128 | color: $good-color; 129 | border-color: $good-color; 130 | font-weight: bold; 131 | background-color: $separating-line-color; 132 | } 133 | 134 | .footer { 135 | margin-top: 1rem; 136 | font-size: 1.25rem; 137 | display: flex; 138 | justify-content: center; 139 | } 140 | .footer-inner { 141 | text-align: center; 142 | } 143 | .section-border { 144 | margin-top: 2rem; 145 | margin-left: 1rem; 146 | margin-right: 1rem; 147 | border-bottom: 1px solid $dark-in-between-color; 148 | } 149 | 150 | .unit { 151 | color: $light-color; 152 | font-weight: normal; 153 | } 154 | 155 | 156 | .content-container { 157 | margin-top: 1rem; 158 | text-align: left; 159 | padding-left: 3rem; 160 | padding-right: 3rem; 161 | } 162 | 163 | .section-heading { 164 | position: relative; 165 | z-index: -1; 166 | font-size: 2rem; 167 | font-weight: normal; 168 | text-decoration: none; 169 | text-align: center; 170 | } 171 | 172 | .subsection-heading { 173 | color: white; 174 | font-size: 2rem; 175 | } 176 | 177 | .subcontent-container { 178 | 179 | .help-text { 180 | font-size: 1.25rem; 181 | margin-top: .5rem; 182 | } 183 | 184 | .subsection-content { 185 | margin-top: 1rem; 186 | font-size: 1.25rem; 187 | } 188 | 189 | .download-container { 190 | width: 12.5rem; 191 | height: 12.5rem; 192 | border: 1px solid $dark-in-between-color; 193 | display: inline-block; 194 | margin-top: 1rem; 195 | margin-right: 3rem; 196 | vertical-align: top; 197 | 198 | .intermediate { 199 | width: 12.5rem; 200 | height: 12.5rem; 201 | display: flex; 202 | align-items: center; 203 | justify-content: center; 204 | } 205 | 206 | .download-sub-container { 207 | display: flex; 208 | align-items: center; 209 | justify-content: center; 210 | width: 100%; 211 | height: 100%; 212 | 213 | .operating-system { 214 | cursor: default; 215 | font-size: 2.5rem; 216 | text-align: center; 217 | } 218 | .architecture-container { 219 | text-align: center; 220 | font-size: 1.5rem; 221 | 222 | .architecture { 223 | cursor: pointer; 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | a.btn { 231 | color: $dark-color; 232 | font-weight: bold; 233 | } 234 | 235 | .big-button { 236 | width: 21rem; 237 | } 238 | 239 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Params, Router } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { debounceTime } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent implements OnInit { 12 | constructor( 13 | private ref: ChangeDetectorRef, 14 | private route: ActivatedRoute, 15 | private router: Router, 16 | private httpClient: HttpClient 17 | ) { 18 | } 19 | 20 | explorerQuery = ''; 21 | queryType = ''; 22 | explorerResponse = null; 23 | queryNode = ''; 24 | errorStr = ('Please enter a valid DeSo public key, transaction ID, block ' + 25 | 'hash, or block height. Public keys start with "BC", transaction IDs ' + 26 | 'start with "3J", and block hashes usually start with zeros.'); 27 | 28 | blockRes: any; 29 | txnRes: any; 30 | hasParam = false; 31 | 32 | hasInitialized = false; 33 | txnsLoading = false; 34 | 35 | // Pagination variables 36 | PAGE_SIZE = 200; 37 | currentPage = 1; 38 | pageCache: { [k: number]: any } = {}; 39 | lastTransactionIDBase58Check = ''; 40 | lastPublicKeyTransactionIndex = -1; 41 | 42 | ngOnInit(): void { 43 | // Debounce because angular fires twice when loading with a param 44 | this.route.queryParams.pipe(debounceTime(10)).subscribe((params: Params) => { 45 | this.hasInitialized = true; 46 | this.refreshParams(params); 47 | }); 48 | } 49 | 50 | getExplorerPathBasedOnQuery(): string { 51 | const query = this.explorerQuery.trim(); 52 | 53 | if (this.queryType === 'mempool') { 54 | return 'mempool'; 55 | } 56 | // TODO: what about tip? 57 | if (this.queryType === 'block-hash' || this.queryType === 'block-height') { 58 | return 'blocks/' + query; 59 | } 60 | if (this.queryType === 'transaction-id') { 61 | return 'txn/' + query; 62 | } 63 | if (this.queryType === 'public-key') { 64 | return 'u/' + query; 65 | } 66 | return ''; 67 | } 68 | 69 | refreshParams(params: any): void { 70 | if (params['query-node'] != null) { 71 | this.queryNode = params['query-node']; 72 | } 73 | 74 | if (this.queryNode === '') { 75 | this.queryNode = 'https://node.deso.org'; 76 | } 77 | 78 | let newQuery = '' 79 | if (params.mempool) { 80 | this.queryType = 'mempool'; 81 | newQuery = 'mempool'; 82 | } else if (params['block-hash'] != null) { 83 | this.queryType = 'block-hash'; 84 | newQuery = params['block-hash']; 85 | } else if (params['block-height'] != null) { 86 | this.queryType = 'block-height'; 87 | newQuery = params['block-height']; 88 | } else if (params['transaction-id'] != null) { 89 | this.queryType = 'transaction-id'; 90 | newQuery = params['transaction-id']; 91 | } else if (params['public-key'] != null) { 92 | this.queryType = 'public-key'; 93 | newQuery = params['public-key']; 94 | } else { 95 | this.queryType = 'tip'; 96 | newQuery = 'tip'; 97 | } 98 | 99 | if (params['last-txn-idx'] != null) { 100 | this.lastPublicKeyTransactionIndex = Number(params['last-txn-idx']); 101 | } 102 | 103 | if (params['last-txn-hash'] != null) { 104 | this.lastTransactionIDBase58Check = params['last-txn-hash']; 105 | } 106 | 107 | if (params['page'] != null) { 108 | this.currentPage = Number(params['page']); 109 | } else { 110 | this.resetPagination(); 111 | } 112 | 113 | console.log(this.queryNode); 114 | console.log(this.explorerQuery); 115 | 116 | this.explorerQuery = newQuery; 117 | 118 | this.submitQuery(newQuery); 119 | } 120 | 121 | searchButtonPressed(): void { 122 | this.resetPagination(); 123 | this.relocateForQuery(); 124 | } 125 | 126 | searchEnterPressed(event: KeyboardEvent): void { 127 | if (event.key !== 'Enter') { 128 | return; 129 | } 130 | 131 | this.resetPagination(); 132 | this.relocateForQuery(); 133 | } 134 | 135 | copy(val: string): void { 136 | const selBox = document.createElement('textarea'); 137 | selBox.style.position = 'fixed'; 138 | selBox.style.left = '0'; 139 | selBox.style.top = '0'; 140 | selBox.style.opacity = '0'; 141 | selBox.value = val; 142 | document.body.appendChild(selBox); 143 | selBox.focus(); 144 | selBox.select(); 145 | document.execCommand('copy'); 146 | document.body.removeChild(selBox); 147 | } 148 | 149 | resetPagination(): void { 150 | // Reset the pagination 151 | this.currentPage = 1; 152 | this.pageCache = {}; 153 | this.lastPublicKeyTransactionIndex = -1; 154 | this.lastTransactionIDBase58Check = ''; 155 | } 156 | 157 | relocateForQuery(): void { 158 | if (this.explorerQuery == null || this.explorerQuery === '') { 159 | alert(this.errorStr); 160 | return; 161 | } 162 | 163 | if (this.explorerQuery.startsWith('BC') || this.explorerQuery.startsWith('tBC')) { 164 | this.router.navigate(['/'], { 165 | queryParams: { 166 | 'query-node': this.queryNode, 167 | 'public-key': this.explorerQuery, 168 | 'last-txn-idx': this.lastPublicKeyTransactionIndex, 169 | 'page': this.currentPage, 170 | } 171 | }); 172 | 173 | } else if (this.explorerQuery === 'mempool') { 174 | this.router.navigate(['/'], { 175 | queryParams: { 176 | 'query-node': this.queryNode, 177 | 'last-txn-hash': this.lastTransactionIDBase58Check, 178 | 'page': this.currentPage, 179 | mempool: true 180 | } 181 | }); 182 | 183 | } else if (this.explorerQuery.startsWith('0')) { 184 | this.router.navigate(['/'], { 185 | queryParams: { 186 | 'query-node': this.queryNode, 187 | 'block-hash': this.explorerQuery 188 | } 189 | }); 190 | 191 | } else if (this.explorerQuery.startsWith('3Ju') || this.explorerQuery.startsWith('CbU') || this.explorerQuery.length === 64) { 192 | this.router.navigate(['/'], { 193 | queryParams: { 194 | 'query-node': this.queryNode, 195 | 'transaction-id': this.explorerQuery 196 | } 197 | }); 198 | 199 | } else if (parseInt(this.explorerQuery, 10) != null && !isNaN(parseInt(this.explorerQuery, 10))) { 200 | this.router.navigate(['/'], { 201 | queryParams: { 202 | 'query-node': this.queryNode, 203 | 'block-height': this.explorerQuery 204 | } 205 | }); 206 | 207 | } else { 208 | alert(this.errorStr); 209 | } 210 | } 211 | 212 | responseHandler(context: AppComponent, res: any): void { 213 | if (res?.Header) { 214 | this.blockRes = res; 215 | this.blockRes.Header.DateTime = new Date(this.blockRes.Header.TstampSecs * 1000); 216 | } 217 | 218 | if (res.Transactions) { 219 | // Clear unnecessary fields 220 | for (const vv of res.Transactions) { 221 | if (vv && vv.TransactionMetadata && vv.TransactionMetadata.BasicTransferTxindexMetadata && 222 | vv.TransactionMetadata.BasicTransferTxindexMetadata.UtxoOpsDump) { 223 | vv.TransactionMetadata.BasicTransferTxindexMetadata.UtxoOpsDump = 'redacted'; 224 | } 225 | } 226 | 227 | this.txnRes = { 228 | Transactions: res.Transactions, 229 | LastTransactionIDBase58Check: res.LastTransactionIDBase58Check, 230 | LastPublicKeyTransactionIndex: res.LastPublicKeyTransactionIndex, 231 | BalanceNanos: res.BalanceNanos, 232 | }; 233 | 234 | this.lastTransactionIDBase58Check = res.LastTransactionIDBase58Check; 235 | this.lastPublicKeyTransactionIndex = res.LastPublicKeyTransactionIndex; 236 | this.pageCache[this.currentPage] = this.txnRes; 237 | } 238 | 239 | this.ref.detectChanges(); 240 | this.txnsLoading = false; 241 | } 242 | 243 | errorHandler(context: AppComponent, err: any): void { 244 | let errorMessage; 245 | 246 | if (err.error != null && err.error.Error != null) { 247 | // Is it obvious yet that I'm not a frontend gal? 248 | // TODO: Error handling between BE and FE needs a major redesign. 249 | errorMessage = err.error.Error; 250 | } else if (err.status != null && err.status !== 200) { 251 | errorMessage = 'Error connecting to query node: ' + this.queryNode; 252 | } else { 253 | errorMessage = 'Unknown error occurred: ' + JSON.stringify(err); 254 | } 255 | 256 | alert(errorMessage); 257 | this.txnsLoading = false; 258 | } 259 | 260 | submitQuery(query: string): void { 261 | console.log('Submitting query'); 262 | 263 | if (query == null || query === '') { 264 | console.error(this.errorStr) 265 | alert(this.errorStr); 266 | return; 267 | } 268 | 269 | // Cache paginated results 270 | if (this.currentPage in this.pageCache) { 271 | console.log('Using cached paginated results for page ' + this.currentPage); 272 | this.txnRes = this.pageCache[this.currentPage]; 273 | this.lastTransactionIDBase58Check = this.txnRes.LastTransactionIDBase58Check; 274 | this.lastPublicKeyTransactionIndex = this.txnRes.LastPublicKeyTransactionIndex; 275 | return 276 | } 277 | 278 | // If we're calling submitQuery, set hasParam so the tip node information stops showing. 279 | this.hasParam = true; 280 | this.blockRes = null; 281 | this.txnRes = null; 282 | this.txnsLoading = true; 283 | 284 | if (query === 'tip') { 285 | this.httpClient.get( 286 | `${this.queryNode}/api/v1`, {withCredentials: true} 287 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 288 | } else if (query.startsWith('BC') || query.startsWith('tBC')) { 289 | // If the string starts with "BC" we treat it as a public key query. 290 | this.httpClient.post( 291 | `${this.queryNode}/api/v1/transaction-info`, { 292 | PublicKeyBase58Check: query, 293 | LastTransactionIDBase58Check: this.lastTransactionIDBase58Check, 294 | LastPublicKeyTransactionIndex: this.lastPublicKeyTransactionIndex, 295 | Limit: this.PAGE_SIZE, 296 | }, {withCredentials: true} 297 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 298 | 299 | } else if (query === 'mempool') { 300 | this.httpClient.post( 301 | `${this.queryNode}/api/v1/transaction-info`, { 302 | IsMempool: true, 303 | LastTransactionIDBase58Check: this.lastTransactionIDBase58Check, 304 | Limit: this.PAGE_SIZE, 305 | }, {withCredentials: true} 306 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 307 | } else if (query.startsWith('0')) { 308 | // If it starts with a 0, we know we're dealing with a block hash. 309 | this.httpClient.post( 310 | `${this.queryNode}/api/v1/block`, { 311 | HashHex: query, 312 | FullBlock: true, 313 | }, {withCredentials: true} 314 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 315 | 316 | } else if (query.startsWith('3Ju') || query.startsWith('CbU') || query.length === 64) { 317 | // If the string starts with 3Ju/CbU/or 64 chars (hex rosetta version) we treat it as a transaction ID. 318 | this.httpClient.post( 319 | `${this.queryNode}/api/v1/transaction-info`, { 320 | TransactionIDBase58Check: query, 321 | }, {withCredentials: true} 322 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 323 | 324 | } else if (parseInt(query, 10) != null && !isNaN(parseInt(query, 10))) { 325 | // As a last attempt, if the query can be parsed as a block height, then do that. 326 | this.httpClient.post( 327 | `${this.queryNode}/api/v1/block`, { 328 | Height: parseInt(query, 10), 329 | FullBlock: true, 330 | }, {withCredentials: true} 331 | ).subscribe((res) => this.responseHandler(this, res), (err) => this.errorHandler(this, err)); 332 | 333 | } else { 334 | this.txnsLoading = false; 335 | alert(this.errorStr); 336 | } 337 | } 338 | 339 | showNextPageBtn(): boolean { 340 | return ( 341 | // We have a response with transactions 342 | this.txnRes && 343 | this.txnRes.Transactions && 344 | // That is at least PAGE_SIZE 345 | this.txnRes.Transactions.length >= this.PAGE_SIZE && 346 | // That is not a block 347 | !this.blockRes 348 | ) || ( 349 | // OR we are loading transactions and on 2+ page 350 | this.txnsLoading && this.currentPage >= 2 351 | ); 352 | } 353 | 354 | nextPage(): void { 355 | if (this.txnsLoading) { 356 | return; 357 | } 358 | 359 | this.currentPage += 1; 360 | this.relocateForQuery(); 361 | } 362 | 363 | prevPage(): void { 364 | if (this.txnsLoading) { 365 | return; 366 | } 367 | 368 | this.currentPage -= 1; 369 | this.relocateForQuery(); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Click here to try the new block explorer! 4 |
5 |
6 |
7 | Welcome to the DeSo Block Explorer 8 |
9 |
10 | Enter a DeSo transaction ID, public key, block hash, or block height. 11 |
12 |
13 | 16 |
18 |
19 | Search 20 |
21 |
22 |
24 |
25 | Previous Page 26 |
27 |
28 |
30 |
31 | Next Page 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 | Learn more about DeSo at deso.org. 40 |
41 | 42 |
43 |
44 | Block Information 45 |
46 |
47 | Current Block Tip 48 |
49 |
50 |
51 | Block Hash: 52 | 53 |
54 | {{blockRes.Header.BlockHashHex}} 56 | 57 | 58 |
59 |
60 |
61 | Timestamp:{{blockRes.Header.DateTime}} 62 |
63 |
64 | Height: 65 | 66 |
67 | {{blockRes.Header.Height}} 70 | 71 | 72 |
73 |
74 | 75 |
76 | Previous Block Hash: 77 | 78 | 86 |
87 |
88 | Transaction Merkle Root:{{blockRes.Header.TransactionMerkleRootHex}} 89 |
90 |
91 | Nonce:{{blockRes.Header.Nonce}} 92 |
93 |
94 | Version:{{blockRes.Header.Version}} 95 |
96 |
97 | 98 |
99 |
100 |
101 | Block Transactions 102 |
103 |
104 |
105 | Final Balance (Including Unconfirmed Transactions) 106 |
107 |

108 | 109 | {{(txnRes.BalanceNanos / 1e9).toLocaleString('en-US', {minimumFractionDigits: 9})}} 110 | 111 |

112 |
113 |
114 | Transactions Found 115 |
116 |
117 |
118 | Transaction #{{txnIndex + (currentPage - 1)*PAGE_SIZE}} 119 | (latest): 120 |
121 |
122 |
123 | Transaction ID: 124 | 125 |
126 | {{thisTxn.TransactionIDBase58Check}} 129 | 130 | 132 |
133 |
134 | 135 |
136 | Block Hash: 137 |
138 | 143 |
{{thisTxn.BlockHashHex}}
144 | 145 |
146 |
149 | UNCONFIRMED 150 | 151 |
152 |
153 |
154 |
155 | Transaction Type:{{thisTxn.TransactionType}} 156 |
157 |
160 |
161 | Imporant: 162 |
163 |
164 | This transaction type can take up to 24 hours to confirm. This is normal, and is due to the fact that 165 | we have to wait for enough "work" to build on the Bitcoin blockchain before accepting it. 166 |
167 |
168 |
169 | Transaction Metadata: 170 |
171 |
172 |

Parsed Fields

173 |
174 | Total Input: {{(thisTxn.TransactionMetadata.BasicTransferTxindexMetadata.TotalInputNanos / 175 | 1e9).toFixed(9)}} 176 |
177 |
178 | Total Output: {{(thisTxn.TransactionMetadata.BasicTransferTxindexMetadata.TotalOutputNanos / 179 | 1e9).toFixed(9)}} 180 |
181 |
182 | Fees: {{(thisTxn.TransactionMetadata.BasicTransferTxindexMetadata.FeeNanos / 1e9).toFixed(9)}} 183 |
184 | 185 |
186 |
187 | Bitcoin Burned: {{(thisTxn.TransactionMetadata.BitcoinExchangeTxindexMetadata.SatoshisBurned / 188 | 1e8).toFixed(8)}} 189 |
190 |
191 | DeSo Created: {{(thisTxn.TransactionMetadata.BitcoinExchangeTxindexMetadata.NanosCreated / 192 | 1e9).toFixed(9)}} 193 |
194 |
195 | Total DeSo Purchased Before: 196 | {{(thisTxn.TransactionMetadata.BitcoinExchangeTxindexMetadata.TotalNanosPurchasedBefore / 197 | 1e9).toFixed(9)}} 198 |
199 |
200 | Total DeSo Purchased After: 201 | {{(thisTxn.TransactionMetadata.BitcoinExchangeTxindexMetadata.TotalNanosPurchasedAfter / 202 | 1e9).toFixed(9)}} 203 |
204 | 211 |
212 | 213 |

Raw Metadata

214 |
{{thisTxn.TransactionMetadata | json}}
215 |
216 |
217 | (coming soon!) 218 |
219 |
220 |
221 |
222 | Raw Transaction Hex:{{thisTxn.RawTransactionHex.slice(0, 50)}}... 224 |
226 |
227 | Copy 228 |
229 |
230 | 231 |
232 | 233 |
234 | Transaction Inputs 235 |
236 |
237 |
238 | Input #{{inputIndex}} 239 |
240 |
241 |
242 | Transaction ID: 243 | 244 |
245 | {{txnInput.TransactionIDBase58Check}} 248 | 249 | 251 |
252 |
253 |
254 | Index:{{txnInput.Index}} 255 |
256 |
257 |
258 |
259 | Transaction Outputs 260 |
261 |
262 |
263 | Output #{{outputIndex}} 264 |
265 |
266 |
267 | Public Key: 268 |
269 | {{txnOutput.PublicKeyBase58Check}} 272 | 273 | 275 |
276 |
277 |
278 | DeSo:{{(txnOutput.AmountNanos / 279 | 1e9).toFixed(9)}} 280 |
281 |
282 |
283 |
284 |
285 | 286 |
287 |
288 |

289 | Loading... 290 |

291 |
292 |
293 |
294 |
295 |
296 | 297 | --------------------------------------------------------------------------------