diff --git a/angular.json b/angular.json index 6f5cf1c9..c2d22a3b 100644 --- a/angular.json +++ b/angular.json @@ -151,5 +151,8 @@ } } }, - "defaultProject": "yellow-spyglass-client" + "defaultProject": "yellow-spyglass-client", + "cli": { + "analytics": false + } } diff --git a/package.json b/package.json index fb5edb61..15f153ec 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@brightlayer-ui/colors-branding": "^3.2.1", "@dev-ptera/nano-node-rpc": "^2.0.2", "@ngneat/until-destroy": "^9.2.3", + "banani-bns": "^0.0.9", "banano-unit-converter": "^0.1.0", "core-js": "^2.5.4", "file-saver": "^2.0.5", diff --git a/src/app/navigation/search-bar/search-bar.component.ts b/src/app/navigation/search-bar/search-bar.component.ts index 2c93ce53..65eab3f1 100644 --- a/src/app/navigation/search-bar/search-bar.component.ts +++ b/src/app/navigation/search-bar/search-bar.component.ts @@ -60,7 +60,7 @@ export let APP_SEARCH_BAR_ID = 0; export class SearchBarComponent implements OnInit, AfterViewInit, AfterViewChecked { @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger; - @Input() placeholder: string = 'Search by Address, Block or Alias'; + @Input() placeholder: string = 'Search by Address, Block, BNS, or Alias'; @Input() toolbarTitle: string; /** This input is used to turn off the autofocus logic. Home page search does not need autofocus, but app-bar search does. */ @@ -127,7 +127,11 @@ export class SearchBarComponent implements OnInit, AfterViewInit, AfterViewCheck return this.invalidSearch.emit(); } - if (this._searchService.isValidAddress(value) || this._searchService.isValidBlock(value)) { + if ( + this._searchService.isValidAddress(value) || + this._searchService.isValidBlock(value) || + this._searchService.isValidBNSDomain(value) + ) { return this._emitSearch(value, controlKey); } diff --git a/src/app/pages/account/account.component.ts b/src/app/pages/account/account.component.ts index 47a6f7f6..a5f6291d 100644 --- a/src/app/pages/account/account.component.ts +++ b/src/app/pages/account/account.component.ts @@ -9,7 +9,7 @@ import { OnlineRepsService } from '@app/services/online-reps/online-reps.service import { NavigationEnd, Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { AliasService } from '@app/services/alias/alias.service'; -import { APP_NAV_ITEMS, hashNavItem } from '../../navigation/nav-items'; +import { APP_NAV_ITEMS, accountNavItem, hashNavItem } from '../../navigation/nav-items'; import { environment } from '../../../environments/environment'; import { DelegatorsTabService } from '@app/pages/account/tabs/delegators/delegators-tab.service'; import { InsightsTabService } from '@app/pages/account/tabs/insights/insights-tab.service'; @@ -64,12 +64,12 @@ export class AccountComponent implements OnDestroy { private readonly _insightsTabService: InsightsTabService, private readonly _delegatorsTabService: DelegatorsTabService ) { - this.routeListener = this._router.events.subscribe((route) => { + this.routeListener = this._router.events.subscribe(async (route) => { if (route instanceof NavigationEnd) { const splitUrl = this._router.url.replace('/history', '').split('/'); const path = splitUrl[splitUrl.length - 1]; const address = path.substring(0, 64); - this._searchAccount(address); + await this._searchAccount(address); } }); @@ -111,13 +111,30 @@ export class AccountComponent implements OnDestroy { void this._router.navigate([`/${hashNavItem.route}/${hash}`], { replaceUrl: true }); } + /** Call this method whenever someone has enters a BNS domain, and it is resolved to a Banano address. */ + private _redirectToAccountPage(account: string): void { + void this._router.navigate([`/${accountNavItem.route}/${account}`], { replaceUrl: true }); + } + /** Given a ban address, searches for account. */ - private _searchAccount(address): void { + private async _searchAccount(address): Promise { if (!address) { return; } if (!address.startsWith('ban_')) { + //if not a banano address, and is in the format ., search in api + if (this._util.isValidBNSDomain(address)) { + //search in api + try { + const parts = address.split('.'); + const domain = await this.apiService.fetchBNSDomain(parts[0], parts[1]); + if (domain.domain?.resolved_address) { + return this._redirectToAccountPage(domain.domain?.resolved_address); + } + } catch (_) {} + } + //if not in that format, or not found in api, assume it is a block hash this._redirectToHashPage(address); } diff --git a/src/app/services/api/api.service.ts b/src/app/services/api/api.service.ts index ef96eab6..6db450e3 100644 --- a/src/app/services/api/api.service.ts +++ b/src/app/services/api/api.service.ts @@ -11,6 +11,7 @@ import { AliasDto, BlockAtHeightDto, BlockDtoV2, + BNSDomainDto, ConfirmedTransactionDto, DelegatorsOverviewDto, DiscordResponseDto, @@ -204,6 +205,17 @@ export class ApiService { .toPromise(); } + /** Fetch/query BNS domain. */ + async fetchBNSDomain(domain_name: string, tld: string): Promise { + await this._hasPingedApi(); + return this._http + .post(`${this.httpApi}/v1/account/bns`, { + domain_name, + tld, + }) + .toPromise(); + } + /** Fetches monitored representatives stats. */ async fetchMonitoredRepresentatives(): Promise { await this._hasPingedApi(); diff --git a/src/app/services/search/search.service.ts b/src/app/services/search/search.service.ts index e81980a1..5db8cf6e 100644 --- a/src/app/services/search/search.service.ts +++ b/src/app/services/search/search.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { UtilService } from '@app/services/util/util.service'; import { Observable, Subject } from 'rxjs'; import { APP_NAV_ITEMS } from '../../navigation/nav-items'; import { Router } from '@angular/router'; @@ -9,17 +10,17 @@ import { Router } from '@angular/router'; export class SearchService { search$ = new Subject<{ search: string; openInNewWindow: boolean }>(); - constructor(router: Router) { + constructor(router: Router, private readonly _utilService: UtilService) { this.searchEvents().subscribe((data: { search: string; openInNewWindow: boolean }) => { if (data.openInNewWindow) { - if (data.search.startsWith('ban_')) { + if (data.search.startsWith('ban_') || this.isValidBNSDomain(data.search)) { const origin = window.location.origin; window.open(`${origin}/${APP_NAV_ITEMS.account.route}/${data.search}`, '_blank'); } else { window.open(`${origin}/${APP_NAV_ITEMS.hash.route}/${data.search}`, '_blank'); } } else { - if (data.search.startsWith('ban_')) { + if (data.search.startsWith('ban_') || this.isValidBNSDomain(data.search)) { void router.navigate([`${APP_NAV_ITEMS.account.route}/${data.search}`]); } else { void router.navigate([`${APP_NAV_ITEMS.hash.route}/${data.search}`]); @@ -47,4 +48,8 @@ export class SearchService { isValidBlock(block: string): boolean { return block && block.length === 64; } + + isValidBNSDomain(bns: string): boolean { + return this._utilService.isValidBNSDomain(bns); + } } diff --git a/src/app/services/util/util.service.ts b/src/app/services/util/util.service.ts index b00a4048..ae78bdca 100644 --- a/src/app/services/util/util.service.ts +++ b/src/app/services/util/util.service.ts @@ -45,4 +45,10 @@ export class UtilService { shortenAddress(addr: string): string { return `${addr.substr(0, 12)}...${addr.substr(addr.length - 6, addr.length)}`; } + + isValidBNSDomain(bns: string): boolean { + const parts = bns.split('.'); + //later, can also check for illegal characters once that is more settled + return parts.length === 2 && parts[0].length <= 32; + } } diff --git a/src/app/types/dto/BNSDomainDto.ts b/src/app/types/dto/BNSDomainDto.ts new file mode 100644 index 00000000..cf790d35 --- /dev/null +++ b/src/app/types/dto/BNSDomainDto.ts @@ -0,0 +1,5 @@ +import { Domain } from 'banani-bns'; + +export type BNSDomainDto = { + domain: Domain; +}; diff --git a/src/app/types/dto/index.ts b/src/app/types/dto/index.ts index 5f52bb26..f817a3e3 100644 --- a/src/app/types/dto/index.ts +++ b/src/app/types/dto/index.ts @@ -6,6 +6,7 @@ export * from './AccountOverviewDto'; export * from './AliasDto'; export * from './BlockDto'; export * from './BlockAtHeightDto'; +export * from './BNSDomainDto'; export * from './ConfirmedTransactionDto'; export * from './DelegatorDto'; export * from './DiscordResponseDto'; @@ -14,6 +15,7 @@ export * from './HostNodeStatsDto'; export * from './InsightsDto'; export * from './KnownAccountDto'; export * from './NakamotoCoefficientDto'; +export * from './BNSDomainDto'; export * from './QuorumCoefficientDto'; export * from './MonitoredRepDto'; export * from './PeerVersionsDto'; diff --git a/tsconfig.json b/tsconfig.json index e2295961..a1f5c860 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,10 +17,10 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2020", "module": "es2020", "lib": [ - "es2018", + "es2020", "dom" ] }, diff --git a/yarn.lock b/yarn.lock index f22a827d..7357d7c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2554,6 +2554,11 @@ axios@^0.21.1: dependencies: follow-redirects "^1.14.0" +b4a@^1.0.1: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + babel-loader@8.2.5: version "8.2.5" resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz" @@ -2611,6 +2616,20 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +banani-bns@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/banani-bns/-/banani-bns-0.0.9.tgz#30eea4c94fbc3f1c9cf6e0282731c7a3edc3d550" + integrity sha512-G9eRXk24ykHLqSCr2LgqNHyUvuzye0VTpNMZdyD4/8HtZRU05Pv1Dbk1lbINkz0ZVf1Y2Jl6dF0ivXMLWBqIRA== + dependencies: + banani "^1.0.4" + +banani@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/banani/-/banani-1.0.4.tgz#62a9102e70cefdbdb7826b0e66dca33ea8a64943" + integrity sha512-/W5cedmDLcrQ9T/Y17Yrk82t1EtJPf3sH14JAzgoHWgqsza2QC0YQallIOc51sY/WxEdnqLSSSoNk9hFkcd3og== + dependencies: + blake2b "^2.1.4" + banano-unit-converter@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/banano-unit-converter/-/banano-unit-converter-0.1.0.tgz" @@ -2652,6 +2671,22 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +blake2b-wasm@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz#9115649111edbbd87eb24ce7c04b427e4e2be5be" + integrity sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w== + dependencies: + b4a "^1.0.1" + nanoassert "^2.0.0" + +blake2b@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.4.tgz#817d278526ddb4cd673bfb1af16d1ad61e393ba3" + integrity sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A== + dependencies: + blake2b-wasm "^2.4.0" + nanoassert "^2.0.0" + body-parser@1.20.0, body-parser@^1.19.0: version "1.20.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" @@ -5878,6 +5913,11 @@ mute-stream@0.0.8: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nanoassert@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-2.0.0.tgz#a05f86de6c7a51618038a620f88878ed1e490c09" + integrity sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA== + nanoid@^3.3.3, nanoid@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz"