diff --git a/src/app/lib/genesisAPI/index.ts b/src/app/lib/genesisAPI/index.ts index a3abf4a57..9647ffa3a 100644 --- a/src/app/lib/genesisAPI/index.ts +++ b/src/app/lib/genesisAPI/index.ts @@ -23,7 +23,7 @@ import queryString from 'query-string'; import urlJoin from 'url-join'; import urlTemplate from 'url-template'; -import { IUIDResponse, ILoginRequest, ILoginResponse, IRowRequest, IRowResponse, IPageResponse, IBlockResponse, IMenuResponse, IContentRequest, IContentResponse, IContentTestRequest, IContentJsonRequest, IContentJsonResponse, ITableResponse, ISegmentRequest, ITablesResponse, IDataRequest, IDataResponse, ISectionsRequest, ISectionsResponse, IHistoryRequest, IHistoryResponse, INotificationsRequest, IParamResponse, IParamsRequest, IParamsResponse, IParamRequest, ITemplateRequest, IContractRequest, IContractResponse, IContractsResponse, ITableRequest, TConfigRequest, ISystemParamsRequest, ISystemParamsResponse, IContentHashRequest, IContentHashResponse, TTxCallRequest, TTxCallResponse, TTxStatusRequest, TTxStatusResponse, ITxStatus, IKeyInfo } from 'genesis/api'; +import { IUIDResponse, ILoginRequest, ILoginResponse, IRowRequest, IRowResponse, IPageResponse, IBlockResponse, IMenuResponse, IContentRequest, IContentResponse, IContentTestRequest, IContentJsonRequest, IContentJsonResponse, ITableResponse, ISegmentRequest, ITablesResponse, IDataRequest, IDataResponse, ISectionsRequest, ISectionsResponse, IHistoryRequest, IHistoryResponse, INotificationsRequest, IParamResponse, IParamsRequest, IParamsResponse, IParamRequest, ITemplateRequest, IContractRequest, IContractResponse, IContractsResponse, ITableRequest, TConfigRequest, ISystemParamsRequest, ISystemParamsResponse, IContentHashRequest, IContentHashResponse, TTxCallRequest, TTxCallResponse, TTxStatusRequest, TTxStatusResponse, ITxStatus, IKeyInfo, IPageValidatorsResponse, IPageValidatorsRequest } from 'genesis/api'; export type TRequestMethod = 'get' | @@ -291,6 +291,10 @@ class GenesisAPI { }) }); + public pageValidatorsCount = this.setEndpoint('get', 'page/validators_count/{name}', { + requestTransformer: request => null + }); + // Transactions private _txSend = this.setSecuredEndpoint<{ [key: string]: Blob }, { hashes: { [key: string]: string } }>('post', 'sendTx'); public txSend = (params: TTxCallRequest) => this._txSend(params) as Promise>; diff --git a/src/app/modules/engine/util/NodeObservable.ts b/src/app/modules/engine/util/NodeObservable.ts index 23b66825f..92a2301ef 100644 --- a/src/app/modules/engine/util/NodeObservable.ts +++ b/src/app/modules/engine/util/NodeObservable.ts @@ -23,8 +23,22 @@ import { Observable } from 'rxjs'; import { IAPIDependency } from 'modules/dependencies'; +function shuffle(array: Array) { + let currentIndex = array.length, temporaryValue, randomIndex; + while (0 !== currentIndex) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + const NodeObservable = (params: { nodes: string[], count: number, timeout?: number, concurrency?: number, api: IAPIDependency }) => - Observable.from(params.nodes) + Observable.from(shuffle(params.nodes.slice())) .distinct() .flatMap(l => { const client = params.api({ apiHost: l }); diff --git a/src/app/modules/sections/epics/renderPageEpic.ts b/src/app/modules/sections/epics/renderPageEpic.ts index 7e1922d9b..2f018d2df 100644 --- a/src/app/modules/sections/epics/renderPageEpic.ts +++ b/src/app/modules/sections/epics/renderPageEpic.ts @@ -23,6 +23,12 @@ import { Epic } from 'modules'; import { Observable } from 'rxjs/Observable'; import { renderPage } from '../actions'; +import NodeObservable from 'modules/engine/util/NodeObservable'; +import keyring from 'lib/keyring'; + +const invalidationError = { + error: 'E_INVALIDATED' +}; const renderPageEpic: Epic = (action$, store, { api }) => action$.ofAction(renderPage.started) .flatMap(action => { @@ -45,27 +51,62 @@ const renderPageEpic: Epic = (action$, store, { api }) => action$.ofAction(rende locale: state.storage.locale }) : Promise.resolve(null) - ])).map(payload => { + ])).flatMap(payload => { const page = payload[0]; const defaultPage = payload[1]; - return renderPage.done({ - params: action.payload, - result: { - defaultMenu: defaultPage && defaultPage.menu !== page.menu && { - name: defaultPage.menu, - content: defaultPage.menutree - }, - menu: { - name: page.menu, - content: page.menutree - }, - page: { - params: action.payload.params, + return NodeObservable({ + nodes: state.storage.fullNodes, + count: Math.min(state.storage.fullNodes.length, 3), + api + }).flatMap(node => client.to(node).pageValidatorsCount({ + name: action.payload.name + + })).map(v => v.validate_count).max().defaultIfEmpty(0).flatMap(count => { + return NodeObservable({ + nodes: state.storage.fullNodes, + count, + concurrency: 3, + api + + }).flatMap(apiHost => Observable.from( + api({ apiHost }).contentHash({ name: action.payload.name, - content: page.tree + ecosystem: state.auth.wallet.access.ecosystem, + walletID: state.auth.wallet.wallet.id, + role: state.auth.wallet.role ? Number(state.auth.wallet.role.id) : null, + locale: state.storage.locale, + params: action.payload.params + + }) + )).catch(e => Observable.empty()).toArray().map(result => { + const contentHash = keyring.hashData(page.plainText.trim()); + + for (let i = 0; i < result.length; i++) { + if (contentHash !== result[i].hash) { + throw invalidationError; + } } - } + + return renderPage.done({ + params: action.payload, + result: { + defaultMenu: defaultPage && defaultPage.menu !== page.menu && { + name: defaultPage.menu, + content: defaultPage.menutree + }, + menu: { + name: page.menu, + content: page.menutree + }, + page: { + params: action.payload.params, + name: action.payload.name, + content: page.tree + } + } + }); + }); }); }).catch(e => diff --git a/src/defs/api.d.ts b/src/defs/api.d.ts index f490d02ba..84b3bea9a 100644 --- a/src/defs/api.d.ts +++ b/src/defs/api.d.ts @@ -201,7 +201,6 @@ declare module 'genesis/api' { tree: TProtypoElement[]; menutree?: TProtypoElement[]; plainText: string; - nodesCount: number; } interface IContentTestRequest { @@ -237,6 +236,14 @@ declare module 'genesis/api' { hash: string; } + interface IPageValidatorsRequest { + name: string + } + + interface IPageValidatorsResponse { + validate_count: number + } + interface ISegmentRequest { offset?: number; limit?: number;