diff --git a/package.json b/package.json index dfd1854..39c2f33 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "main": "cjs/index.js", "types": "cjs/index.d.ts", "scripts": { - "testCommand": "jest --detect-open-handles", + "testCommand": "jest", "prepublishOnly": "npm run build", "build": "tsc -p ./tsconfig-cjs.json", "test": "node-state -e -n -m typescript -o ./test/_state.ts -r" @@ -45,8 +45,8 @@ "@types/jest": "^30.0.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", - "@waves/node-state": "^0.2.0-snapshot.1", - "@waves/waves-transactions": "4.4.0-snapshot.2", + "@waves/node-state": "^0.2.0-snapshot.2", + "@waves/waves-transactions": "4.4.0-snapshot.3", "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", @@ -60,6 +60,7 @@ }, "overrides": { "test-exclude": "^8.0.0", - "glob": "^13.0.6" + "glob": "^13.0.6", + "flatted": "^3.4.2" } } diff --git a/src/api-node/finality/index.ts b/src/api-node/finality/index.ts new file mode 100644 index 0000000..6df36e3 --- /dev/null +++ b/src/api-node/finality/index.ts @@ -0,0 +1,110 @@ +import request from '../../tools/request'; +import {TLong} from '../../interface'; +import {fetchActivationStatus} from '../activation'; +import {fetchHeight, IBlockHeader} from '../blocks'; + + +/** + * GET /blocks/headers/finalized + * Last finalized block header + * @param base + * @param options + */ +export function fetchFinalized(base: string, options: RequestInit = Object.create(null)): Promise { + return request({ + base, + url: `/blocks/headers/finalized`, + options + }); +} + +/** + * GET last finalized block height + * @param base + * @param options + */ +export function fetchFinalizedHeight(base: string, options: RequestInit = Object.create(null)): Promise<{ height: number }> { + return request({ + base, + url: `/blocks/height/finalized`, + options + }) +} + +/** + * GET finalized block height at + * @param base + * @param height + * @param options + */ +export function fetchFinalizedHeightAt(base: string, height: number, options: RequestInit = Object.create(null)): Promise<{ height: number }> { + return request({ + base, + url: `/blocks/finalized/at/${height}`, + options + }) +} + +/** + * GET /generators/at/{height} + * Committed generators list at height + * @param base + * @param height + * @param options + */ +export function fetchCommittedGeneratorsAt(base: string, height: number, options: RequestInit = Object.create(null)): Promise> { + return request({ + base, + url: `/generators/at/${height}`, + options + }); +} + +/** + * Get committed generator index for provided address. + * Returns index from 0, or -1 when address is missing in the list. + * @param base + * @param height + * @param address + * @param options + */ +export function fetchCommittedGeneratorIndex(base: string, height: number, address: string, options: RequestInit = Object.create(null)): Promise { + return fetchCommittedGeneratorsAt(base, height, options).then((list) => { + const index = list.findIndex((item) => item.address === address); + return index >= 0 ? index : -1; + }); +} + +export function fetchFinalityInfo(base: string, options: RequestInit = Object.create(null)): Promise { + return request({ + base, + url: '/blockchain/finality', + options + }) +} + +export interface IGenerationPeriod { + start: number; + end: number; +} + +export interface IFinalityInfo { + height: number; + finalizedHeight: number; + currentGenerationPeriod?: IGenerationPeriod; + currentGenerators: ICommittedGenerator[]; + nextGenerationPeriod?: IGenerationPeriod; + nextGenerators: INextCommittedGenerator[]; +} + +export interface ICommittedGenerator { + address: string; + balance: TLong; + transactionId: string; + conflictHeight?: number; +} + +export interface INextCommittedGenerator { + address: string; + transactionId: string; +} diff --git a/src/api-node/finalization/index.ts b/src/api-node/finalization/index.ts deleted file mode 100644 index 6bcc19c..0000000 --- a/src/api-node/finalization/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import request from '../../tools/request'; -import { TLong } from '../../interface'; -import { fetchActivationStatus } from '../activation'; -import {fetchHeight, IBlockHeader} from '../blocks'; - - -/** - * GET /blocks/headers/finalized - * Last finalized block header - * @param base - */ -export function fetchFinalized(base: string, options: RequestInit = Object.create(null)): Promise { - return request({ - base, - url: `/blocks/headers/finalized`, - options - }); -} - -/** - * GET last finalized block height - * @param base - */ -export function fetchFinalizedHeight(base: string): Promise<{ height: number }> { - return request({ - base, - url: `/blocks/height/finalized` - }) -} - -/** - * GET finalized block height at - * @param base - * @param height - */ -export function fetchFinalizedHeightAt(base: string, height: number): Promise<{ height: number }> { - return request({ - base, - url: `/blocks/finalized/at/${height}` - }) -} - -/** - * GET /generators/at/{height} - * Committed generators list at height - * @param base - * @param height - */ -export function fetchComittedGeneratorsAt(base: string, height: number, options: RequestInit = Object.create(null)): Promise> { - return request({ - base, - url: `/generators/at/${height}`, - options - }); -} - -/** - * Get committed generator index for provided address. - * Returns index from 0, or -1 when address is missing in the list. - * @param base - * @param height - * @param address - */ -export function fetchCommittedGeneratorIndex(base: string, height: number, address: string, options: RequestInit = Object.create(null)): Promise { - return fetchComittedGeneratorsAt(base, height, options).then((list) => { - const index = list.findIndex((item) => item.address === address); - return index >= 0 ? index : -1; - }); -} - -/** - * Calculates commitment period boundaries depends on feature 25 activation. - * @param base - * @param periodLength - */ -export function fetchCommitmentPeriodHeights(base: string, periodLength: number = 10000): Promise { - return Promise.all([ - fetchActivationStatus(base), - fetchHeight(base) - ]).then(([activationStatus, heightStatus]) => { - const feature25 = activationStatus.features.find((feature) => - feature.id === 25 && - feature.blockchainStatus === 'ACTIVATED' - ); - - if (!feature25) { - throw new Error('Finalization voting is not activated'); - } - if (typeof feature25.activationHeight !== 'number') { - throw new Error('Feature 25 activation height is unavailable'); - } - - const featureActivationHeight = feature25.activationHeight === 0 ? 1 : feature25.activationHeight; - let nextPeriodStart = featureActivationHeight + periodLength; - while (heightStatus.height >= nextPeriodStart) { - nextPeriodStart += periodLength; - } - - return { - currentPeriodStart: Math.max(nextPeriodStart - periodLength, 1), - nextPeriodStart - }; - }); -} - -export interface ICommittedGenerator { - address: string; - balance: TLong; - transactionId: string; - conflictHeight?: number; -} - -export interface ICommitmentPeriodHeights { - currentPeriodStart: number; - nextPeriodStart: number; -} diff --git a/src/create.ts b/src/create.ts index 4c85ef1..f625533 100644 --- a/src/create.ts +++ b/src/create.ts @@ -8,7 +8,7 @@ import * as utilsModule from './api-node/utils'; import * as debugModule from './api-node/debug'; import * as aliasModule from './api-node/alias'; import * as activationModule from './api-node/activation'; -import * as finalizationModule from './api-node/finalization'; +import * as finalityModule from './api-node/finality'; import * as nodeModule from './api-node/node'; import * as assetsModule from './api-node/assets'; import * as ethModule from './api-node/eth'; @@ -49,7 +49,7 @@ export function create(base: string) { const debug: TWrapRecord = wrapRecord(base, debugModule); const alias: TWrapRecord = wrapRecord(base, aliasModule); const activation: TWrapRecord = wrapRecord(base, activationModule); - const finalization: TWrapRecord = wrapRecord(base, finalizationModule); + const finality: TWrapRecord = wrapRecord(base, finalityModule); const node: TWrapRecord = wrapRecord(base, nodeModule); const assets: TWrapRecord = wrapRecord(base, assetsModule); const eth: TWrapRecord = wrapRecord(base, ethModule); @@ -92,7 +92,7 @@ export function create(base: string) { debug, alias, activation, - finalization, + finality, node, assets, eth diff --git a/src/nodeInteraction.ts b/src/nodeInteraction.ts index 684976e..deba4cd 100644 --- a/src/nodeInteraction.ts +++ b/src/nodeInteraction.ts @@ -33,7 +33,7 @@ export interface INodeRequestOptions { const DEFAULT_NODE_REQUEST_OPTIONS: Required = { timeout: 120000, - apiBase: 'https://nodes.wavesplatform.com', + apiBase: 'https://nodes.wavesnodes.com', }; export const currentHeight = async (apiBase: string): Promise => diff --git a/test/api-node/finality.spec.ts b/test/api-node/finality.spec.ts new file mode 100644 index 0000000..82a584d --- /dev/null +++ b/test/api-node/finality.spec.ts @@ -0,0 +1,24 @@ +import {MASTER_ACCOUNT, NODE_URL} from '../_state'; +import {create} from '../../src'; + +import {commitToGeneration} from '@waves/waves-transactions'; + +const api = create(NODE_URL); + +it('Finality info', async () => { + const finalityInfo = await api.finality.fetchFinalityInfo() + + const tx = await api.transactions.broadcast(commitToGeneration({ + chainId: 82, + generationPeriodStart: finalityInfo.nextGenerationPeriod!.start + }, MASTER_ACCOUNT.SEED)) + + await api.tools.transactions.wait(tx, {confirmations: 1}) + + const newFinality = await api.finality.fetchFinalityInfo() + expect(newFinality.nextGenerators).toContainEqual({ + transactionId: tx.id, + address: MASTER_ACCOUNT.ADDRESS + }) + +}, 10000)