├── .firebaserc ├── .gitignore ├── src ├── assets │ ├── seo │ │ ├── google736091bb9c73b86b.html │ │ ├── robots.txt │ │ └── sitemap.xml │ ├── fonts │ │ ├── pt-mono │ │ │ ├── PTM55F.ttf │ │ │ └── SourceCodePro-Regular.otf │ │ └── courier-prime │ │ │ ├── Courier-Prime.ttf │ │ │ ├── Courier Prime Bold.ttf │ │ │ ├── Courier Prime Italic.ttf │ │ │ └── Courier Prime Bold Italic.ttf │ ├── icons │ │ ├── social-media │ │ │ ├── jtv.png │ │ │ ├── twitter.svg │ │ │ ├── telegram.svg │ │ │ └── discord.svg │ │ ├── github-icon.svg │ │ ├── banano-mark.svg │ │ ├── banano-mark-gray.svg │ │ └── bananologo.svg │ ├── branding │ │ ├── icon │ │ │ ├── creeper-on-dark-icon-1024w.png │ │ │ ├── creeper-on-dark-icon-128w.png │ │ │ ├── creeper-on-dark-icon-192w.png │ │ │ ├── creeper-on-dark-icon-256w.png │ │ │ ├── creeper-on-dark-icon-32w.png │ │ │ ├── creeper-on-dark-icon-512w.png │ │ │ ├── creeper-on-dark-icon-64w.png │ │ │ ├── creeper-on-dark-icon-768w.png │ │ │ ├── creeper-on-dark-icon-180w-apple-touch-icon.png │ │ │ └── creeper-on-dark-icon.svg │ │ ├── creeper-on-light-horizontal.svg │ │ ├── creeper-on-light-vertical.svg │ │ └── creeper-on-dark-vertical.svg │ └── gobanme │ │ └── banano.json ├── app │ ├── types │ │ ├── dto │ │ │ ├── AliasDto.ts │ │ │ ├── PeerVersionsDto.ts │ │ │ ├── PriceDataDto.ts │ │ │ ├── BNSDomainDto.ts │ │ │ ├── AccountBalanceDto.ts │ │ │ ├── DiscordResponseDto.ts │ │ │ ├── SocialMediaAccountAliasDto.ts │ │ │ ├── ReceivableTransactionDto.ts │ │ │ ├── NakamotoCoefficientDto.ts │ │ │ ├── RepresentativeDto.ts │ │ │ ├── SupplyDto.ts │ │ │ ├── DelegatorDto.ts │ │ │ ├── ConfirmedTransactionDto.ts │ │ │ ├── QuorumCoefficientDto.ts │ │ │ ├── AccountNFTDto.ts │ │ │ ├── QuorumDto.ts │ │ │ ├── ExplorerSummaryDto.ts │ │ │ ├── KnownAccountDto.ts │ │ │ ├── AccountOverviewDto.ts │ │ │ ├── AccountDistributionStatsDto.ts │ │ │ ├── HostNodeStatsDto.ts │ │ │ ├── RepScoreDto.ts │ │ │ ├── RepresentativeUptimeDto.ts │ │ │ ├── BlockAtHeightDto.ts │ │ │ ├── BlockDto.ts │ │ │ ├── InsightsDto.ts │ │ │ ├── MonitoredRepDto.ts │ │ │ └── index.ts │ │ └── modal │ │ │ ├── Bookmark.ts │ │ │ ├── MicroRepresentative.ts │ │ │ ├── index.ts │ │ │ ├── MonitoredRep.ts │ │ │ ├── Representative.ts │ │ │ └── SharedRepProps.ts │ ├── common │ │ ├── components │ │ │ ├── load-spinner │ │ │ │ └── load-spinner.component.ts │ │ │ ├── error │ │ │ │ └── error.component.ts │ │ │ ├── alias │ │ │ │ └── alias.component.scss │ │ │ ├── copy-button │ │ │ │ └── copy-button.component.ts │ │ │ └── bookmark-button │ │ │ │ └── bookmark-button.component.ts │ │ ├── pipes │ │ │ ├── safe.pipe.ts │ │ │ ├── percentage.pipe.ts │ │ │ ├── color-address.pipe.ts │ │ │ ├── little-decimal.pipe.ts │ │ │ └── comma.directive.ts │ │ ├── animations │ │ │ └── animations.ts │ │ ├── directives │ │ │ └── responsive.directive.ts │ │ └── app-common.module.ts │ ├── navigation │ │ ├── ha.scss │ │ ├── search-bar │ │ │ ├── bold-search.pipe.ts │ │ │ └── search-bar.component.scss │ │ ├── user-menu │ │ │ ├── user-menu.component.scss │ │ │ └── user-menu.component.ts │ │ ├── navigation.component.spec.ts │ │ ├── navigation.module.ts │ │ ├── app-bar │ │ │ └── app-bar.component.scss │ │ └── nav-items.ts │ ├── pages │ │ ├── bookmarks │ │ │ ├── bookmarks.component.scss │ │ │ ├── bookmarks.component.spec.ts │ │ │ ├── delete-bookmark-dialog.component.ts │ │ │ └── bookmarks.module.ts │ │ ├── account │ │ │ ├── tabs │ │ │ │ ├── transaction │ │ │ │ │ ├── compact-view │ │ │ │ │ │ └── compact-view.component.scss │ │ │ │ │ ├── transaction-tab.component.html │ │ │ │ │ └── casual-view │ │ │ │ │ │ └── casual-view.component.scss │ │ │ │ ├── nfts │ │ │ │ │ ├── nfts-tab.component.scss │ │ │ │ │ └── nfts-tab.components.ts │ │ │ │ ├── insights │ │ │ │ │ ├── insights-tab.service.ts │ │ │ │ │ └── insights-tab.component.scss │ │ │ │ └── delegators │ │ │ │ │ ├── delegators-tab.component.scss │ │ │ │ │ └── delegators-tab.service.ts │ │ │ ├── action-buttons │ │ │ │ ├── filter-button │ │ │ │ │ └── filter-button.component.ts │ │ │ │ ├── qr-button │ │ │ │ │ └── qr-button.component.ts │ │ │ │ ├── view-button │ │ │ │ │ └── view-button.component.ts │ │ │ │ └── csv-button │ │ │ │ │ └── csv-button.component.ts │ │ │ ├── qr-dialog │ │ │ │ └── qr-dialog.component.ts │ │ │ └── account-actions-menu │ │ │ │ └── account-actions-menu.component.ts │ │ ├── vanity │ │ │ ├── vanity.module.ts │ │ │ ├── vanity.component.spec.ts │ │ │ ├── vanity.component.ts │ │ │ └── vanity.component.html │ │ ├── representatives │ │ │ ├── display │ │ │ │ ├── monitored-rep-table │ │ │ │ │ └── monitored-rep-table.component.scss │ │ │ │ ├── metrics │ │ │ │ │ ├── score-metric │ │ │ │ │ │ └── score-metric.component.ts │ │ │ │ │ └── uptime-metric │ │ │ │ │ │ └── uptime-metric.component.ts │ │ │ │ ├── large-rep-table │ │ │ │ │ └── large-rep-table.component.scss │ │ │ │ ├── weight-chart │ │ │ │ │ └── weight-chart.component.scss │ │ │ │ └── micro-rep-list │ │ │ │ │ └── micro-rep-list.component.ts │ │ │ ├── representatives.service.ts │ │ │ ├── representatives.component.spec.ts │ │ │ ├── representatives.component.scss │ │ │ └── representatives.module.ts │ │ ├── block │ │ │ ├── block.module.ts │ │ │ └── block.component.scss │ │ ├── node-monitor │ │ │ ├── node-monitor.module.ts │ │ │ ├── node-monitor.component.spec.ts │ │ │ └── node-monitor.component.scss │ │ ├── network │ │ │ ├── network.component.spec.ts │ │ │ ├── network.module.ts │ │ │ └── network.component.scss │ │ ├── wallets │ │ │ ├── wallets.component.spec.ts │ │ │ ├── wallets.module.ts │ │ │ ├── wallets.component.scss │ │ │ └── paginator │ │ │ │ └── paginator.component.ts │ │ ├── home │ │ │ ├── home.component.spec.ts │ │ │ ├── home.module.ts │ │ │ └── home.component.ts │ │ └── known-accounts │ │ │ ├── known-accounts.component.spec.ts │ │ │ ├── known-accounts.module.ts │ │ │ └── known-accounts.component.scss │ ├── services │ │ ├── viewport │ │ │ ├── viewport.service.spec.ts │ │ │ └── viewport.service.ts │ │ ├── loading │ │ │ └── loading.service.ts │ │ ├── drawer-state │ │ │ ├── drawer-state.service.spec.ts │ │ │ └── drawer-state.service.ts │ │ ├── monkey-cache │ │ │ └── monkey-cache.service.ts │ │ ├── online-reps │ │ │ └── online-reps.service.ts │ │ ├── theme │ │ │ └── theme.service.ts │ │ ├── price │ │ │ └── price.service.ts │ │ ├── plausible │ │ │ └── plausible.service.ts │ │ ├── util │ │ │ └── util.service.ts │ │ ├── search │ │ │ └── search.service.ts │ │ ├── bookmarks │ │ │ └── bookmarks.service.ts │ │ ├── account-actions │ │ │ └── account-actions.service.ts │ │ └── alias │ │ │ └── alias.service.ts │ ├── app.component.spec.ts │ ├── app.module.ts │ └── app.routing.ts ├── environments │ ├── environment.prod.ts │ ├── environment.brpd.ts │ └── environment.ts ├── main.ts ├── test.ts ├── index.html ├── theme │ ├── animation.scss │ └── typography.scss └── polyfills.ts ├── .eslintrc.js ├── .editorconfig ├── firebase.json ├── tsconfig.app.json ├── .github └── workflows │ ├── tagging.yml │ ├── firebase-hosting-merge.yml │ ├── firebase-hosting-pull-request.yml │ └── ci.yml ├── tsconfig.spec.json ├── tsconfig.json ├── karma.conf.js ├── README.md ├── CHANGELOG.md ├── CONTRIBUTING.md └── package.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "brpd-creeper" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .firebase 4 | .idea 5 | yarn-error.log 6 | .angular 7 | -------------------------------------------------------------------------------- /src/assets/seo/google736091bb9c73b86b.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google736091bb9c73b86b.html 2 | -------------------------------------------------------------------------------- /src/assets/seo/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Sitemap: https://creeper.banano.cc/sitemap.xml 4 | -------------------------------------------------------------------------------- /src/app/types/dto/AliasDto.ts: -------------------------------------------------------------------------------- 1 | export type AliasDto = { 2 | address: string; 3 | alias: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/types/modal/Bookmark.ts: -------------------------------------------------------------------------------- 1 | export type Bookmark = { 2 | id: string; 3 | alias?: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/types/dto/PeerVersionsDto.ts: -------------------------------------------------------------------------------- 1 | export type PeerVersionsDto = { 2 | version: string; 3 | count: number; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/types/dto/PriceDataDto.ts: -------------------------------------------------------------------------------- 1 | export type PriceDataDto = { 2 | bananoPriceUsd: number; 3 | bitcoinPriceUsd: number; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/types/modal/MicroRepresentative.ts: -------------------------------------------------------------------------------- 1 | export type MicroRepresentative = { 2 | address: string; 3 | weight: number; 4 | }; 5 | -------------------------------------------------------------------------------- /src/assets/fonts/pt-mono/PTM55F.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/pt-mono/PTM55F.ttf -------------------------------------------------------------------------------- /src/app/types/dto/BNSDomainDto.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from 'banani-bns'; 2 | 3 | export type BNSDomainDto = { 4 | domain: Domain; 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/icons/social-media/jtv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/icons/social-media/jtv.png -------------------------------------------------------------------------------- /src/app/types/dto/AccountBalanceDto.ts: -------------------------------------------------------------------------------- 1 | export type AccountBalanceDto = { 2 | address: string; 3 | amount: number; 4 | representative: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/courier-prime/Courier-Prime.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/courier-prime/Courier-Prime.ttf -------------------------------------------------------------------------------- /src/assets/fonts/pt-mono/SourceCodePro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/pt-mono/SourceCodePro-Regular.otf -------------------------------------------------------------------------------- /src/app/types/dto/DiscordResponseDto.ts: -------------------------------------------------------------------------------- 1 | export type DiscordResponseDto = { 2 | user_id: string; 3 | user_last_known_name: string; 4 | address: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/types/modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bookmark'; 2 | export * from './Representative'; 3 | export * from './MonitoredRep'; 4 | export * from './MicroRepresentative'; 5 | -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-1024w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-1024w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-128w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-192w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-192w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-256w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-32w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-32w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-512w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-64w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-64w.png -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-768w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-768w.png -------------------------------------------------------------------------------- /src/assets/fonts/courier-prime/Courier Prime Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/courier-prime/Courier Prime Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/courier-prime/Courier Prime Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/courier-prime/Courier Prime Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/courier-prime/Courier Prime Bold Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/fonts/courier-prime/Courier Prime Bold Italic.ttf -------------------------------------------------------------------------------- /src/app/types/dto/SocialMediaAccountAliasDto.ts: -------------------------------------------------------------------------------- 1 | export type SocialMediaAccountAliasDto = { 2 | address: string; 3 | alias: string; 4 | platform: string; 5 | platformUserId: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/branding/icon/creeper-on-dark-icon-180w-apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ptera/yellow-spyglass-client/HEAD/src/assets/branding/icon/creeper-on-dark-icon-180w-apple-touch-icon.png -------------------------------------------------------------------------------- /src/app/types/dto/ReceivableTransactionDto.ts: -------------------------------------------------------------------------------- 1 | export type ReceivableTransactionDto = { 2 | amount: number; 3 | amountRaw: string; 4 | timestamp: number; 5 | hash: string; 6 | address: string; 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | brpd: false, 4 | api1: 'https://api.spyglass.pw/banano', 5 | api2: 'https://api.creeper.banano.cc/banano', 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/types/modal/MonitoredRep.ts: -------------------------------------------------------------------------------- 1 | import { MonitoredRepDto } from '@app/types/dto'; 2 | import { SharedRepProps } from '@app/types/modal/SharedRepProps'; 3 | 4 | export type MonitoredRep = MonitoredRepDto & SharedRepProps; 5 | -------------------------------------------------------------------------------- /src/app/types/modal/Representative.ts: -------------------------------------------------------------------------------- 1 | import { RepresentativeDto } from '@app/types/dto'; 2 | import { SharedRepProps } from '@app/types/modal/SharedRepProps'; 3 | 4 | export type Representative = RepresentativeDto & SharedRepProps; 5 | -------------------------------------------------------------------------------- /src/assets/gobanme/banano.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dev Ptera", 3 | "description": "Creeper Banano Explorer", 4 | "suggested_donation": "1", 5 | "address": "ban_3batmanuenphd7osrez9c45b3uqw9d9u81ne8xa6m43e1py56y9p48ap69zg" 6 | } 7 | -------------------------------------------------------------------------------- /src/app/types/dto/NakamotoCoefficientDto.ts: -------------------------------------------------------------------------------- 1 | export type NakamotoCoefficientDto = { 2 | delta: number; 3 | nakamotoCoefficient: number; 4 | ncRepresentatives: Array<{ address: string; weight: number }>; 5 | ncRepsWeight: number; 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/types/dto/RepresentativeDto.ts: -------------------------------------------------------------------------------- 1 | export type RepresentativeDto = { 2 | address: string; 3 | online: boolean; 4 | alias?: string; 5 | weight: number; 6 | delegatorsCount?: number; 7 | fundedDelegatorsCount?: number; 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/types/dto/SupplyDto.ts: -------------------------------------------------------------------------------- 1 | export type SupplyDto = { 2 | totalAmount: number; 3 | circulatingAmount: number; 4 | circulatingPercent: number; 5 | burnedAmount: number; 6 | devFundAmount: number; 7 | devFundPercent: number; 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ '@brightlayer-ui/eslint-config/ts' ], 4 | parserOptions: { 5 | project: "./tsconfig.json", 6 | }, 7 | env: { 8 | browser: true 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/types/modal/SharedRepProps.ts: -------------------------------------------------------------------------------- 1 | export type SharedRepProps = { 2 | score?: number; 3 | principal?: boolean; 4 | uptimePercentages?: { 5 | day: number; 6 | week: number; 7 | month: number; 8 | semiAnnual: number; 9 | year: number; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/app/types/dto/DelegatorDto.ts: -------------------------------------------------------------------------------- 1 | export type DelegatorsOverviewDto = { 2 | delegators: DelegatorDto[]; 3 | count: number; 4 | fundedCount: number; 5 | emptyCount: number; 6 | weightSum: number; 7 | }; 8 | 9 | export type DelegatorDto = { 10 | address: string; 11 | weight: number; 12 | }; 13 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/yellow-spyglass-client", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/types/dto/ConfirmedTransactionDto.ts: -------------------------------------------------------------------------------- 1 | export type ConfirmedTransactionDto = { 2 | amount?: number; 3 | amountRaw?: string; 4 | hash: string; 5 | type: 'receive' | 'send' | 'change'; 6 | height: number; 7 | address?: string; 8 | timestamp: number; 9 | date: string; 10 | newRepresentative?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/types/dto/QuorumCoefficientDto.ts: -------------------------------------------------------------------------------- 1 | export type QuorumCoefficientDto = { 2 | delta: number; 3 | onlineWeight: number; 4 | onlineWeightMinimum: number; 5 | coefficient: number; 6 | representatives: BasicRep[]; 7 | repsWeight: number; 8 | }; 9 | 10 | type BasicRep = { 11 | address: string; 12 | weight: number; 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/common/components/load-spinner/load-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-load-spinner', 5 | encapsulation: ViewEncapsulation.None, 6 | template: ` `, 7 | }) 8 | export class LoadSpinnerComponent {} 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/navigation/ha.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .navigation-drawer-content.isPranked { 2 | .mat-drawer-content { 3 | height: unset !important; 4 | } 5 | app-home .app-page-root { 6 | padding-top: 0 !important; 7 | } 8 | .app-page-root { 9 | padding-top: 48px; 10 | } 11 | .sm.app-page-root { 12 | padding-top: 32px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/types/dto/AccountNFTDto.ts: -------------------------------------------------------------------------------- 1 | export type AccountNFTDto = { 2 | name: string; 3 | image: string; 4 | description: string; 5 | properties: { 6 | issuer: string; 7 | supply_block_hash: string; 8 | }; 9 | certain: boolean; 10 | receive_hash: string; 11 | rep: string; 12 | quantity: number; 13 | isExpanded: boolean; 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/common/pipes/safe.pipe.ts: -------------------------------------------------------------------------------- 1 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 2 | import { Pipe } from '@angular/core'; 3 | 4 | @Pipe({ name: 'safe' }) 5 | export class SafeHtmlPipe { 6 | constructor(private readonly _sanitizer: DomSanitizer) {} 7 | 8 | transform(html: string): SafeHtml { 9 | return this._sanitizer.bypassSecurityTrustHtml(html); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/pages/bookmarks/bookmarks.component.scss: -------------------------------------------------------------------------------- 1 | .bookmarks-data-cell { 2 | display: flex; 3 | align-items: center; 4 | .bookmarks-data { 5 | word-break: break-all; 6 | cursor: pointer; 7 | &:hover { 8 | text-decoration: underline; 9 | } 10 | } 11 | ::ng-deep .mat-form-field-wrapper { 12 | padding-bottom: 0 !important; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/tagging.yml: -------------------------------------------------------------------------------- 1 | name: Tagging 2 | 3 | env: 4 | GH_TOKEN: ${{ github.token }} 5 | CURRENT_BRANCH: ${{ github.ref_name }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | tag-package: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - run: yarn tag:package -b ${CURRENT_BRANCH} 18 | -------------------------------------------------------------------------------- /src/app/types/dto/QuorumDto.ts: -------------------------------------------------------------------------------- 1 | export type QuorumDto = { 2 | noRepPercent: number; 3 | noRepWeight: number; 4 | nonBurnedWeight: number; 5 | offlinePercent: number; 6 | offlineWeight: number; 7 | onlinePercent: number; 8 | onlineWeight: number; 9 | onlineWeightMinimum: number; 10 | onlineWeightQuorumPercent: number; 11 | peersStakeWeight: number; 12 | quorumDelta: number; 13 | }; 14 | -------------------------------------------------------------------------------- /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() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/app/types/dto/ExplorerSummaryDto.ts: -------------------------------------------------------------------------------- 1 | export type ExplorerSummaryDto = { 2 | knownAccountsCount: number; 3 | circulatingCount: number; 4 | devFundCount: number; 5 | totalPrincipalRepsCount: number; 6 | representativesOnlineCount: number; 7 | principalRepsOnlineCount: number; 8 | confirmedTransactionsCount: number; 9 | ledgerSizeMB: number; 10 | ledgerDatabaseType: string; 11 | bananoPriceUsd: number; 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/services/viewport/viewport.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ViewportService } from './viewport.service'; 4 | 5 | describe('ViewportService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ViewportService = TestBed.get(ViewportService); 10 | void expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/common/pipes/percentage.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'appPercent' }) 4 | export class PercentagePipe implements PipeTransform { 5 | transform(value: number): string { 6 | return this.formatPercentage(value * 100, 1); 7 | } 8 | 9 | formatPercentage(num: number, decimals: number): string { 10 | return `${Number(parseFloat(String(num)).toFixed(decimals))}%`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/types/dto/KnownAccountDto.ts: -------------------------------------------------------------------------------- 1 | export type KnownAccountDto = { 2 | address: string; 3 | alias: string; 4 | owner?: string; 5 | hasLore: boolean; 6 | lore?: string; 7 | balance: number; 8 | type?: KnownAccountType; 9 | }; 10 | 11 | export type KnownAccountType = 12 | | '' 13 | | 'representative' 14 | | 'exchange' 15 | | 'distribution' 16 | | 'faucet' 17 | | 'explorer' 18 | | 'citizen' 19 | | 'burn'; 20 | -------------------------------------------------------------------------------- /src/app/types/dto/AccountOverviewDto.ts: -------------------------------------------------------------------------------- 1 | export type AccountOverviewDto = { 2 | opened: boolean; 3 | address: string; 4 | balanceRaw?: string; 5 | balance?: number; 6 | receivableRaw: string; 7 | receivable: number; 8 | blockCount: number; 9 | delegatorsCount: number; 10 | representative?: string; 11 | principal: boolean; 12 | 13 | /** I currently only know the weight for opened accounts. */ 14 | weight?: number; 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/services/loading/loading.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class LoadingService { 8 | loading$ = new Subject(); 9 | 10 | searchEvents(): Observable { 11 | return this.loading$; 12 | } 13 | 14 | emitLoad(loading: boolean): void { 15 | this.loading$.next(loading); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/navigation/search-bar/bold-search.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | @Pipe({ 3 | name: 'boldSearch', 4 | }) 5 | export class BoldSearchPipe implements PipeTransform { 6 | transform(item: string, searchText: string): string { 7 | if (searchText) { 8 | const re = new RegExp(searchText, 'gi'); 9 | return item.replace(re, '$&'); 10 | } 11 | return item; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/services/drawer-state/drawer-state.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { DrawerStateService } from './drawer-state.service'; 3 | 4 | describe('DrawerStateService', () => { 5 | beforeEach(() => void TestBed.configureTestingModule({})); 6 | 7 | it('should be created', () => { 8 | const service: DrawerStateService = TestBed.get(DrawerStateService); 9 | void expect(service).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/environments/environment.brpd.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: true, 7 | brpd: true, 8 | //api1: 'http://localhost:3001/banano', 9 | api1: 'https://api.spyglass.pw/banano', 10 | api2: 'https://api.creeper.banano.cc/banano', 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/navigation/search-bar/search-bar.component.scss: -------------------------------------------------------------------------------- 1 | .app-search-bar-input { 2 | border-radius: 8px !important; 3 | } 4 | 5 | .alias-search-menu { 6 | min-width: 0 !important; 7 | min-height: 0 !important; 8 | max-width: unset !important; 9 | max-height: 322px !important; 10 | button { 11 | width: 300px !important; 12 | } 13 | } 14 | 15 | .red { 16 | // background-color: red!important;; 17 | } 18 | 19 | .yellow { 20 | background-color: #fbdd1129 !important; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/types/dto/AccountDistributionStatsDto.ts: -------------------------------------------------------------------------------- 1 | export type AccountDistributionStatsDto = { 2 | number0_0001: number; 3 | number0_001: number; 4 | number0_01: number; 5 | number0_1: number; 6 | number1: number; 7 | number10: number; 8 | number100: number; 9 | number1_000: number; 10 | number10_000: number; 11 | number100_000: number; 12 | number1_000_000: number; 13 | number10_000_000: number; 14 | number100_000_000: number; 15 | totalAccounts: number; 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/pages/account/tabs/transaction/compact-view/compact-view.component.scss: -------------------------------------------------------------------------------- 1 | textarea:focus, 2 | input:focus { 3 | border-radius: 4px; 4 | border: none !important; 5 | outline: 0; 6 | } 7 | .invisible-full-address { 8 | position: absolute; 9 | color: transparent; 10 | z-index: 0; 11 | left: -100px; 12 | font-size: 1px !important; 13 | letter-spacing: 100vw; 14 | bottom: 0px; 15 | } 16 | 17 | .brpd-tab-transaction-list { 18 | position: relative; 19 | overflow: hidden; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/common/animations/animations.ts: -------------------------------------------------------------------------------- 1 | import { style, transition, trigger, animate } from '@angular/animations'; 2 | 3 | /** This is an experimental file, see the networking page for a sample placeholder loading screen. */ 4 | export const faded = trigger('fade', [ 5 | transition(':enter', [ 6 | // using status here for transition 7 | style({ opacity: 0 }), 8 | animate(250, style({ opacity: 1 })), 9 | ]), 10 | /*transition(':leave', [ 11 | animate(250, style({ opacity: 0 })) 12 | ])*/ 13 | ]); 14 | -------------------------------------------------------------------------------- /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 | brpd: false, 8 | api1: 'http://localhost:3001/banano', 9 | api2: 'http://localhost:3001/banano', 10 | // api1: 'https://api.spyglass.pw/banano', 11 | // api2: 'https://api.creeper.banano.cc/banano', 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/common/pipes/color-address.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'colorAddress' }) 4 | export class ColorAddressPipe implements PipeTransform { 5 | transform(address: string): string { 6 | const firstBits = address.substring(0, 12); 7 | const midBits = address.substring(12, 58); 8 | const lastBits = address.substring(58, 64); 9 | 10 | return `${firstBits}${midBits}${lastBits}`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent, AppModule } from './app.module'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(() => { 6 | void TestBed.configureTestingModule({ 7 | imports: [AppModule], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | void expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/monkey-cache/monkey-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | // Saves Monkeys to prevent future duplicate requests. Resets per session. 7 | export class MonkeyCacheService { 8 | monkeyCache: Map = new Map(); 9 | 10 | addCache(address: string, monkey: string): void { 11 | this.monkeyCache.set(address, monkey); 12 | } 13 | 14 | getMonkey(address: string): string | undefined { 15 | return this.monkeyCache.get(address); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pages/vanity/vanity.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AppCommonModule } from '@app/common/app-common.module'; 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { VanityComponent } from '@app/pages/vanity/vanity.component'; 6 | import { RouterModule } from '@angular/router'; 7 | 8 | @NgModule({ 9 | declarations: [VanityComponent], 10 | imports: [CommonModule, AppCommonModule, MatCardModule, RouterModule], 11 | exports: [VanityComponent], 12 | }) 13 | export class VanityModule {} 14 | -------------------------------------------------------------------------------- /src/app/types/dto/HostNodeStatsDto.ts: -------------------------------------------------------------------------------- 1 | export type HostNodeStatsDto = { 2 | addressAsRepresentative?: string; 3 | peerCount: number; 4 | currentBlock: number; 5 | cementedBlocks: number; 6 | uncheckedBlocks: number; 7 | usedMemoryGB?: number; 8 | totalMemoryGB?: number; 9 | ledgerSizeMB?: number; 10 | availableDiskSpaceGB?: number; 11 | monitorUrl?: string; 12 | nodeUptimeSeconds: number; 13 | location?: string; 14 | rpcVersion: string; 15 | storeVersion: string; 16 | protocolVersion: string; 17 | nodeVendor: string; 18 | storeVendor: string; 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/navigation/user-menu/user-menu.component.scss: -------------------------------------------------------------------------------- 1 | .app-user-menu { 2 | margin-right: -8px; 3 | .blui-user-menu-header-icon-wrapper { 4 | display: none !important; 5 | } 6 | .blui-user-menu-avatar { 7 | background-color: rgba(255, 255, 255, 0); 8 | } 9 | } 10 | .cdk-overlay-pane { 11 | .blui-user-menu-header-title-wrapper { 12 | width: 100% !important; 13 | } 14 | .blui-user-menu-header-icon-wrapper { 15 | display: none !important; 16 | } 17 | .mat-toolbar.mat-toolbar-single-row { 18 | background-color: unset !important; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/common/pipes/little-decimal.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'appLittleDecimal' }) 4 | export class LittleDecimalPipe implements PipeTransform { 5 | transform(value: any): string { 6 | const before = String(value).split('.')[0]; 7 | const after = String(value).split('.')[1]; 8 | 9 | let results = ''; 10 | results = `${before}`; 11 | 12 | if (after) { 13 | return `${results}.${after}`; 14 | } 15 | return results; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pages/account/tabs/nfts/nfts-tab.component.scss: -------------------------------------------------------------------------------- 1 | .nfts-tab-container { 2 | .nft-body-container { 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: space-between; 6 | margin-left: -16px; 7 | margin-right: -16px; 8 | &.sm { 9 | justify-content: center; 10 | } 11 | mat-card { 12 | width: 300px; 13 | margin: 32px 16px 32px 16px; 14 | } 15 | } 16 | 17 | @media only screen and (max-width: 1115px) { 18 | .nft-body-container { 19 | justify-content: center; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/account/tabs/insights/insights-tab.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { InsightsDto } from '@app/types/dto'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class InsightsTabService { 8 | private insights: InsightsDto; 9 | 10 | forgetAccount(): void { 11 | this.insights = undefined; 12 | } 13 | 14 | shouldLoadInsights(): boolean { 15 | return !this.insights; 16 | } 17 | 18 | setInsights(insights: InsightsDto): void { 19 | this.insights = insights; 20 | } 21 | 22 | getInsights(): InsightsDto { 23 | return this.insights; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/types/dto/RepScoreDto.ts: -------------------------------------------------------------------------------- 1 | export type RepScoreDto = { 2 | address: string; 3 | alias?: string; 4 | online: boolean; 5 | daysAge: number; 6 | monitorStats?: { 7 | name: string; 8 | hasMinMemoryRequirement: boolean; 9 | hasAboveAvgCementedBlocks: boolean; 10 | hasBelowAvgUncheckedBlocks: boolean; 11 | }; 12 | principal: boolean; 13 | weight: number; 14 | weightPercentage: number; 15 | score: number; 16 | 17 | uptimePercentages?: { 18 | day: number; 19 | week: number; 20 | month: number; 21 | semiAnnual: number; 22 | year: number; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/common/components/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error', 5 | template: ` 6 | 7 | {{ icon }} 8 | 9 | `, 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class ErrorComponent { 13 | @Input() title = 'Something went wrong...'; 14 | @Input() description = 'An error has occurred. If this persists, please contact dev.ptera@gmail.com.'; 15 | @Input() icon = 'error'; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | 'on': 6 | push: 7 | branches: 8 | - master 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: yarn && yarn build 15 | - uses: FirebaseExtended/action-hosting-deploy@v0 16 | with: 17 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_YELLOW_SPYGLASS }}' 19 | channelId: live 20 | projectId: yellow-spyglass 21 | -------------------------------------------------------------------------------- /src/app/pages/representatives/display/monitored-rep-table/monitored-rep-table.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1; 4 | } 5 | 6 | .monitored-reps-table { 7 | height: 400px; 8 | * { 9 | font-size: 14px; 10 | } 11 | } 12 | 13 | .monitored-reps-table { 14 | .representatives-address-cell * { 15 | font-size: 14px; 16 | } 17 | display: block; 18 | max-height: 60vh; 19 | height: 100%; 20 | width: calc(100% - 1px); 21 | max-width: 100%; 22 | overflow: auto !important; 23 | 24 | thead th:nth-child(2) { 25 | padding-left: 16px; 26 | } 27 | tr td:nth-child(2) { 28 | padding-left: 16px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/types/dto/RepresentativeUptimeDto.ts: -------------------------------------------------------------------------------- 1 | export type RepresentativeUptimeDto = { 2 | address: string; 3 | online: boolean; 4 | uptimePercentDay: number; 5 | uptimePercentWeek: number; 6 | uptimePercentMonth: number; 7 | uptimePercentSemiAnnual: number; 8 | uptimePercentYear: number; 9 | creationUnixTimestamp: number; 10 | creationDate: string; 11 | 12 | /* Not provided if representative has never been offline. */ 13 | lastOutage?: LastOutage; 14 | }; 15 | 16 | export type LastOutage = { 17 | offlineUnixTimestamp: number; 18 | onlineUnixTimestamp: number; 19 | onlineDate: string; 20 | offlineDate: string; 21 | durationMinutes: number; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | jobs: 7 | build_and_preview: 8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: yarn && yarn build 13 | - uses: FirebaseExtended/action-hosting-deploy@v0 14 | with: 15 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 16 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_YELLOW_SPYGLASS }}' 17 | projectId: yellow-spyglass 18 | -------------------------------------------------------------------------------- /src/app/types/dto/BlockAtHeightDto.ts: -------------------------------------------------------------------------------- 1 | export type BlockAtHeightDto = { 2 | hash: string; 3 | block_account: string; 4 | amount: string; 5 | amount_decimal: string; 6 | balance: string; 7 | balance_decimal: string; 8 | height: string; 9 | local_timestamp: string; 10 | successor: string; 11 | confirmed: string; 12 | contents: { 13 | type: string; 14 | account: string; 15 | previous: string; 16 | representative: string; 17 | balance: string; 18 | balance_decimal: string; 19 | link: string; 20 | link_as_account: string; 21 | signature: string; 22 | work: string; 23 | }; 24 | subtype: string; 25 | source_account: string; 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/pages/block/block.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BlockComponent } from '@app/pages/block/block.component'; 4 | import { AppCommonModule } from '@app/common/app-common.module'; 5 | import { RouterModule } from '@angular/router'; 6 | import { EmptyStateModule } from '@brightlayer-ui/angular-components'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | 10 | @NgModule({ 11 | declarations: [BlockComponent], 12 | imports: [AppCommonModule, CommonModule, RouterModule, EmptyStateModule, MatButtonModule, MatIconModule], 13 | exports: [BlockComponent], 14 | }) 15 | export class BlockModule {} 16 | -------------------------------------------------------------------------------- /src/assets/icons/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/services/drawer-state/drawer-state.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class DrawerStateService { 7 | private drawerOpen = false; 8 | private selectedItem: string; 9 | 10 | setDrawerOpen(drawerOpen: boolean): void { 11 | document.body.style.overflow = drawerOpen ? 'hidden' : 'auto'; // Fight content scroll on drawer open. 12 | this.drawerOpen = drawerOpen; 13 | } 14 | 15 | getDrawerOpen(): boolean { 16 | return this.drawerOpen; 17 | } 18 | 19 | setSelectedItem(item: string): void { 20 | this.selectedItem = item; 21 | } 22 | 23 | getSelectedItem(): string { 24 | return this.selectedItem; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/common/components/alias/alias.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | } 4 | 5 | .social-media-icon { 6 | width: 14px; 7 | height: 14px; 8 | font-size: 14px; 9 | line-height: 24px; 10 | opacity: 0.75; 11 | } 12 | 13 | .social-media-button { 14 | $size: 24px; 15 | cursor: pointer; 16 | font-size: 16px !important; 17 | height: $size !important; 18 | width: $size !important; 19 | line-height: $size !important; 20 | margin-right: 4px; 21 | min-width: unset !important; 22 | min-height: unset !important; 23 | padding: 0; 24 | } 25 | .social-media-button .mat-button-wrapper { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | } 30 | 31 | .alias { 32 | font-size: 14px; 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Prettier Check 18 | shell: bash 19 | run: yarn && yarn prettier:check 20 | 21 | # - name: Lint Check 22 | # shell: bash 23 | # run: yarn && yarn lint 24 | 25 | # - name: Unit test 26 | # shell: bash 27 | # run: yarn && yarn test:ci 28 | 29 | - name: Build 30 | shell: bash 31 | run: yarn && yarn build 32 | -------------------------------------------------------------------------------- /src/app/pages/account/action-buttons/filter-button/filter-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-filter-button', 5 | encapsulation: ViewEncapsulation.None, 6 | template: ` 7 | 16 | `, 17 | }) 18 | export class FilterButtonComponent { 19 | @Input() showFilter: boolean; 20 | @Output() showFilterChange = new EventEmitter(); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/pages/node-monitor/node-monitor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NodeMonitorComponent } from '@app/pages/node-monitor/node-monitor.component'; 4 | import { AppCommonModule } from '@app/common/app-common.module'; 5 | import { InfoListItemModule } from '@brightlayer-ui/angular-components'; 6 | import { MatListModule } from '@angular/material/list'; 7 | import { MatCardModule } from '@angular/material/card'; 8 | import { RouterModule } from '@angular/router'; 9 | 10 | @NgModule({ 11 | declarations: [NodeMonitorComponent], 12 | imports: [AppCommonModule, CommonModule, InfoListItemModule, MatListModule, MatCardModule, RouterModule], 13 | exports: [NodeMonitorComponent], 14 | }) 15 | export class NodeMonitorModule {} 16 | -------------------------------------------------------------------------------- /src/app/pages/account/tabs/delegators/delegators-tab.component.scss: -------------------------------------------------------------------------------- 1 | .account-delegator-weight { 2 | height: 64px; 3 | display: flex; 4 | width: 100%; 5 | align-items: center; 6 | 7 | .account-delegator-weight-sum { 8 | margin-left: 16px; 9 | margin-right: 8px; 10 | font-size: 16px; 11 | font-weight: 600; 12 | } 13 | 14 | .account-delegator-weight-sum-description { 15 | font-size: 16px; 16 | } 17 | } 18 | 19 | .delegators-table.sm { 20 | border-top-right-radius: 0 !important; 21 | border-top-left-radius: 0 !important; 22 | } 23 | 24 | .delegators-address-cell { 25 | padding-top: 8px; 26 | padding-bottom: 8px; 27 | } 28 | .sm .delegators-address-cell { 29 | padding-top: 16px !important; 30 | padding-bottom: 16px !important; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/types/dto/BlockDto.ts: -------------------------------------------------------------------------------- 1 | export type Block = { 2 | block_account: string; 3 | amount: string; 4 | amount_decimal: string; 5 | balance?: string; 6 | height: string; 7 | local_timestamp: string; 8 | balance_decimal: string; 9 | confirmed: boolean; 10 | successor?: string; 11 | contents: { 12 | type: string; 13 | account: string; 14 | previous: string; 15 | representative: string; 16 | balance: string; 17 | balance_decimal: string; 18 | link: string; 19 | link_as_account: string; 20 | signature: string; 21 | work: string; 22 | }; 23 | subtype: string; 24 | pending?: string; 25 | source_account?: string; 26 | }; 27 | 28 | export type BlockDtoV2 = { 29 | blocks: { 30 | [hash: string]: Block; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/pages/representatives/display/metrics/score-metric/score-metric.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { ViewportService } from '@app/services/viewport/viewport.service'; 3 | 4 | @Component({ 5 | selector: 'rep-score', 6 | template: ` 7 | 12 | 13 | {{ score }} 14 | 15 | / 100 16 | -- 17 | `, 18 | encapsulation: ViewEncapsulation.None, 19 | }) 20 | export class ScoreMetricComponent { 21 | @Input() score: number; 22 | 23 | constructor(public vp: ViewportService) {} 24 | } 25 | -------------------------------------------------------------------------------- /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 { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: { 8 | context( 9 | path: string, 10 | deep?: boolean, 11 | filter?: RegExp 12 | ): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /src/app/pages/bookmarks/bookmarks.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { BookmarksComponent } from '@app/pages/bookmarks/bookmarks.component'; 3 | import { BookmarksModule } from '@app/pages/bookmarks/bookmarks.module'; 4 | 5 | describe('BookmarksComponent', () => { 6 | let component: BookmarksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [BookmarksModule], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(BookmarksComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | void expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "paths": { 7 | "@angular/*": [ "../node_modules/@angular/*" ], 8 | "@app/common/*": ["app/common/*"], 9 | "@app/pages/*": ["app/pages/*"], 10 | "@app/services/*": ["app/services/*"], 11 | "@app/types/*": ["app/types/*"], 12 | }, 13 | "outDir": "./dist/out-tsc", 14 | "sourceMap": true, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "es2020", 21 | "module": "es2020", 22 | "lib": [ 23 | "es2020", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/pages/representatives/representatives.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MonitoredRepDto } from '@app/types/dto'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class RepresentativesService { 8 | openMonitoredRep(rep: MonitoredRepDto): void { 9 | window.open(this.getMonitoredRepUrl(rep), '_blank'); 10 | } 11 | 12 | getMonitoredRepUrl(rep: MonitoredRepDto): string { 13 | let url = rep.ip; 14 | url = url.replace('/api.php', ''); 15 | url = url.replace('/api', ''); 16 | 17 | if (url.includes('http') || url.includes('https')) { 18 | return url; 19 | } 20 | return `http://${url}`; 21 | } 22 | 23 | formatVersion(version: string): string { 24 | if (version) { 25 | return version.toUpperCase().replace('BANANO', ''); 26 | } 27 | return ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/pages/account/action-buttons/qr-button/qr-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { AccountActionsService } from '@app/services/account-actions/account-actions.service'; 3 | 4 | @Component({ 5 | selector: 'app-qr-button', 6 | encapsulation: ViewEncapsulation.None, 7 | template: ` 8 | 17 | `, 18 | }) 19 | export class QrButtonComponent { 20 | @Input() address: string; 21 | 22 | constructor(private readonly _accountActionsService: AccountActionsService) {} 23 | 24 | openDialog(): void { 25 | this._accountActionsService.openAccountQRCode(this.address); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/navigation/navigation.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NavigationModule } from './navigation.module'; 3 | import { NavigationComponent } from './navigation.component'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('NavigationComponent', () => { 7 | let component: NavigationComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [NavigationModule, RouterTestingModule], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NavigationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | void expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/network/network.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NetworkComponent } from './network.component'; 4 | import { NetworkModule } from '@app/pages/network/network.module'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | describe('NetworkComponent', () => { 8 | let component: NetworkComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [NetworkModule, RouterTestingModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(NetworkComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | void expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/pages/wallets/wallets.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { WalletsModule } from '@app/pages/wallets/wallets.module'; 4 | import { WalletsComponent } from '@app/pages/wallets/wallets.component'; 5 | 6 | describe('WalletsComponent', () => { 7 | let component: WalletsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [WalletsModule, HttpClientTestingModule], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WalletsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | void expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/account/qr-dialog/qr-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, Inject, ViewEncapsulation } from '@angular/core'; 2 | 3 | import * as QRCode from 'qrcode'; 4 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 5 | 6 | @Component({ 7 | selector: 'app-qr-dialog', 8 | template: ` 9 |

Address

10 |
11 | 12 |
13 | `, 14 | encapsulation: ViewEncapsulation.None, 15 | }) 16 | export class QrDialogComponent implements AfterViewInit { 17 | constructor(@Inject(MAT_DIALOG_DATA) public data: { address: string }) {} 18 | 19 | ngAfterViewInit(): void { 20 | const canvas = document.getElementById('qr-code-dialog'); 21 | QRCode.toCanvas(canvas, this.data.address, (error) => { 22 | if (error) console.error(error); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/common/pipes/comma.directive.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'appComma' }) 4 | export class CommaPipe implements PipeTransform { 5 | transform(value: any): string { 6 | let adjustedValue = value; 7 | if (!value) { 8 | adjustedValue = 0; 9 | } 10 | 11 | return this.numberWithCommas(this.removeInsigFigs(Number(adjustedValue))); 12 | } 13 | 14 | removeInsigFigs(x: number): number { 15 | if (x > 100_000) { 16 | return Number(x.toFixed(2)); 17 | } 18 | if (x > 1_000) { 19 | return Number(x.toFixed(2)); 20 | } 21 | if (x > 100) { 22 | return Number(x.toFixed(4)); 23 | } 24 | if (x > 1) { 25 | return Number(x.toFixed(5)); 26 | } 27 | return Number(x.toFixed(6)); 28 | } 29 | 30 | numberWithCommas(x: number): string { 31 | return x.toLocaleString('en'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | import { HomeModule } from './home.module'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 7 | 8 | describe('HomeComponent', () => { 9 | let component: HomeComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [HomeModule, RouterTestingModule, HttpClientTestingModule], 15 | }).compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(HomeComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | void expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/services/online-reps/online-reps.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ApiService } from '@app/services/api/api.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | /** 8 | /** Fetches online representatives on initialization. 9 | * Refreshes every minute. 10 | */ 11 | export class OnlineRepsService { 12 | onlineReps: Set = new Set(); 13 | 14 | constructor(private readonly _api: ApiService) { 15 | this._refreshOnlineReps(); 16 | setInterval(() => { 17 | this._refreshOnlineReps(); 18 | }, 60000 * 1); 19 | } 20 | 21 | private _refreshOnlineReps(): void { 22 | this._api 23 | .fetchOnlineRepresentatives() 24 | .then((data) => { 25 | this.onlineReps.clear(); 26 | data.map((rep) => this.onlineReps.add(rep)); 27 | }) 28 | .catch((err) => { 29 | console.error(err); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/types/dto/InsightsDto.ts: -------------------------------------------------------------------------------- 1 | export type InsightsDto = { 2 | blockCount: number; 3 | firstInTxUnixTimestamp: number; 4 | firstInTxHash: string; 5 | firstOutTxUnixTimestamp?: number; 6 | firstOutTxHash?: string; 7 | heightBalances?: Array<{ 8 | balance: number; 9 | height: number; 10 | }>; 11 | lastInTxUnixTimestamp: number; 12 | lastInTxHash: string; 13 | lastOutTxUnixTimestamp?: number; 14 | lastOutTxHash?: string; 15 | maxAmountReceivedHash: string; 16 | maxAmountReceived: number; 17 | maxAmountSentHash: string; 18 | maxAmountSent: number; 19 | maxBalanceHash: string; 20 | maxBalance: number; 21 | mostCommonSenderAddress: string; 22 | mostCommonSenderTxCount: number; 23 | mostCommonRecipientAddress?: string; 24 | mostCommonRecipientTxCount: number; 25 | totalAmountReceived: number; 26 | totalAmountSent: number; 27 | totalTxChange: number; 28 | totalTxReceived: number; 29 | totalTxSent: number; 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/types/dto/MonitoredRepDto.ts: -------------------------------------------------------------------------------- 1 | export type MonitoredRepDto = { 2 | address: string; 3 | online: boolean; 4 | delegatorsCount?: number; 5 | fundedDelegatorsCount?: number; 6 | 7 | /* Optional (populated from node-monitor) */ 8 | cementedBlocks?: number; 9 | confirmationInfo?: { 10 | average: number; 11 | }; 12 | currentBlock?: number; 13 | location?: string; 14 | ip?: string; 15 | name?: string; 16 | nodeUptimeStartup?: number; 17 | representative?: string; 18 | peers?: number; 19 | totalMem?: number; 20 | systemLoad?: number; 21 | uncheckedBlocks?: number; 22 | usedMem?: number; 23 | version?: string; 24 | weight?: number; 25 | 26 | /** Populated these stats from scores. */ 27 | daysAge: number; 28 | principal: boolean; 29 | score: number; 30 | uptimePercentages?: { 31 | day: number; 32 | week: number; 33 | month: number; 34 | semiAnnual: number; 35 | year: number; 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/pages/node-monitor/node-monitor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { NodeMonitorComponent } from '@app/pages/node-monitor/node-monitor.component'; 4 | import { NodeMonitorModule } from '@app/pages/node-monitor/node-monitor.module'; 5 | 6 | describe('NodeMonitorComponent', () => { 7 | let component: NodeMonitorComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [NodeMonitorModule, HttpClientTestingModule], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NodeMonitorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | void expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/representatives/display/large-rep-table/large-rep-table.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1; 4 | } 5 | 6 | .all-reps-table-alias-cell { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | position: relative; 11 | .invisible-full-address { 12 | position: absolute; 13 | color: transparent; 14 | z-index: 0; 15 | left: -100px; 16 | font-size: 1px !important; 17 | letter-spacing: 100vw; 18 | top: 34px; 19 | } 20 | } 21 | 22 | .all-reps-table { 23 | width: 100%; 24 | .blui-list-item-tag-label { 25 | font-size: 0.625rem !important; 26 | } 27 | * { 28 | font-size: 14px; 29 | } 30 | .link { 31 | font-size: 14px !important; 32 | } 33 | th.mat-header-cell:last-of-type, 34 | td.mat-cell:last-of-type, 35 | td.mat-footer-cell:last-of-type { 36 | padding-right: 0 !important; 37 | } 38 | } 39 | 40 | th.mat-sort-header-sorted { 41 | color: black; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/pages/representatives/representatives.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RepresentativesComponent } from './representatives.component'; 3 | import { RepresentativesModule } from '@app/pages/representatives/representatives.module'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | 6 | describe('RepresentativesComponent', () => { 7 | let component: RepresentativesComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [RepresentativesModule, HttpClientTestingModule], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RepresentativesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | void expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/vanity/vanity.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { VanityComponent } from '@app/pages/vanity/vanity.component'; 4 | import { VanityModule } from '@app/pages/vanity/vanity.module'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | describe('VanityComponent', () => { 8 | let component: VanityComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [VanityModule, HttpClientTestingModule, RouterTestingModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(VanityComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | void expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/pages/account/action-buttons/view-button/view-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; 2 | import { AccountActionsService } from '@app/services/account-actions/account-actions.service'; 3 | 4 | @Component({ 5 | selector: 'app-tx-view-button', 6 | encapsulation: ViewEncapsulation.None, 7 | template: ` 8 | 17 | `, 18 | }) 19 | export class ViewButtonComponent { 20 | @Input() isCompact: boolean; 21 | @Output() isCompactChange: EventEmitter = new EventEmitter(); 22 | 23 | constructor(private readonly _accountActionsService: AccountActionsService) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/icons/social-media/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /src/app/common/components/copy-button/copy-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { AccountActionsService } from '@app/services/account-actions/account-actions.service'; 3 | 4 | @Component({ 5 | selector: 'app-copy-button', 6 | encapsulation: ViewEncapsulation.None, 7 | template: ` 8 | 17 | `, 18 | }) 19 | export class CopyButtonComponent { 20 | @Input() data: string; 21 | 22 | constructor(private readonly _accountActionsService: AccountActionsService) {} 23 | 24 | copyToClipboard(): void { 25 | this._accountActionsService.copyDataToClipboard(this.data); 26 | } 27 | 28 | getToolTip(): string { 29 | const isHash = !this.data.includes('ban_'); 30 | return isHash ? 'Copy Hash' : 'Copy Address'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/known-accounts/known-accounts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { KnownAccountsComponent } from '@app/pages/known-accounts/known-accounts.component'; 3 | import { KnownAccountsModule } from '@app/pages/known-accounts/known-accounts.module'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | describe('KnownAccountsComponent', () => { 8 | let component: KnownAccountsComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [KnownAccountsModule, HttpClientTestingModule, RouterTestingModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(KnownAccountsComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | void expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/types/dto/index.ts: -------------------------------------------------------------------------------- 1 | /* Types sent to the client. */ 2 | export * from './AccountBalanceDto'; 3 | export * from './AccountDistributionStatsDto'; 4 | export * from './AccountNFTDto'; 5 | export * from './AccountOverviewDto'; 6 | export * from './AliasDto'; 7 | export * from './BlockDto'; 8 | export * from './BlockAtHeightDto'; 9 | export * from './BNSDomainDto'; 10 | export * from './ConfirmedTransactionDto'; 11 | export * from './DelegatorDto'; 12 | export * from './DiscordResponseDto'; 13 | export * from './ExplorerSummaryDto'; 14 | export * from './HostNodeStatsDto'; 15 | export * from './InsightsDto'; 16 | export * from './KnownAccountDto'; 17 | export * from './NakamotoCoefficientDto'; 18 | export * from './BNSDomainDto'; 19 | export * from './QuorumCoefficientDto'; 20 | export * from './MonitoredRepDto'; 21 | export * from './PeerVersionsDto'; 22 | export * from './PriceDataDto'; 23 | export * from './QuorumDto'; 24 | export * from './ReceivableTransactionDto'; 25 | export * from './RepresentativeDto'; 26 | export * from './RepScoreDto'; 27 | export * from './RepresentativeUptimeDto'; 28 | export * from './SocialMediaAccountAliasDto'; 29 | export * from './SupplyDto'; 30 | -------------------------------------------------------------------------------- /src/app/pages/block/block.component.scss: -------------------------------------------------------------------------------- 1 | .hash-root { 2 | .hash-searched { 3 | display: flex; 4 | align-items: center; 5 | word-break: break-all; 6 | margin-right: 24px; 7 | } 8 | .hash-section { 9 | .before-decimal { 10 | font-size: 16px; 11 | } 12 | .after-decimal { 13 | font-size: 14px; 14 | } 15 | 16 | .alias-row { 17 | display: flex; 18 | justify-content: space-between; 19 | } 20 | 21 | margin: 24px 0; 22 | word-break: break-word; 23 | div { 24 | display: flex; 25 | justify-content: left; 26 | align-items: center; 27 | flex-wrap: wrap; 28 | } 29 | .app-section-title { 30 | margin-right: 16px; 31 | } 32 | .app-section-subtitle { 33 | margin-bottom: 0; 34 | } 35 | .hash-description { 36 | font-size: 14px; 37 | } 38 | } 39 | .original-block-content { 40 | font-size: 0.875rem; 41 | overflow: auto; 42 | padding: 1rem; 43 | word-break: break-all; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/pages/known-accounts/known-accounts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { KnownAccountsComponent } from '@app/pages/known-accounts/known-accounts.component'; 4 | import { MatTableModule } from '@angular/material/table'; 5 | import { MatSortModule } from '@angular/material/sort'; 6 | import { AppCommonModule } from '@app/common/app-common.module'; 7 | import { RouterModule } from '@angular/router'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatChipsModule } from '@angular/material/chips'; 12 | 13 | @NgModule({ 14 | declarations: [KnownAccountsComponent], 15 | imports: [ 16 | AppCommonModule, 17 | BrowserAnimationsModule, 18 | MatButtonModule, 19 | CommonModule, 20 | MatTableModule, 21 | MatSortModule, 22 | RouterModule, 23 | MatIconModule, 24 | MatChipsModule, 25 | ], 26 | exports: [KnownAccountsComponent], 27 | }) 28 | export class KnownAccountsModule {} 29 | -------------------------------------------------------------------------------- /src/app/pages/bookmarks/delete-bookmark-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | import { ViewportService } from '@app/services/viewport/viewport.service'; 4 | 5 | @Component({ 6 | selector: 'delete-bookmark-dialog', 7 | template: ` 8 |
9 |

Remove bookmark?

10 | 11 |
{{ data.alias }}
12 |
13 | 16 | 17 |
18 |
19 |
20 | `, 21 | }) 22 | export class DeleteBookmarkDialog { 23 | constructor(@Inject(MAT_DIALOG_DATA) public data: { alias: string }, public vp: ViewportService) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/app/services/theme/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export type Theme = 'jungle-green' | 'dark'; 4 | 5 | const LIGHT_THEME = 'jungle-green'; 6 | const DARK_THEME = 'dark'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ThemeService { 12 | themeLocalStorageId = 'CREEPER_THEME'; 13 | currentTheme: Theme; 14 | 15 | constructor() { 16 | this.currentTheme = localStorage.getItem(this.themeLocalStorageId) as Theme; 17 | // Use light theme by default. 18 | if (this.currentTheme !== DARK_THEME) { 19 | this.currentTheme = LIGHT_THEME; 20 | } 21 | this.setTheme(this.currentTheme); 22 | } 23 | 24 | isLightMode(): boolean { 25 | return this.currentTheme === LIGHT_THEME; 26 | } 27 | 28 | isDarkMode(): boolean { 29 | return this.currentTheme === DARK_THEME; 30 | } 31 | 32 | setTheme(newTheme: Theme): void { 33 | this.currentTheme = newTheme; 34 | document.body.classList.remove('jungle-green'); 35 | document.body.classList.remove('dark'); 36 | document.body.classList.add(newTheme); 37 | setTimeout(() => { 38 | localStorage.setItem(this.themeLocalStorageId, newTheme); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/services/price/price.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ApiService } from '@app/services/api/api.service'; 3 | import { PriceDataDto } from '@app/types/dto'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | /** Fetches price data & refreshes every 5 minutes. */ 9 | export class PriceService { 10 | priceData: PriceDataDto; 11 | 12 | constructor(private readonly _api: ApiService) { 13 | this._refreshPriceData(); 14 | setInterval(() => { 15 | this._refreshPriceData(); 16 | }, 60000 * 5); 17 | } 18 | 19 | private _refreshPriceData(): void { 20 | this._api 21 | .fetchPriceInfo() 22 | .then((data: PriceDataDto) => { 23 | this.priceData = data; 24 | }) 25 | .catch((err) => { 26 | console.error(err); 27 | }); 28 | } 29 | 30 | priceInBitcoin(ban: number): number { 31 | if (this.priceData) { 32 | return this.priceInUSD(ban) / this.priceData.bitcoinPriceUsd; 33 | } 34 | return 0; 35 | } 36 | 37 | priceInUSD(ban: number): number { 38 | if (this.priceData) { 39 | return this.priceData.bananoPriceUsd * ban; 40 | } 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/pages/representatives/display/metrics/uptime-metric/uptime-metric.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { RepScoreDto } from '@app/types/dto'; 3 | import { ViewportService } from '@app/services/viewport/viewport.service'; 4 | 5 | @Component({ 6 | selector: 'rep-uptime', 7 | template: ` 8 | 9 | 14 | {{ uptimePercentages.month }}% 15 | 16 | · {{ uptimePercentages.week }}% · {{ uptimePercentages.day 18 | }}% 19 | 20 | 21 | `, 22 | encapsulation: ViewEncapsulation.None, 23 | }) 24 | export class UptimeMetricComponent { 25 | @Input() uptimePercentages: RepScoreDto['uptimePercentages']; 26 | 27 | constructor(public vp: ViewportService) {} 28 | } 29 | -------------------------------------------------------------------------------- /src/app/pages/wallets/wallets.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { WalletsComponent } from '@app/pages/wallets/wallets.component'; 4 | import { HighchartsChartModule } from 'highcharts-angular'; 5 | import { MatTableModule } from '@angular/material/table'; 6 | import { MatBadgeModule } from '@angular/material/badge'; 7 | import { AppCommonModule } from '@app/common/app-common.module'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { RouterModule } from '@angular/router'; 10 | import { MatCardModule } from '@angular/material/card'; 11 | import { WalletPaginatorComponent } from '@app/pages/wallets/paginator/paginator.component'; 12 | import { SpacerModule } from '@brightlayer-ui/angular-components'; 13 | import { MatIconModule } from '@angular/material/icon'; 14 | 15 | @NgModule({ 16 | declarations: [WalletsComponent, WalletPaginatorComponent], 17 | imports: [ 18 | AppCommonModule, 19 | CommonModule, 20 | HighchartsChartModule, 21 | MatBadgeModule, 22 | MatButtonModule, 23 | MatIconModule, 24 | MatCardModule, 25 | MatTableModule, 26 | SpacerModule, 27 | RouterModule, 28 | ], 29 | exports: [WalletsComponent], 30 | }) 31 | export class WalletsModule {} 32 | -------------------------------------------------------------------------------- /src/app/services/viewport/viewport.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BreakpointObserver } from '@angular/cdk/layout'; 3 | import { Subject } from 'rxjs'; 4 | 5 | export type Breakpoint = 'sm' | 'md' | undefined; 6 | 7 | // Use this service to get viewport size. 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ViewportService { 12 | breakpoint: Breakpoint; 13 | breakpointSubscription: any; 14 | md: boolean; 15 | sm: boolean; 16 | 17 | vpChange = new Subject(); 18 | 19 | // Viewports are treated as mutually exclusive; a viewpoint cannot be 'sm' and 'md' at the same time. 20 | constructor(private readonly _breakpointObserver: BreakpointObserver) { 21 | this.breakpointSubscription = this._breakpointObserver 22 | .observe(['(max-width: 1280px)', '(max-width: 750px)']) 23 | .subscribe((result) => { 24 | const md = Object.keys(result.breakpoints)[0]; 25 | const sm = Object.keys(result.breakpoints)[1]; 26 | this.sm = result.breakpoints[sm]; 27 | this.md = result.breakpoints[md] && !this.sm; 28 | this.breakpoint = this.sm ? 'sm' : this.md ? 'md' : undefined; 29 | this.vpChange.next(this.breakpoint); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { AppCommonModule } from '@app/common/app-common.module'; 5 | import { HomeComponent } from '@app/pages/home/home.component'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { MatButtonModule } from '@angular/material/button'; 10 | import { NavigationModule } from '../../navigation/navigation.module'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { RouterModule } from '@angular/router'; 14 | 15 | @NgModule({ 16 | declarations: [HomeComponent], 17 | imports: [ 18 | AppCommonModule, 19 | NavigationModule, 20 | BrowserAnimationsModule, 21 | CommonModule, 22 | MatFormFieldModule, 23 | ReactiveFormsModule, 24 | MatInputModule, 25 | MatButtonModule, 26 | MatCardModule, 27 | MatIconModule, 28 | FormsModule, 29 | RouterModule, 30 | ], 31 | exports: [HomeComponent], 32 | }) 33 | export class HomeModule {} 34 | -------------------------------------------------------------------------------- /src/assets/seo/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 |