Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to node core with blockchain service #363

Merged
merged 7 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"tsconfig-paths": "^3.12.0",
"typescript": "^5.5.4"
"typescript": "^5.7.3"
},
"resolutions": {
"node-fetch": "2.6.7"
Expand Down
1 change: 1 addition & 0 deletions packages/common-ethereum/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Update header year to 2025 (#362)
- Update `@subql/common` (#363)

## [4.6.1] - 2025-01-28
### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/common-ethereum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"main": "dist/index.js",
"license": "GPL-3.0",
"dependencies": {
"@subql/common": "^5.3.0",
"@subql/common": "^5.4.0",
"@subql/types-ethereum": "workspace:*",
"@typechain/ethers-v5": "^11.1.1",
"@zilliqa-js/crypto": "^3.5.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Update header year to 2025 (#362)
- Update nestjs dependencies (#363)
- Update node core and implement blockchain service (#363)

## [5.4.0] - 2025-01-28
### Changed
Expand Down
16 changes: 8 additions & 8 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"subql-node-ethereum": "./bin/run"
},
"dependencies": {
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/common": "^11.0.8",
"@nestjs/core": "^11.0.8",
"@nestjs/event-emitter": "^2.0.0",
"@nestjs/platform-express": "^9.4.0",
"@nestjs/schedule": "^3.0.1",
"@subql/common": "^5.3.0",
"@nestjs/platform-express": "^11.0.8",
"@nestjs/schedule": "^5.0.1",
"@subql/common": "^5.4.0",
"@subql/common-ethereum": "workspace:*",
"@subql/node-core": "^16.2.0",
"@subql/node-core": "^17.0.0",
"@subql/testing": "^2.2.1",
"@subql/types-ethereum": "workspace:*",
"cacheable-lookup": "6",
Expand All @@ -42,8 +42,8 @@
"@subql/utils": "*"
},
"devDependencies": {
"@nestjs/schematics": "^9.2.0",
"@nestjs/testing": "^9.4.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.8",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.178",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// SPDX-License-Identifier: GPL-3.0

import { EventEmitter2 } from '@nestjs/event-emitter';
import { EthereumApi, EthereumApiService } from '../ethereum';
import { ProjectService } from './project.service';
import { BlockchainService } from './blockchain.service';
import { EthereumApi, EthereumApiService } from './ethereum';

const HTTP_ENDPOINT = 'https://ethereum.rpc.subquery.network/public';

Expand All @@ -17,32 +17,17 @@ const mockApiService = (): EthereumApiService => {
} as any;
};

describe('ProjectService', () => {
let projectService: ProjectService;
describe('BlockchainService', () => {
let blockchainService: BlockchainService;

beforeEach(() => {
const apiService = mockApiService();

projectService = new ProjectService(
null as any,
apiService,
null as any,
null as any,
null as any,
null as any,
null as any,
null as any,
{} as any,
null as any,
null as any,
null as any,
);
blockchainService = new BlockchainService(apiService);
});

it('can get a block timestamps', async () => {
const timestamp = await (projectService as any).getBlockTimestamp(
4_000_000,
);
const timestamp = await blockchainService.getBlockTimestamp(4_000_000);

expect(timestamp).toEqual(new Date('2017-07-09T20:52:47.000Z'));
});
Expand Down
155 changes: 155 additions & 0 deletions packages/node/src/blockchain.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import { Inject } from '@nestjs/common';
import {
EthereumHandlerKind,
EthereumRuntimeDataSourceImpl,
isCustomDs,
isRuntimeDs,
SubqlEthereumDataSource,
} from '@subql/common-ethereum';
import {
DatasourceParams,
Header,
IBlock,
IBlockchainService,
} from '@subql/node-core';
import {
EthereumBlock,
LightEthereumBlock,
SubqlCustomDatasource,
SubqlCustomHandler,
SubqlDatasource,
SubqlMapping,
SubqlRuntimeDatasource,
} from '@subql/types-ethereum';
import { plainToClass } from 'class-transformer';
import { validateSync } from 'class-validator';
import { SubqueryProject } from './configure/SubqueryProject';
import { EthereumApiService } from './ethereum';
import SafeEthProvider from './ethereum/safe-api';
import { calcInterval, ethereumBlockToHeader } from './ethereum/utils.ethereum';
import { BlockContent, getBlockSize } from './indexer/types';
import { IIndexerWorker } from './indexer/worker/worker';

const BLOCK_TIME_VARIANCE = 5000;

const INTERVAL_PERCENT = 0.9;

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version: packageVersion } = require('../package.json');

export class BlockchainService
implements
IBlockchainService<
SubqlDatasource,
SubqlCustomDatasource<string, SubqlMapping<SubqlCustomHandler>>,
SubqueryProject,
SafeEthProvider,
LightEthereumBlock,
EthereumBlock,
IIndexerWorker
>
{
blockHandlerKind = EthereumHandlerKind.Block;
isCustomDs = isCustomDs;
isRuntimeDs = isRuntimeDs;
packageVersion = packageVersion;

constructor(@Inject('APIService') private apiService: EthereumApiService) {}

async fetchBlocks(
blockNums: number[],
): Promise<IBlock<EthereumBlock>[] | IBlock<LightEthereumBlock>[]> {
return this.apiService.fetchBlocks(blockNums);
}

async fetchBlockWorker(
worker: IIndexerWorker,
blockNum: number,
context: { workers: IIndexerWorker[] },
): Promise<Header> {
return worker.fetchBlock(blockNum, 0);
}

getBlockSize(block: IBlock<BlockContent>): number {
return getBlockSize(block.block);
}

async getFinalizedHeader(): Promise<Header> {
const block = await this.apiService.api.getFinalizedBlock();
return ethereumBlockToHeader(block);
}

async getBestHeight(): Promise<number> {
return this.apiService.api.getBestBlockHeight();
}

// eslint-disable-next-line @typescript-eslint/require-await
async getChainInterval(): Promise<number> {
const CHAIN_INTERVAL = calcInterval(this.apiService.api) * INTERVAL_PERCENT;

return Math.min(BLOCK_TIME_VARIANCE, CHAIN_INTERVAL);
}

async getHeaderForHash(hash: string): Promise<Header> {
const block = await this.apiService.api.getBlockByHeightOrHash(hash);
return ethereumBlockToHeader(block);
}

async getHeaderForHeight(height: number): Promise<Header> {
const block = await this.apiService.api.getBlockByHeightOrHash(height);
return ethereumBlockToHeader(block);
}

// eslint-disable-next-line @typescript-eslint/require-await
async getSafeApi(block: BlockContent): Promise<SafeEthProvider> {
return this.apiService.safeApi(block.number);
}

async getBlockTimestamp(height: number): Promise<Date | undefined> {
const block = await this.apiService.unsafeApi.api.getBlock(height);

return new Date(block.timestamp * 1000); // TODO test and make sure its in MS not S
}

// eslint-disable-next-line @typescript-eslint/require-await
async updateDynamicDs(
params: DatasourceParams,
dsObj: SubqlEthereumDataSource | SubqlCustomDatasource,
): Promise<void> {
if (isCustomDs(dsObj)) {
dsObj.processor.options = {
...dsObj.processor.options,
...params.args,
};
// TODO how to retain this functionality
// await this.dsProcessorService.validateCustomDs([dsObj]);
} else if (isRuntimeDs(dsObj)) {
dsObj.options = {
...dsObj.options,
...params.args,
};

const parsedDs = plainToClass(EthereumRuntimeDataSourceImpl, dsObj);

const errors = validateSync(parsedDs, {
whitelist: true,
forbidNonWhitelisted: false,
});
if (errors.length) {
throw new Error(
`Dynamic ds is invalid\n${errors
.map((e) => e.toString())
.join('\n')}`,
);
}
}
// return dsObj;
}

onProjectChange(project: SubqueryProject): Promise<void> | void {
this.apiService.updateBlockFetching();
}
}
14 changes: 11 additions & 3 deletions packages/node/src/ethereum/api.service.ethereum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0

import { INestApplication } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { Test } from '@nestjs/testing';
import {
ConnectionPoolService,
Expand Down Expand Up @@ -47,15 +47,23 @@ const prepareApiService = async (
provide: 'ISubqueryProject',
useFactory: () => testSubqueryProject(endpoint),
},
EthereumApiService,
{
provide: EthereumApiService,
useFactory: EthereumApiService.init,
inject: [
'ISubqueryProject',
ConnectionPoolService,
EventEmitter2,
NodeConfig,
],
},
],
imports: [EventEmitterModule.forRoot()],
}).compile();

const app = module.createNestApplication();
await app.init();
const apiService = app.get(EthereumApiService);
await apiService.init();
return [apiService, app];
};

Expand Down
36 changes: 25 additions & 11 deletions packages/node/src/ethereum/api.service.ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class EthereumApiService extends ApiService<
};
private nodeConfig: EthereumNodeConfig;

constructor(
private constructor(
@Inject('ISubqueryProject') private project: SubqueryProject,
connectionPoolService: ConnectionPoolService<EthereumApiConnection>,
eventEmitter: EventEmitter2,
Expand All @@ -60,31 +60,45 @@ export class EthereumApiService extends ApiService<
this.updateBlockFetching();
}

async init(): Promise<EthereumApiService> {
static async init(
project: SubqueryProject,
connectionPoolService: ConnectionPoolService<EthereumApiConnection>,
eventEmitter: EventEmitter2,
nodeConfig: NodeConfig,
): Promise<EthereumApiService> {
let network: EthereumNetworkConfig;
try {
network = this.project.network;
network = project.network;
} catch (e) {
exitWithError(new Error(`Failed to init api`, { cause: e }), logger);
}

if (this.nodeConfig.primaryNetworkEndpoint) {
const [endpoint, config] = this.nodeConfig.primaryNetworkEndpoint;
if (nodeConfig.primaryNetworkEndpoint) {
const [endpoint, config] = nodeConfig.primaryNetworkEndpoint;
(network.endpoint as Record<string, IEndpointConfig>)[endpoint] = config;
}

await this.createConnections(network, (endpoint, config) =>
const ethNodeConfig = new EthereumNodeConfig(nodeConfig);

const apiService = new EthereumApiService(
project,
connectionPoolService,
eventEmitter,
nodeConfig,
);

await apiService.createConnections(network, (endpoint, config) =>
EthereumApiConnection.create(
endpoint,
this.nodeConfig.blockConfirmations,
this.fetchBlocksBatches,
this.eventEmitter,
this.nodeConfig.unfinalizedBlocks,
ethNodeConfig.blockConfirmations,
apiService.fetchBlocksBatches,
eventEmitter,
nodeConfig.unfinalizedBlocks,
config,
),
);

return this;
return apiService;
}

protected metadataMismatchError(
Expand Down
Loading
Loading