Merge pull request #1224 from mempool/simon/liquid-asset-grouping
Featured assets and asset groups
This commit is contained in:
		
						commit
						32c6ca5e89
					
				| @ -319,7 +319,9 @@ class Server { | |||||||
|     if (Common.isLiquid()) { |     if (Common.isLiquid()) { | ||||||
|       this.app |       this.app | ||||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon) |         .get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon) | ||||||
|  |         .get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets) | ||||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon) |         .get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon) | ||||||
|  |         .get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup) | ||||||
|       ; |       ; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ import bitcoinClient from './api/bitcoin/bitcoin-client'; | |||||||
| import elementsParser from './api/liquid/elements-parser'; | import elementsParser from './api/liquid/elements-parser'; | ||||||
| import icons from './api/liquid/icons'; | import icons from './api/liquid/icons'; | ||||||
| import miningStats from './api/mining'; | import miningStats from './api/mining'; | ||||||
|  | import axios from 'axios'; | ||||||
| 
 | 
 | ||||||
| class Routes { | class Routes { | ||||||
|   constructor() {} |   constructor() {} | ||||||
| @ -855,6 +856,25 @@ class Routes { | |||||||
|       res.status(404).send('Asset icons not found'); |       res.status(404).send('Asset icons not found'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   public async $getAllFeaturedLiquidAssets(req: Request, res: Response) { | ||||||
|  |     try { | ||||||
|  |       const response = await axios.get('https://liquid.network/api/v1/assets/featured', { responseType: 'stream', timeout: 10000 }); | ||||||
|  |       response.data.pipe(res); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).end(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $getAssetGroup(req: Request, res: Response) { | ||||||
|  |     try { | ||||||
|  |       const response = await axios.get('https://liquid.network/api/v1/assets/group/' + parseInt(req.params.id, 10), | ||||||
|  |         { responseType: 'stream', timeout: 10000 }); | ||||||
|  |       response.data.pipe(res); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).end(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new Routes(); | export default new Routes(); | ||||||
|  | |||||||
| @ -115,17 +115,16 @@ describe('Liquid', () => { | |||||||
| 
 | 
 | ||||||
|     describe('assets', () => { |     describe('assets', () => { | ||||||
|       it('shows the assets screen', () => { |       it('shows the assets screen', () => { | ||||||
|         cy.visit(`${basePath}`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.get('#btn-assets'); |  | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('table tr').should('have.length.at.least', 5); |         cy.get('.featuredBox .card').should('have.length.at.least', 5); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('allows searching assets', () => { |       it('allows searching assets', () => { | ||||||
|         cy.visit(`${basePath}/assets`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { |         cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { | ||||||
|           cy.get('table tr').should('have.length', 1); |           cy.get('ngb-typeahead-window').should('have.length', 1); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
| @ -133,7 +132,7 @@ describe('Liquid', () => { | |||||||
|         cy.visit(`${basePath}/assets`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.container-xl input').click().type('Liquid AUD').then(() => { |         cy.get('.container-xl input').click().type('Liquid AUD').then(() => { | ||||||
|           cy.get('table tr td:nth-of-type(1) a').click(); |           cy.get('ngb-typeahead-window:nth-of-type(1) button').click(); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| @ -197,7 +196,7 @@ describe('Liquid', () => { | |||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('shows asset peg in/out and burn transactions', () => { |       it('shows asset peg in/out and burn transactions', () => { | ||||||
|         cy.visit(`${basePath}/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`); |         cy.visit(`${basePath}/assets/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('#table-tx-vout tr').not('.assetBox'); |         cy.get('#table-tx-vout tr').not('.assetBox'); | ||||||
|         cy.get('#table-tx-vin tr').not('.assetBox'); |         cy.get('#table-tx-vin tr').not('.assetBox'); | ||||||
|  | |||||||
| @ -76,14 +76,14 @@ describe('Liquid Testnet', () => { | |||||||
|       it('shows the assets screen', () => { |       it('shows the assets screen', () => { | ||||||
|         cy.visit(`${basePath}/assets`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('table tr').should('have.length.at.least', 5); |         cy.get('.featuredBox .card').should('have.length.at.least', 5); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('allows searching assets', () => { |       it('allows searching assets', () => { | ||||||
|         cy.visit(`${basePath}/assets`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { |         cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => { | ||||||
|           cy.get('table tr').should('have.length', 1); |           cy.get('ngb-typeahead-window').should('have.length', 1); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
| @ -91,7 +91,7 @@ describe('Liquid Testnet', () => { | |||||||
|         cy.visit(`${basePath}/assets`); |         cy.visit(`${basePath}/assets`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.container-xl input').click().type('Liquid CAD').then(() => { |         cy.get('.container-xl input').click().type('Liquid CAD').then(() => { | ||||||
|           cy.get('table tr td:nth-of-type(1) a').click(); |           cy.get('ngb-typeahead-window:nth-of-type(1) button').click(); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| @ -150,7 +150,7 @@ describe('Liquid Testnet', () => { | |||||||
|         cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`); |         cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`); | ||||||
|         cy.get('#table-tx-vout tr:nth-child(2) .amount a').click().then(() => { |         cy.get('#table-tx-vout tr:nth-child(2) .amount a').click().then(() => { | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.url().should('contain', '/asset/38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5'); |           cy.url().should('contain', '/assets/asset/38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5'); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
| @ -162,7 +162,7 @@ describe('Liquid Testnet', () => { | |||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('shows asset peg in/out and burn transactions', () => { |       it('shows asset peg in/out and burn transactions', () => { | ||||||
|         cy.visit(`${basePath}/asset/ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926`); |         cy.visit(`${basePath}/assets/asset/ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926`); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('#table-tx-vout tr').not('.assetBox'); |         cy.get('#table-tx-vout tr').not('.assetBox'); | ||||||
|         cy.get('#table-tx-vin tr').not('.assetBox'); |         cy.get('#table-tx-vin tr').not('.assetBox'); | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ import { TelevisionComponent } from './components/television/television.componen | |||||||
| import { StatisticsComponent } from './components/statistics/statistics.component'; | import { StatisticsComponent } from './components/statistics/statistics.component'; | ||||||
| import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component'; | import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component'; | ||||||
| import { AssetComponent } from './components/asset/asset.component'; | import { AssetComponent } from './components/asset/asset.component'; | ||||||
| import { AssetsComponent } from './assets/assets.component'; | import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; | ||||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||||
| import { DashboardComponent } from './dashboard/dashboard.component'; | import { DashboardComponent } from './dashboard/dashboard.component'; | ||||||
| import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; | import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; | ||||||
| @ -23,6 +23,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component'; | |||||||
| import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; | import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; | ||||||
| import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; | import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; | ||||||
| import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; | import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; | ||||||
|  | import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; | ||||||
|  | import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; | ||||||
|  | import { AssetsComponent } from './components/assets/assets.component'; | ||||||
| 
 | 
 | ||||||
| let routes: Routes = [ | let routes: Routes = [ | ||||||
|   { |   { | ||||||
| @ -343,13 +346,31 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | |||||||
|         path: 'address/:id', |         path: 'address/:id', | ||||||
|         component: AddressComponent |         component: AddressComponent | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         path: 'assets', | ||||||
|  |         component: AssetsNavComponent, | ||||||
|  |         children: [ | ||||||
|  |           { | ||||||
|  |             path: 'featured', | ||||||
|  |             component: AssetsFeaturedComponent, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             path: 'all', | ||||||
|  |             component: AssetsComponent, | ||||||
|  |           }, | ||||||
|           { |           { | ||||||
|             path: 'asset/:id', |             path: 'asset/:id', | ||||||
|             component: AssetComponent |             component: AssetComponent | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|         path: 'assets', |             path: 'group/:id', | ||||||
|         component: AssetsComponent, |             component: AssetGroupComponent | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             path: '**', | ||||||
|  |             redirectTo: 'featured' | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         path: 'docs/api/:type', |         path: 'docs/api/:type', | ||||||
| @ -434,13 +455,31 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | |||||||
|             path: 'address/:id', |             path: 'address/:id', | ||||||
|             component: AddressComponent |             component: AddressComponent | ||||||
|           }, |           }, | ||||||
|  |           { | ||||||
|  |             path: 'assets', | ||||||
|  |             component: AssetsNavComponent, | ||||||
|  |             children: [ | ||||||
|  |               { | ||||||
|  |                 path: 'featured', | ||||||
|  |                 component: AssetsFeaturedComponent, | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 path: 'all', | ||||||
|  |                 component: AssetsComponent, | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 path: 'asset/:id', |                 path: 'asset/:id', | ||||||
|                 component: AssetComponent |                 component: AssetComponent | ||||||
|               }, |               }, | ||||||
|               { |               { | ||||||
|             path: 'assets', |                 path: 'group/:id', | ||||||
|             component: AssetsComponent, |                 component: AssetGroupComponent | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 path: '**', | ||||||
|  |                 redirectTo: 'featured' | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             path: 'docs/api/:type', |             path: 'docs/api/:type', | ||||||
|  | |||||||
| @ -40,7 +40,8 @@ import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph. | |||||||
| import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; | import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component'; | ||||||
| import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; | import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; | ||||||
| import { AssetComponent } from './components/asset/asset.component'; | import { AssetComponent } from './components/asset/asset.component'; | ||||||
| import { AssetsComponent } from './assets/assets.component'; | import { AssetsComponent } from './components/assets/assets.component'; | ||||||
|  | import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; | ||||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||||
| import { MinerComponent } from './components/miner/miner.component'; | import { MinerComponent } from './components/miner/miner.component'; | ||||||
| import { SharedModule } from './shared/shared.module'; | import { SharedModule } from './shared/shared.module'; | ||||||
| @ -64,6 +65,8 @@ import { LanguageService } from './services/language.service'; | |||||||
| import { SponsorComponent } from './components/sponsor/sponsor.component'; | import { SponsorComponent } from './components/sponsor/sponsor.component'; | ||||||
| import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; | import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; | ||||||
| import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; | ||||||
|  | import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; | ||||||
|  | import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
| @ -110,6 +113,9 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; | |||||||
|     PushTransactionComponent, |     PushTransactionComponent, | ||||||
|     DocsComponent, |     DocsComponent, | ||||||
|     ApiDocsNavComponent, |     ApiDocsNavComponent, | ||||||
|  |     AssetsNavComponent, | ||||||
|  |     AssetsFeaturedComponent, | ||||||
|  |     AssetGroupComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule.withServerTransition({ appId: 'serverApp' }), |     BrowserModule.withServerTransition({ appId: 'serverApp' }), | ||||||
|  | |||||||
| @ -1,71 +0,0 @@ | |||||||
| <div class="container-xl"> |  | ||||||
|   <div class="title-asset"> |  | ||||||
|     <h1 i18n="Registered assets page header">Registered assets</h1> |  | ||||||
|   </div> |  | ||||||
|   <div class="clearfix"></div> |  | ||||||
| 
 |  | ||||||
|   <form [formGroup]="searchForm" class="form-inline"> |  | ||||||
|     <div class="input-group mb-2"> |  | ||||||
|       <input style="width: 250px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset"> |  | ||||||
|       <div class="input-group-append"> |  | ||||||
|         <button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </form> |  | ||||||
| 
 |  | ||||||
|   <ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading"> |  | ||||||
|     <table class="table table-borderless table-striped"> |  | ||||||
|       <thead> |  | ||||||
|         <th class="td-name" i18n="Asset name header">Name</th> |  | ||||||
|         <th i18n="Asset ticker header">Ticker</th> |  | ||||||
|         <th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th> |  | ||||||
|         <th i18n="Asset ID header">Asset ID</th> |  | ||||||
|       </thead> |  | ||||||
|       <tbody> |  | ||||||
|         <tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset"> |  | ||||||
|           <td class="td-name"><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td> |  | ||||||
|           <td>{{ asset.ticker }}</td> |  | ||||||
|           <td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td> |  | ||||||
|           <td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td> |  | ||||||
|         </tr> |  | ||||||
|       </tbody> |  | ||||||
|     </table> |  | ||||||
| 
 |  | ||||||
|     <br> |  | ||||||
| 
 |  | ||||||
|     <ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination> |  | ||||||
| 
 |  | ||||||
|   </ng-container> |  | ||||||
| 
 |  | ||||||
|   <ng-template #isLoading> |  | ||||||
| 
 |  | ||||||
|     <table class="table table-borderless table-striped"> |  | ||||||
|       <thead> |  | ||||||
|         <th i18n="Asset name header">Name</th> |  | ||||||
|         <th i18n="Asset ticker header">Ticker</th> |  | ||||||
|         <th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th> |  | ||||||
|         <th i18n="Asset ID header">Asset ID</th> |  | ||||||
|       </thead> |  | ||||||
|       <tbody> |  | ||||||
|         <tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]"> |  | ||||||
|           <td><span class="skeleton-loader"></span></td> |  | ||||||
|           <td><span class="skeleton-loader"></span></td> |  | ||||||
|           <td class="d-none d-md-block"><span class="skeleton-loader"></span></td> |  | ||||||
|           <td><span class="skeleton-loader"></span></td> |  | ||||||
|         </tr> |  | ||||||
|       </tbody> |  | ||||||
|     </table> |  | ||||||
| 
 |  | ||||||
|   </ng-template> |  | ||||||
| 
 |  | ||||||
|   <ng-template [ngIf]="error"> |  | ||||||
|     <div class="text-center"> |  | ||||||
|       <ng-container i18n="Asset data load error">Error loading assets data.</ng-container> |  | ||||||
|       <br> |  | ||||||
|       <i>{{ error.error }}</i> |  | ||||||
|     </div> |  | ||||||
|   </ng-template> |  | ||||||
| 
 |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <br> |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |  | ||||||
| 
 |  | ||||||
| import { AssetsComponent } from './assets.component'; |  | ||||||
| 
 |  | ||||||
| describe('AssetsComponent', () => { |  | ||||||
|   let component: AssetsComponent; |  | ||||||
|   let fixture: ComponentFixture<AssetsComponent>; |  | ||||||
| 
 |  | ||||||
|   beforeEach(async(() => { |  | ||||||
|     TestBed.configureTestingModule({ |  | ||||||
|       declarations: [ AssetsComponent ] |  | ||||||
|     }) |  | ||||||
|     .compileComponents(); |  | ||||||
|   })); |  | ||||||
| 
 |  | ||||||
|   beforeEach(() => { |  | ||||||
|     fixture = TestBed.createComponent(AssetsComponent); |  | ||||||
|     component = fixture.componentInstance; |  | ||||||
|     fixture.detectChanges(); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('should create', () => { |  | ||||||
|     expect(component).toBeTruthy(); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| @ -1,168 +0,0 @@ | |||||||
| import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; |  | ||||||
| import { AssetsService } from '../services/assets.service'; |  | ||||||
| import { environment } from 'src/environments/environment'; |  | ||||||
| import { FormGroup, FormBuilder, Validators } from '@angular/forms'; |  | ||||||
| import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators'; |  | ||||||
| import { ActivatedRoute, Router } from '@angular/router'; |  | ||||||
| import { merge, combineLatest, Observable } from 'rxjs'; |  | ||||||
| import { AssetExtended } from '../interfaces/electrs.interface'; |  | ||||||
| import { SeoService } from '../services/seo.service'; |  | ||||||
| import { StateService } from '../services/state.service'; |  | ||||||
| 
 |  | ||||||
| @Component({ |  | ||||||
|   selector: 'app-assets', |  | ||||||
|   templateUrl: './assets.component.html', |  | ||||||
|   styleUrls: ['./assets.component.scss'], |  | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush |  | ||||||
| }) |  | ||||||
| export class AssetsComponent implements OnInit { |  | ||||||
|   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; |  | ||||||
| 
 |  | ||||||
|   assets: AssetExtended[]; |  | ||||||
|   assetsCache: AssetExtended[]; |  | ||||||
|   searchForm: FormGroup; |  | ||||||
|   assets$: Observable<AssetExtended[]>; |  | ||||||
| 
 |  | ||||||
|   error: any; |  | ||||||
| 
 |  | ||||||
|   page = 1; |  | ||||||
|   itemsPerPage: number; |  | ||||||
|   contentSpace = window.innerHeight - (250 + 200); |  | ||||||
|   fiveItemsPxSize = 250; |  | ||||||
| 
 |  | ||||||
|   constructor( |  | ||||||
|     private assetsService: AssetsService, |  | ||||||
|     private formBuilder: FormBuilder, |  | ||||||
|     private route: ActivatedRoute, |  | ||||||
|     private router: Router, |  | ||||||
|     private seoService: SeoService, |  | ||||||
|     private stateService: StateService, |  | ||||||
|   ) { } |  | ||||||
| 
 |  | ||||||
|   ngOnInit() { |  | ||||||
|     this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); |  | ||||||
|     this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); |  | ||||||
| 
 |  | ||||||
|     this.searchForm = this.formBuilder.group({ |  | ||||||
|       searchText: [{ value: '', disabled: true }, Validators.required] |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     this.assets$ = combineLatest([ |  | ||||||
|       this.assetsService.getAssetsJson$, |  | ||||||
|       this.route.queryParams |  | ||||||
|     ]) |  | ||||||
|     .pipe( |  | ||||||
|       take(1), |  | ||||||
|       mergeMap(([assets, qp]) => { |  | ||||||
|         this.assets = Object.values(assets); |  | ||||||
|         if (this.stateService.network === 'liquid') { |  | ||||||
|           // @ts-ignore
 |  | ||||||
|           this.assets.push({ |  | ||||||
|             name: 'Liquid Bitcoin', |  | ||||||
|             ticker: 'L-BTC', |  | ||||||
|             asset_id: this.nativeAssetId, |  | ||||||
|           }); |  | ||||||
|         } else if (this.stateService.network === 'liquidtestnet') { |  | ||||||
|           // @ts-ignore
 |  | ||||||
|           this.assets.push({ |  | ||||||
|             name: 'Test Liquid Bitcoin', |  | ||||||
|             ticker: 'tL-BTC', |  | ||||||
|             asset_id: this.nativeAssetId, |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name)); |  | ||||||
|         this.assetsCache = this.assets; |  | ||||||
|         this.searchForm.get('searchText').enable(); |  | ||||||
| 
 |  | ||||||
|         if (qp.search) { |  | ||||||
|           this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return merge( |  | ||||||
|           this.searchForm.get('searchText').valueChanges |  | ||||||
|             .pipe( |  | ||||||
|               distinctUntilChanged(), |  | ||||||
|               tap((text) => { |  | ||||||
|                 this.page = 1; |  | ||||||
|                 this.searchTextChanged(text); |  | ||||||
|               }) |  | ||||||
|             ), |  | ||||||
|           this.route.queryParams |  | ||||||
|             .pipe( |  | ||||||
|               filter((queryParams) => { |  | ||||||
|                 const newPage = parseInt(queryParams.page, 10); |  | ||||||
|                 if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) { |  | ||||||
|                   return true; |  | ||||||
|                 } |  | ||||||
|                 return false; |  | ||||||
|               }), |  | ||||||
|               map((queryParams) => { |  | ||||||
|                 if (queryParams.page) { |  | ||||||
|                   const newPage = parseInt(queryParams.page, 10); |  | ||||||
|                   this.page = newPage; |  | ||||||
|                 } else { |  | ||||||
|                   this.page = 1; |  | ||||||
|                 } |  | ||||||
|                 if (this.searchForm.get('searchText').value !== (queryParams.search || '')) { |  | ||||||
|                   this.searchTextChanged(queryParams.search); |  | ||||||
|                 } |  | ||||||
|                 if (queryParams.search) { |  | ||||||
|                   this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false }); |  | ||||||
|                   return queryParams.search; |  | ||||||
|                 } |  | ||||||
|                 return ''; |  | ||||||
|               }) |  | ||||||
|             ), |  | ||||||
|         ); |  | ||||||
|       }), |  | ||||||
|       map((searchText) => { |  | ||||||
|         const start = (this.page - 1) * this.itemsPerPage; |  | ||||||
|         if (searchText.length ) { |  | ||||||
|           const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1 |  | ||||||
|             || (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1); |  | ||||||
|           this.assets = filteredAssets; |  | ||||||
|           return filteredAssets.slice(start, this.itemsPerPage + start); |  | ||||||
|         } else { |  | ||||||
|           this.assets = this.assetsCache; |  | ||||||
|           return this.assets.slice(start, this.itemsPerPage + start); |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   pageChange(page: number) { |  | ||||||
|     const queryParams = { page: page, search: this.searchForm.get('searchText').value }; |  | ||||||
|     if (queryParams.search === '') { |  | ||||||
|       queryParams.search = null; |  | ||||||
|     } |  | ||||||
|     if (queryParams.page === 1) { |  | ||||||
|       queryParams.page = null; |  | ||||||
|     } |  | ||||||
|     this.page = -1; |  | ||||||
|     this.router.navigate([], { |  | ||||||
|       relativeTo: this.route, |  | ||||||
|       queryParams: queryParams, |  | ||||||
|       queryParamsHandling: 'merge', |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   searchTextChanged(text: string) { |  | ||||||
|     const queryParams = { search: text, page: 1 }; |  | ||||||
|     if (queryParams.search === '') { |  | ||||||
|       queryParams.search = null; |  | ||||||
|     } |  | ||||||
|     if (queryParams.page === 1) { |  | ||||||
|       queryParams.page = null; |  | ||||||
|     } |  | ||||||
|     this.router.navigate([], { |  | ||||||
|       relativeTo: this.route, |  | ||||||
|       queryParams: queryParams, |  | ||||||
|       queryParamsHandling: 'merge', |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   trackByAsset(index: number, asset: any) { |  | ||||||
|     return asset.asset_id; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -2,7 +2,7 @@ | |||||||
|   <div class="title-asset"> |   <div class="title-asset"> | ||||||
|     <h1 i18n="asset|Liquid Asset page title">Asset</h1> |     <h1 i18n="asset|Liquid Asset page title">Asset</h1> | ||||||
|     <div class="tx-link"> |     <div class="tx-link"> | ||||||
|       <a [routerLink]="['/asset/' | relativeUrl, assetString]"> |       <a [routerLink]="['/assets/asset/' | relativeUrl, assetString]"> | ||||||
|         <span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span> |         <span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span> | ||||||
|         <span class="d-none d-lg-inline">{{ assetString }}</span> |         <span class="d-none d-lg-inline">{{ assetString }}</span> | ||||||
|       </a> |       </a> | ||||||
| @ -20,7 +20,7 @@ | |||||||
|           <table class="table table-borderless table-striped"> |           <table class="table table-borderless table-striped"> | ||||||
|             <tbody> |             <tbody> | ||||||
|               <tr> |               <tr> | ||||||
|                 <td i18n="asset.name|Liquid Asset name">Name</td> |                 <td i18n="Asset name header">Name</td> | ||||||
|                 <td class="assetName">{{ assetContract[2] }} ({{ assetContract[1] }})</td> |                 <td class="assetName">{{ assetContract[2] }} ({{ assetContract[1] }})</td> | ||||||
|               </tr> |               </tr> | ||||||
|               <tr> |               <tr> | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ export class AssetComponent implements OnInit, OnDestroy { | |||||||
|       .pipe( |       .pipe( | ||||||
|         switchMap((params: ParamMap) => { |         switchMap((params: ParamMap) => { | ||||||
|           this.error = undefined; |           this.error = undefined; | ||||||
|  |           this.imageError = false; | ||||||
|           this.isLoadingAsset = true; |           this.isLoadingAsset = true; | ||||||
|           this.loadedConfirmedTxCount = 0; |           this.loadedConfirmedTxCount = 0; | ||||||
|           this.asset = null; |           this.asset = null; | ||||||
|  | |||||||
| @ -0,0 +1,35 @@ | |||||||
|  | <div *ngIf="group$ | async as group; else loading"> | ||||||
|  |    | ||||||
|  |   <div class="main-title"> | ||||||
|  |     <h2>{{ group.group.name }}</h2> | ||||||
|  | 
 | ||||||
|  |     <div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   <div class="clearfix"></div> | ||||||
|  | 
 | ||||||
|  |   <br> | ||||||
|  | 
 | ||||||
|  |   <div class="featuredBox"> | ||||||
|  |     <div *ngFor="let asset of group.assets"> | ||||||
|  |       <div class="card"> | ||||||
|  |         <a [routerLink]="['/assets/asset' | relativeUrl, asset.asset_id]"> | ||||||
|  |           <img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + asset.asset_id + '/icon'"> | ||||||
|  |         </a> | ||||||
|  |         <div class="title"> | ||||||
|  |           <a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a> | ||||||
|  |         </div> | ||||||
|  |         <div class="ticker">{{ asset.ticker }}</div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <ng-template #loading> | ||||||
|  |   <br> | ||||||
|  |   <div class="text-center loadingGraphs"> | ||||||
|  |     <div class="spinner-border text-light"></div> | ||||||
|  |   </div> | ||||||
|  | </ng-template> | ||||||
| @ -0,0 +1,60 @@ | |||||||
|  | .image { | ||||||
|  |   width: 150px; | ||||||
|  |   float: left; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .main-title { | ||||||
|  |   float: left | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sub-title { | ||||||
|  |   color: grey; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .featuredBox { | ||||||
|  |   display: flex; | ||||||
|  |   flex-flow: row wrap; | ||||||
|  |   justify-content: center; | ||||||
|  |   gap: 27px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card { | ||||||
|  |   background-color: #1d1f31; | ||||||
|  |   width: 200px; | ||||||
|  |   height: 200px; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   @media (max-width: 767.98px) { | ||||||
|  |     width: 150px; | ||||||
|  |     height: 150px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .title { | ||||||
|  |   font-size: 14px; | ||||||
|  |   font-weight: bold; | ||||||
|  |   margin-top: 10px; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sub-title { | ||||||
|  |   color: grey; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assetIcon { | ||||||
|  |   width: 100px; | ||||||
|  |   height: 100px; | ||||||
|  |   @media (max-width: 767.98px) { | ||||||
|  |     width: 50px; | ||||||
|  |     height: 50px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .view-link { | ||||||
|  |   margin-top: 30px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ticker { | ||||||
|  |   color: grey; | ||||||
|  | } | ||||||
| @ -0,0 +1,44 @@ | |||||||
|  | import { Component, OnInit } from '@angular/core'; | ||||||
|  | import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||||
|  | import { combineLatest, Observable } from 'rxjs'; | ||||||
|  | import { map, switchMap } from 'rxjs/operators'; | ||||||
|  | import { ApiService } from 'src/app/services/api.service'; | ||||||
|  | import { AssetsService } from 'src/app/services/assets.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-asset-group', | ||||||
|  |   templateUrl: './asset-group.component.html', | ||||||
|  |   styleUrls: ['./asset-group.component.scss'] | ||||||
|  | }) | ||||||
|  | export class AssetGroupComponent implements OnInit { | ||||||
|  |   group$: Observable<any>; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private route: ActivatedRoute, | ||||||
|  |     private apiService: ApiService, | ||||||
|  |     private assetsService: AssetsService, | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.group$ = this.route.paramMap | ||||||
|  |       .pipe( | ||||||
|  |         switchMap((params: ParamMap) => { | ||||||
|  |           return combineLatest([ | ||||||
|  |             this.assetsService.getAssetsJson$, | ||||||
|  |             this.apiService.getAssetGroup$(params.get('id')), | ||||||
|  |           ]); | ||||||
|  |         }), | ||||||
|  |         map(([assets, group]) => { | ||||||
|  |           const items = []; | ||||||
|  |           // @ts-ignore
 | ||||||
|  |           for (const item of group.assets) { | ||||||
|  |             items.push(assets.objects[item]); | ||||||
|  |           } | ||||||
|  |           return { | ||||||
|  |             group: group, | ||||||
|  |             assets: items | ||||||
|  |           }; | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | <div *ngIf="featuredAssets$ | async as featured; else loading" class="featuredBox"> | ||||||
|  | 
 | ||||||
|  |   <div class="card" *ngFor="let group of featured"> | ||||||
|  |     <ng-template [ngIf]="group.assets" [ngIfElse]="singleAsset"> | ||||||
|  |       <a [routerLink]="['/assets/group' | relativeUrl, group.id]"> | ||||||
|  |         <img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.assets[0] + '/icon'"> | ||||||
|  |       </a> | ||||||
|  |       <div class="title"><a [routerLink]="['/assets/group' | relativeUrl, group.id]">{{ group.name }}</a></div> | ||||||
|  |       <div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div> | ||||||
|  |     </ng-template> | ||||||
|  |     <ng-template #singleAsset> | ||||||
|  |       <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]"> | ||||||
|  |         <img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'"> | ||||||
|  |       </a> | ||||||
|  |       <div class="title"> | ||||||
|  |         <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a> | ||||||
|  |       </div> | ||||||
|  |       <div class="ticker">{{ group.ticker }}</div> | ||||||
|  |     </ng-template> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <ng-template #loading> | ||||||
|  |   <br> | ||||||
|  |   <div class="text-center loadingGraphs"> | ||||||
|  |     <div class="spinner-border text-light"></div> | ||||||
|  |   </div> | ||||||
|  | </ng-template> | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | 
 | ||||||
|  | .featuredBox { | ||||||
|  |   display: flex; | ||||||
|  |   flex-flow: row wrap; | ||||||
|  |   justify-content: center; | ||||||
|  |   gap: 27px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card { | ||||||
|  |   background-color: #1d1f31; | ||||||
|  |   width: 200px; | ||||||
|  |   height: 200px; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   @media (max-width: 767.98px) { | ||||||
|  |     width: 150px; | ||||||
|  |     height: 150px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .title { | ||||||
|  |   font-size: 14px; | ||||||
|  |   font-weight: bold; | ||||||
|  |   margin-top: 10px; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sub-title { | ||||||
|  |   color: grey; | ||||||
|  |   font-size: 12px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assetIcon { | ||||||
|  |   width: 100px; | ||||||
|  |   height: 100px; | ||||||
|  |   @media (max-width: 767.98px) { | ||||||
|  |     width: 50px; | ||||||
|  |     height: 50px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .view-link { | ||||||
|  |   margin-top: 30px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ticker { | ||||||
|  |   color: grey; | ||||||
|  | } | ||||||
| @ -0,0 +1,21 @@ | |||||||
|  | import { Component, OnInit } from '@angular/core'; | ||||||
|  | import { Observable } from 'rxjs'; | ||||||
|  | import { ApiService } from 'src/app/services/api.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-assets-featured', | ||||||
|  |   templateUrl: './assets-featured.component.html', | ||||||
|  |   styleUrls: ['./assets-featured.component.scss'] | ||||||
|  | }) | ||||||
|  | export class AssetsFeaturedComponent implements OnInit { | ||||||
|  |   featuredAssets$: Observable<any>; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private apiService: ApiService, | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.featuredAssets$ = this.apiService.listFeaturedAssets$(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | <div class="container-xl"> | ||||||
|  |   <div class="title-asset"> | ||||||
|  |     <h1 i18n="Assets page header">Assets</h1> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <div class="nav-container"> | ||||||
|  |     <ul class="nav nav-pills"> | ||||||
|  |       <li class="nav-item"> | ||||||
|  |         <a class="nav-link" [routerLink]="['/assets/featured' | relativeUrl]" routerLinkActive="active" i18n>Featured</a> | ||||||
|  |          | ||||||
|  |       </li> | ||||||
|  |       <li class="nav-item"> | ||||||
|  |         <a class="nav-link" [routerLink]="['/assets/all' | relativeUrl]" routerLinkActive="active" i18n>All</a> | ||||||
|  |       </li> | ||||||
|  |     </ul> | ||||||
|  | 
 | ||||||
|  |     <form [formGroup]="searchForm"> | ||||||
|  |       <div class="input-group mb-2"> | ||||||
|  |         <input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" [resultFormatter]="formatterFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset"> | ||||||
|  |         <div class="input-group-append"> | ||||||
|  |           <button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  |   <div class="clearfix"></div> | ||||||
|  | 
 | ||||||
|  |   <router-outlet></router-outlet> | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <br> | ||||||
| @ -0,0 +1,24 @@ | |||||||
|  | ul { | ||||||
|  |   margin-bottom: 20px; | ||||||
|  |   float: left; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | form { | ||||||
|  |   float: right; | ||||||
|  |   width: 300px; | ||||||
|  |   @media (max-width: 767.98px) { | ||||||
|  |     width: 90%; | ||||||
|  |     margin-bottom: 15px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (max-width: 767.98px) { | ||||||
|  |   .nav-container { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     margin: auto; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,95 @@ | |||||||
|  | import { Component, OnInit, ViewChild } from '@angular/core'; | ||||||
|  | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||||
|  | import { Router } from '@angular/router'; | ||||||
|  | import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; | ||||||
|  | import { merge, Observable, of, Subject } from 'rxjs'; | ||||||
|  | import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; | ||||||
|  | import { AssetExtended } from 'src/app/interfaces/electrs.interface'; | ||||||
|  | import { AssetsService } from 'src/app/services/assets.service'; | ||||||
|  | import { SeoService } from 'src/app/services/seo.service'; | ||||||
|  | import { StateService } from 'src/app/services/state.service'; | ||||||
|  | import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; | ||||||
|  | import { environment } from 'src/environments/environment'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-assets-nav', | ||||||
|  |   templateUrl: './assets-nav.component.html', | ||||||
|  |   styleUrls: ['./assets-nav.component.scss'] | ||||||
|  | }) | ||||||
|  | export class AssetsNavComponent implements OnInit { | ||||||
|  |   @ViewChild('instance', {static: true}) instance: NgbTypeahead; | ||||||
|  |   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; | ||||||
|  |   searchForm: FormGroup; | ||||||
|  |   assetsCache: AssetExtended[]; | ||||||
|  | 
 | ||||||
|  |   typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>); | ||||||
|  |   formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker  + ')'; | ||||||
|  |   focus$ = new Subject<string>(); | ||||||
|  |   click$ = new Subject<string>(); | ||||||
|  | 
 | ||||||
|  |   itemsPerPage = 15; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private formBuilder: FormBuilder, | ||||||
|  |     private seoService: SeoService, | ||||||
|  |     private router: Router, | ||||||
|  |     private assetsService: AssetsService, | ||||||
|  |     private stateService: StateService, | ||||||
|  |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); | ||||||
|  |     this.typeaheadSearchFn = this.typeaheadSearch; | ||||||
|  | 
 | ||||||
|  |     this.searchForm = this.formBuilder.group({ | ||||||
|  |       searchText: [{ value: '', disabled: false }, Validators.required] | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   typeaheadSearch = (text$: Observable<string>) => { | ||||||
|  |     const debouncedText$ = text$.pipe( | ||||||
|  |       distinctUntilChanged() | ||||||
|  |     ); | ||||||
|  |     const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); | ||||||
|  |     const inputFocus$ = this.focus$; | ||||||
|  | 
 | ||||||
|  |     return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$) | ||||||
|  |       .pipe( | ||||||
|  |         switchMap((searchText) => { | ||||||
|  |           if (!searchText.length) { | ||||||
|  |             return of([]); | ||||||
|  |           } | ||||||
|  |           return this.assetsService.getAssetsJson$.pipe( | ||||||
|  |             map((assets) => { | ||||||
|  |               if (searchText.length ) { | ||||||
|  |                 const filteredAssets = assets.array.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1 | ||||||
|  |                   || (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1 | ||||||
|  |                   || (asset.entity && asset.entity.domain || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1); | ||||||
|  |                 return filteredAssets.slice(0, this.itemsPerPage); | ||||||
|  |               } else { | ||||||
|  |                 return assets.array.slice(0, this.itemsPerPage); | ||||||
|  |               } | ||||||
|  |             }) | ||||||
|  |           ) | ||||||
|  |         }), | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   itemSelected() { | ||||||
|  |     setTimeout(() => this.search()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   search() { | ||||||
|  |     const searchText = this.searchForm.value.searchText; | ||||||
|  |     this.navigate('/assets/asset/', searchText.asset_id); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   navigate(url: string, searchText: string, extras?: any) { | ||||||
|  |     this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); | ||||||
|  |     this.searchForm.setValue({ | ||||||
|  |       searchText: '', | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								frontend/src/app/components/assets/assets.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/src/app/components/assets/assets.component.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | <ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading"> | ||||||
|  |   <table class="table table-borderless table-striped"> | ||||||
|  |     <thead> | ||||||
|  |       <th class="td-name" i18n="Asset name header">Name</th> | ||||||
|  |       <th i18n="Asset ticker header">Ticker</th> | ||||||
|  |       <th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th> | ||||||
|  |       <th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |       <tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset"> | ||||||
|  |         <td class="td-name"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td> | ||||||
|  |         <td>{{ asset.ticker }}</td> | ||||||
|  |         <td class="d-none d-md-table-cell">{{ asset.entity && asset.entity.domain }}</td> | ||||||
|  |         <td class="d-none d-lg-table-cell"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td> | ||||||
|  |       </tr> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | 
 | ||||||
|  |   <br> | ||||||
|  | 
 | ||||||
|  |   <ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination> | ||||||
|  | 
 | ||||||
|  | </ng-container> | ||||||
|  | 
 | ||||||
|  | <ng-template #isLoading> | ||||||
|  | 
 | ||||||
|  |   <table class="table table-borderless table-striped"> | ||||||
|  |     <thead> | ||||||
|  |       <th i18n="Asset name header">Name</th> | ||||||
|  |       <th i18n="Asset ticker header">Ticker</th> | ||||||
|  |       <th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th> | ||||||
|  |       <th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |       <tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]"> | ||||||
|  |         <td><span class="skeleton-loader"></span></td> | ||||||
|  |         <td><span class="skeleton-loader"></span></td> | ||||||
|  |         <td class="d-none d-md-table-cell"><span class="skeleton-loader"></span></td> | ||||||
|  |         <td class="d-none d-lg-table-cell"><span class="skeleton-loader"></span></td> | ||||||
|  |       </tr> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | 
 | ||||||
|  | </ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template [ngIf]="error"> | ||||||
|  |   <div class="text-center"> | ||||||
|  |     <ng-container i18n="Asset data load error">Error loading assets data.</ng-container> | ||||||
|  |     <br> | ||||||
|  |     <i>{{ error.error }}</i> | ||||||
|  |   </div> | ||||||
|  | </ng-template> | ||||||
							
								
								
									
										99
									
								
								frontend/src/app/components/assets/assets.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								frontend/src/app/components/assets/assets.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | |||||||
|  | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; | ||||||
|  | import { AssetsService } from 'src/app/services/assets.service'; | ||||||
|  | import { environment } from 'src/environments/environment'; | ||||||
|  | import { FormGroup } from '@angular/forms'; | ||||||
|  | import { filter, map, switchMap, take } from 'rxjs/operators'; | ||||||
|  | import { ActivatedRoute, Router } from '@angular/router'; | ||||||
|  | import { combineLatest, Observable } from 'rxjs'; | ||||||
|  | import { AssetExtended } from 'src/app/interfaces/electrs.interface'; | ||||||
|  | import { SeoService } from 'src/app/services/seo.service'; | ||||||
|  | import { StateService } from 'src/app/services/state.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-assets', | ||||||
|  |   templateUrl: './assets.component.html', | ||||||
|  |   styleUrls: ['./assets.component.scss'], | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush | ||||||
|  | }) | ||||||
|  | export class AssetsComponent implements OnInit { | ||||||
|  |   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; | ||||||
|  |   paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 4 : 6; | ||||||
|  |   ellipses = window.matchMedia('(max-width: 670px)').matches ? false : true; | ||||||
|  | 
 | ||||||
|  |   assets: AssetExtended[]; | ||||||
|  |   assetsCache: AssetExtended[]; | ||||||
|  |   searchForm: FormGroup; | ||||||
|  |   assets$: Observable<AssetExtended[]>; | ||||||
|  | 
 | ||||||
|  |   page = 1; | ||||||
|  |   error: any; | ||||||
|  | 
 | ||||||
|  |   itemsPerPage: number; | ||||||
|  |   contentSpace = window.innerHeight - (250 + 200); | ||||||
|  |   fiveItemsPxSize = 250; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private assetsService: AssetsService, | ||||||
|  |     private route: ActivatedRoute, | ||||||
|  |     private router: Router, | ||||||
|  |     private seoService: SeoService, | ||||||
|  |     private stateService: StateService, | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit() { | ||||||
|  |     this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); | ||||||
|  |     this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); | ||||||
|  | 
 | ||||||
|  |     this.assets$ = combineLatest([ | ||||||
|  |       this.assetsService.getAssetsJson$, | ||||||
|  |       this.route.queryParams, | ||||||
|  |     ]) | ||||||
|  |       .pipe( | ||||||
|  |         take(1), | ||||||
|  |         switchMap(([assets, qp]) => { | ||||||
|  |           this.assets = assets.array; | ||||||
|  | 
 | ||||||
|  |           return this.route.queryParams | ||||||
|  |             .pipe( | ||||||
|  |               filter((queryParams) => { | ||||||
|  |                 const newPage = parseInt(queryParams.page, 10); | ||||||
|  |                 if (newPage !== this.page) { | ||||||
|  |                   return true; | ||||||
|  |                 } | ||||||
|  |                 return false; | ||||||
|  |               }), | ||||||
|  |               map((queryParams) => { | ||||||
|  |                 if (queryParams.page) { | ||||||
|  |                   const newPage = parseInt(queryParams.page, 10); | ||||||
|  |                   this.page = newPage; | ||||||
|  |                 } else { | ||||||
|  |                   this.page = 1; | ||||||
|  |                 } | ||||||
|  |                 return ''; | ||||||
|  |               }) | ||||||
|  |             ); | ||||||
|  |         }), | ||||||
|  |         map(() => { | ||||||
|  |           const start = (this.page - 1) * this.itemsPerPage; | ||||||
|  |           return this.assets.slice(start, this.itemsPerPage + start); | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   pageChange(page: number) { | ||||||
|  |     const queryParams = { page: page }; | ||||||
|  |     if (queryParams.page === 1) { | ||||||
|  |       queryParams.page = null; | ||||||
|  |     } | ||||||
|  |     this.page = -1; | ||||||
|  |     this.router.navigate([], { | ||||||
|  |       relativeTo: this.route, | ||||||
|  |       queryParams: queryParams, | ||||||
|  |       queryParamsHandling: 'merge', | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   trackByAsset(index: number, asset: any) { | ||||||
|  |     return asset.asset_id; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -105,11 +105,11 @@ export class SearchFormComponent implements OnInit { | |||||||
|         const matches = this.regexTransaction.exec(searchText); |         const matches = this.regexTransaction.exec(searchText); | ||||||
|         if (this.network === 'liquid' || this.network === 'liquidtestnet') { |         if (this.network === 'liquid' || this.network === 'liquidtestnet') { | ||||||
|           if (this.assets[matches[1]]) { |           if (this.assets[matches[1]]) { | ||||||
|             this.navigate('/asset/', matches[1]); |             this.navigate('/assets/asset/', matches[1]); | ||||||
|           } |           } | ||||||
|           this.electrsApiService.getAsset$(matches[1]) |           this.electrsApiService.getAsset$(matches[1]) | ||||||
|             .subscribe( |             .subscribe( | ||||||
|               () => { this.navigate('/asset/', matches[1]); }, |               () => { this.navigate('/assets/asset/', matches[1]); }, | ||||||
|               () => { |               () => { | ||||||
|                 this.electrsApiService.getBlock$(matches[1]) |                 this.electrsApiService.getBlock$(matches[1]) | ||||||
|                   .subscribe( |                   .subscribe( | ||||||
|  | |||||||
| @ -274,5 +274,5 @@ | |||||||
|   <br /> |   <br /> | ||||||
|   {{ assetsMinimal[item.asset][0] }} |   {{ assetsMinimal[item.asset][0] }} | ||||||
|   <br /> |   <br /> | ||||||
|   <a [routerLink]="['/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a> |   <a [routerLink]="['/assets/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a> | ||||||
| </ng-template> | </ng-template> | ||||||
|  | |||||||
| @ -117,6 +117,14 @@ export class ApiService { | |||||||
|     return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month'); |     return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   listFeaturedAssets$(): Observable<any[]> { | ||||||
|  |     return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/featured'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getAssetGroup$(id: string): Observable<any> { | ||||||
|  |     return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/group/' + id); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   postTransaction$(hexPayload: string): Observable<any> { |   postTransaction$(hexPayload: string): Observable<any> { | ||||||
|     return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); |     return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -3,12 +3,16 @@ import { HttpClient } from '@angular/common/http'; | |||||||
| import { Observable } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
| import { map, shareReplay, switchMap } from 'rxjs/operators'; | import { map, shareReplay, switchMap } from 'rxjs/operators'; | ||||||
| import { StateService } from './state.service'; | import { StateService } from './state.service'; | ||||||
|  | import { environment } from 'src/environments/environment'; | ||||||
|  | import { AssetExtended } from '../interfaces/electrs.interface'; | ||||||
| 
 | 
 | ||||||
| @Injectable({ | @Injectable({ | ||||||
|   providedIn: 'root' |   providedIn: 'root' | ||||||
| }) | }) | ||||||
| export class AssetsService { | export class AssetsService { | ||||||
|   getAssetsJson$: Observable<any>; |   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; | ||||||
|  | 
 | ||||||
|  |   getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>; | ||||||
|   getAssetsMinimalJson$: Observable<any>; |   getAssetsMinimalJson$: Observable<any>; | ||||||
|   getMiningPools$: Observable<any>; |   getMiningPools$: Observable<any>; | ||||||
| 
 | 
 | ||||||
| @ -24,6 +28,30 @@ export class AssetsService { | |||||||
|     this.getAssetsJson$ = this.stateService.networkChanged$ |     this.getAssetsJson$ = this.stateService.networkChanged$ | ||||||
|       .pipe( |       .pipe( | ||||||
|         switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), |         switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), | ||||||
|  |         map((rawAssets) => { | ||||||
|  |           const assets: AssetExtended[] = Object.values(rawAssets); | ||||||
|  |    | ||||||
|  |           if (this.stateService.network === 'liquid') { | ||||||
|  |             // @ts-ignore
 | ||||||
|  |             assets.push({ | ||||||
|  |               name: 'Liquid Bitcoin', | ||||||
|  |               ticker: 'L-BTC', | ||||||
|  |               asset_id: this.nativeAssetId, | ||||||
|  |             }); | ||||||
|  |           } else if (this.stateService.network === 'liquidtestnet') { | ||||||
|  |             // @ts-ignore
 | ||||||
|  |             assets.push({ | ||||||
|  |               name: 'Test Liquid Bitcoin', | ||||||
|  |               ticker: 'tL-BTC', | ||||||
|  |               asset_id: this.nativeAssetId, | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |    | ||||||
|  |           return { | ||||||
|  |             objects: rawAssets, | ||||||
|  |             array: assets.sort((a: any, b: any) => a.name.localeCompare(b.name)), | ||||||
|  |           }; | ||||||
|  |         }), | ||||||
|         shareReplay(1), |         shareReplay(1), | ||||||
|       ); |       ); | ||||||
|     this.getAssetsMinimalJson$ = this.stateService.networkChanged$ |     this.getAssetsMinimalJson$ = this.stateService.networkChanged$ | ||||||
|  | |||||||
| @ -70,3 +70,15 @@ location /api/v1/translators { | |||||||
| 	proxy_hide_header content-security-policy; | 	proxy_hide_header content-security-policy; | ||||||
| 	proxy_hide_header x-frame-options; | 	proxy_hide_header x-frame-options; | ||||||
| } | } | ||||||
|  | location /api/v1/assets { | ||||||
|  | 	proxy_pass $mempoolSpaceServices; | ||||||
|  | 	proxy_cache services; | ||||||
|  | 	proxy_cache_background_update on; | ||||||
|  | 	proxy_cache_use_stale updating; | ||||||
|  | 	proxy_cache_valid 200 10m; | ||||||
|  | 	expires 10m; | ||||||
|  | 	proxy_hide_header onion-location; | ||||||
|  | 	proxy_hide_header strict-transport-security; | ||||||
|  | 	proxy_hide_header content-security-policy; | ||||||
|  | 	proxy_hide_header x-frame-options; | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user