diff --git a/packages/common-stellar/package.json b/packages/common-stellar/package.json index eb38d1e1..3b6b299e 100644 --- a/packages/common-stellar/package.json +++ b/packages/common-stellar/package.json @@ -14,7 +14,7 @@ "main": "dist/index.js", "license": "GPL-3.0", "dependencies": { - "@stellar/stellar-sdk": "^14.1.0", + "@stellar/stellar-sdk": "^14.3.3", "@subql/common": "^5.7.0", "@subql/types-stellar": "workspace:*", "js-yaml": "^4.1.1", diff --git a/packages/common-stellar/src/project/models.ts b/packages/common-stellar/src/project/models.ts index f2873ee6..090d440b 100644 --- a/packages/common-stellar/src/project/models.ts +++ b/packages/common-stellar/src/project/models.ts @@ -1,13 +1,13 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import type {Horizon} from '@stellar/stellar-sdk'; +import type {xdr} from '@stellar/stellar-sdk'; import {BaseDataSource, forbidNonWhitelisted, ProcessorImpl} from '@subql/common'; import {Processor, FileReference} from '@subql/types-core'; import { StellarHandlerKind, StellarDatasourceKind, - SorobanEventFilter, + StellarEventFilter, SubqlCustomHandler, SubqlMapping, SubqlHandler, @@ -17,13 +17,10 @@ import { StellarBlockFilter, StellarTransactionFilter, StellarOperationFilter, - StellarEffectFilter, SubqlBlockHandler, SubqlTransactionHandler, SubqlOperationHandler, - SubqlEffectHandler, SubqlEventHandler, - SubqlSorobanTransactionHandler, } from '@subql/types-stellar'; import {plainToClass, Transform, Type} from 'class-transformer'; import {IsArray, IsEnum, IsInt, IsOptional, IsString, IsObject, ValidateNested} from 'class-validator'; @@ -46,23 +43,13 @@ export class TransactionFilter implements StellarTransactionFilter { export class OperationFilter implements StellarOperationFilter { @IsOptional() - type!: Horizon.HorizonApi.OperationResponseType; + type!: xdr.OperationType['name']; @IsOptional() @IsString() sourceAccount?: string; } -export class EffectFilter implements StellarEffectFilter { - @IsOptional() - @IsString() - type?: string; - - @IsOptional() - @IsString() - account?: string; -} - export class BlockHandler implements SubqlBlockHandler { @IsObject() @IsOptional() @@ -86,18 +73,6 @@ export class TransactionHandler implements SubqlTransactionHandler { handler!: string; } -export class SorobanTransactionHandler implements SubqlSorobanTransactionHandler { - @forbidNonWhitelisted({account: ''}) - @IsObject() - @IsOptional() - @Type(() => TransactionFilter) - filter?: TransactionFilter; - @IsEnum(StellarHandlerKind, {groups: [StellarHandlerKind.SorobanTransaction]}) - kind!: StellarHandlerKind.SorobanTransaction; - @IsString() - handler!: string; -} - export class OperationHandler implements SubqlOperationHandler { @forbidNonWhitelisted({type: '', sourceAccount: ''}) @IsObject() @@ -110,19 +85,7 @@ export class OperationHandler implements SubqlOperationHandler { handler!: string; } -export class EffectHandler implements SubqlEffectHandler { - @forbidNonWhitelisted({type: '', account: ''}) - @IsObject() - @IsOptional() - @Type(() => EffectFilter) - filter?: EffectFilter; - @IsEnum(StellarHandlerKind, {groups: [StellarHandlerKind.Effects]}) - kind!: StellarHandlerKind.Effects; - @IsString() - handler!: string; -} - -export class EventFilter implements SorobanEventFilter { +export class EventFilter implements StellarEventFilter { @IsOptional() @IsString() contractId?: string; @@ -136,7 +99,7 @@ export class EventHandler implements SubqlEventHandler { @IsOptional() @ValidateNested() @Type(() => EventFilter) - filter?: SorobanEventFilter; + filter?: StellarEventFilter; @IsEnum(StellarHandlerKind, {groups: [StellarHandlerKind.Event]}) kind!: StellarHandlerKind.Event; @IsString() @@ -162,12 +125,8 @@ export class StellarMapping implements SubqlMapping { return plainToClass(BlockHandler, handler); case StellarHandlerKind.Transaction: return plainToClass(TransactionHandler, handler); - case StellarHandlerKind.SorobanTransaction: - return plainToClass(SorobanTransactionHandler, handler); case StellarHandlerKind.Operation: return plainToClass(OperationHandler, handler); - case StellarHandlerKind.Effects: - return plainToClass(EffectHandler, handler); case StellarHandlerKind.Event: return plainToClass(EventHandler, handler); default: diff --git a/packages/common-stellar/src/project/project.spec.ts b/packages/common-stellar/src/project/project.spec.ts index 87a1246e..88605f38 100644 --- a/packages/common-stellar/src/project/project.spec.ts +++ b/packages/common-stellar/src/project/project.spec.ts @@ -64,21 +64,21 @@ describe('project.yaml', () => { expect(deploymentString).toContain('account_merge'); }); - it('get v1.0.0 deployment mapping filter - effect', () => { - const manifestVersioned = loadStellarProjectManifest(path.join(projectsDir, 'project_1.0.0.yaml')); + // it('get v1.0.0 deployment mapping filter - effect', () => { + // const manifestVersioned = loadStellarProjectManifest(path.join(projectsDir, 'project_1.0.0.yaml')); - const deployment = manifestVersioned.asV1_0_0.deployment; - const filter = deployment.dataSources[0].mapping.handlers[2].filter; - const deploymentString = manifestVersioned.toDeployment(); - expect(filter).not.toBeNull(); - expect(deploymentString).toContain('account_credited'); - }); + // const deployment = manifestVersioned.asV1_0_0.deployment; + // const filter = deployment.dataSources[0].mapping.handlers[2].filter; + // const deploymentString = manifestVersioned.toDeployment(); + // expect(filter).not.toBeNull(); + // expect(deploymentString).toContain('account_credited'); + // }); it('get v1.0.0 deployment mapping filter - events', () => { const manifestVersioned = loadStellarProjectManifest(path.join(projectsDir, 'project_1.0.0.yaml')); const deployment = manifestVersioned.asV1_0_0.deployment; - const filter = deployment.dataSources[0].mapping.handlers[3].filter; + const filter = deployment.dataSources[0].mapping.handlers[2].filter; const deploymentString = manifestVersioned.toDeployment(); expect(filter).not.toBeNull(); expect(deploymentString).toContain('COUNTER'); diff --git a/packages/common-stellar/src/project/types.ts b/packages/common-stellar/src/project/types.ts index dc93ceb2..c32f57b8 100644 --- a/packages/common-stellar/src/project/types.ts +++ b/packages/common-stellar/src/project/types.ts @@ -16,7 +16,6 @@ export { StellarBlockFilter, StellarTransactionFilter, StellarOperationFilter, - StellarEffectFilter, SubqlDatasourceProcessor, SubqlHandlerFilter, StellarDatasourceKind, @@ -27,5 +26,4 @@ export type IStellarProjectManifest = IProjectManifest; export interface StellarProjectNetworkConfig extends ProjectNetworkConfig { chainId: string; - sorobanEndpoint: string; } diff --git a/packages/common-stellar/src/project/utils.ts b/packages/common-stellar/src/project/utils.ts index d83c3832..fe45776c 100644 --- a/packages/common-stellar/src/project/utils.ts +++ b/packages/common-stellar/src/project/utils.ts @@ -13,8 +13,6 @@ import { SubqlMapping, } from '@subql/types-stellar'; -type DefaultFilter = Record; - export function isBlockHandlerProcessor, B>( hp: SecondLayerHandlerProcessorArray ): hp is SecondLayerHandlerProcessor { @@ -27,24 +25,12 @@ export function isTransactionHandlerProcessor, return hp.baseHandlerKind === StellarHandlerKind.Transaction; } -export function isSorobanTransactionHandlerProcessor, T>( - hp: SecondLayerHandlerProcessorArray -): hp is SecondLayerHandlerProcessor { - return hp.baseHandlerKind === StellarHandlerKind.SorobanTransaction; -} - export function isOperationHandlerProcessor, O>( hp: SecondLayerHandlerProcessorArray ): hp is SecondLayerHandlerProcessor { return hp.baseHandlerKind === StellarHandlerKind.Operation; } -export function isEffectHandlerProcessor, E>( - hp: SecondLayerHandlerProcessorArray -): hp is SecondLayerHandlerProcessor { - return hp.baseHandlerKind === StellarHandlerKind.Effects; -} - export function isEventHandlerProcessor, E>( hp: SecondLayerHandlerProcessorArray ): hp is SecondLayerHandlerProcessor { diff --git a/packages/common-stellar/test/project_1.0.0.yaml b/packages/common-stellar/test/project_1.0.0.yaml index b309d2ab..3db7e434 100644 --- a/packages/common-stellar/test/project_1.0.0.yaml +++ b/packages/common-stellar/test/project_1.0.0.yaml @@ -34,11 +34,6 @@ dataSources: filter: sourceAccount: 'GAKNXHJ5PCZYFIBNBWB4RCQHH6GDEO7Z334N74BOQUQCHKOURQEPMXCH' type: 'account_merge' - - handler: handleEffect - kind: stellar/EffectHandler - filter: - type: 'account_credited' - account: 'GAKNXHJ5PCZYFIBNBWB4RCQHH6GDEO7Z334N74BOQUQCHKOURQEPMXCH' - handler: handleEvent kind: soroban/EventHandler filter: diff --git a/packages/node/package.json b/packages/node/package.json index b462dd75..90e6a206 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -24,7 +24,7 @@ "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^11.0.11", "@nestjs/schedule": "^5.0.1", - "@stellar/stellar-sdk": "^14.1.0", + "@stellar/stellar-sdk": "^14.3.3", "@subql/common": "^5.7.0", "@subql/common-stellar": "workspace:*", "@subql/node-core": "^18.2.0", diff --git a/packages/node/src/blockchain.service.spec.ts b/packages/node/src/blockchain.service.spec.ts index 1f19aa4b..0de051c4 100644 --- a/packages/node/src/blockchain.service.spec.ts +++ b/packages/node/src/blockchain.service.spec.ts @@ -1,39 +1,38 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { BlockchainService } from './blockchain.service'; -import { StellarApi, StellarApiService } from './stellar'; -import { SorobanServer } from './stellar/soroban.server'; +import {BlockchainService} from './blockchain.service'; +import {StellarApi, StellarApiService} from './stellar'; -const HTTP_ENDPOINT = 'https://horizon-futurenet.stellar.org'; -const SOROBAN_ENDPOINT = 'https://rpc-futurenet.stellar.org'; +const SOROBAN_ENDPOINT = 'https://stellar.api.onfinality.io/public'; + +// https://stellar.expert/explorer/public/ledger/60124580 +const blockHeight = 60124580; describe('BlockchainService', () => { let blockchainService: BlockchainService; beforeEach(() => { const apiService = { - api: new StellarApi(HTTP_ENDPOINT, new SorobanServer(SOROBAN_ENDPOINT)), + api: new StellarApi(SOROBAN_ENDPOINT), } as StellarApiService; blockchainService = new BlockchainService(apiService); }); it('correctly calculates block timestamp', async () => { - //https://stellar.expert/explorer/testnet/ledger/1453893 - const timestamp = await blockchainService.getBlockTimestamp(1453893); - - expect(timestamp.toISOString()).toBe('2024-05-05T04:17:35.000Z'); + const timestamp = await blockchainService.getBlockTimestamp(blockHeight); + expect(timestamp.toISOString()).toBe('2025-12-03T00:51:22.000Z'); }); it('correctly gets the header for a height', async () => { - const header = await blockchainService.getHeaderForHeight(1453893); + const header = await blockchainService.getHeaderForHeight(blockHeight); expect(header).toEqual({ - blockHeight: 1453893, - blockHash: '1453893', - parentHash: '1453892', - timestamp: new Date('2024-05-05T04:17:35.000Z'), + blockHeight: blockHeight, + blockHash: '38258f21481cb72305fbfee9cdd8fb4a6dc12889cea26ef7594fda5a529577a4', + parentHash: 'f616488a2247b0831f53cab1b7bd7a3471df44fef3362aa1a670ed4ae6eb2eb1', + timestamp: new Date('2025-12-03T00:51:22.000Z'), }); }); }); diff --git a/packages/node/src/blockchain.service.ts b/packages/node/src/blockchain.service.ts index 54fa30e1..5f1d8cc5 100644 --- a/packages/node/src/blockchain.service.ts +++ b/packages/node/src/blockchain.service.ts @@ -1,38 +1,21 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { Inject, Injectable } from '@nestjs/common'; -import { Horizon } from '@stellar/stellar-sdk'; -import { - isCustomDs, - isRuntimeDs, - StellarRuntimeDataSourceImpl, -} from '@subql/common-stellar'; -import { - DatasourceParams, - Header, - IBlock, - IBlockchainService, -} from '@subql/node-core'; -import { - StellarBlockWrapper, - StellarHandlerKind, - SubqlCustomDatasource, - SubqlCustomHandler, - SubqlDatasource, - SubqlMapping, -} from '@subql/types-stellar'; -import { plainToClass } from 'class-transformer'; -import { validateSync } from 'class-validator'; -import { SubqueryProject } from './configure/SubqueryProject'; -import { getBlockSize } from './indexer/types'; -import { IIndexerWorker } from './indexer/worker/worker'; -import { StellarApiService } from './stellar'; +import {Inject, Injectable} from '@nestjs/common'; +import {isCustomDs, isRuntimeDs, StellarRuntimeDataSourceImpl} from '@subql/common-stellar'; +import {DatasourceParams, Header, IBlock, IBlockchainService} from '@subql/node-core'; +import {StellarBlockWrapper, StellarHandlerKind, SubqlCustomDatasource, SubqlDatasource} from '@subql/types-stellar'; +import {plainToClass} from 'class-transformer'; +import {validateSync} from 'class-validator'; +import {SubqueryProject} from './configure/SubqueryProject'; +import {getBlockSize} from './indexer/types'; +import {IIndexerWorker} from './indexer/worker/worker'; +import {StellarApiService} from './stellar'; import SafeStellarProvider from './stellar/safe-api'; -import { calcInterval, stellarBlockToHeader } from './stellar/utils.stellar'; +import {calcInterval, stellarBlockToHeader} from './stellar/utils.stellar'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const { version: packageVersion } = require('../package.json'); +const {version: packageVersion} = require('../package.json'); const BLOCK_TIME_VARIANCE = 5000; const INTERVAL_PERCENT = 0.9; @@ -57,16 +40,14 @@ export class BlockchainService constructor(@Inject('APIService') private apiService: StellarApiService) {} - async fetchBlocks( - blockNums: number[], - ): Promise[]> { + async fetchBlocks(blockNums: number[]): Promise[]> { return this.apiService.fetchBlocks(blockNums); } async fetchBlockWorker( worker: IIndexerWorker, blockNum: number, - context: { workers: IIndexerWorker[] }, + context: {workers: IIndexerWorker[]}, ): Promise
{ return worker.fetchBlock(blockNum, 0 /* Not used by stellar*/); } @@ -97,15 +78,20 @@ export class BlockchainService } async getHeaderForHeight(height: number): Promise
{ - const res = await this.apiService.api.api.ledgers().ledger(height).call(); - return stellarBlockToHeader(res as any); + const res = await this.apiService.api.rpcClient.getLedgers({ + startLedger: height, + pagination: { + limit: 1, + }, + }); + if (!res.ledgers[0]) { + throw new Error(`Failed to get ledger for sequence ${height}`); + } + return stellarBlockToHeader(res.ledgers[0]); } // eslint-disable-next-line @typescript-eslint/require-await - async updateDynamicDs( - params: DatasourceParams, - dsObj: SubqlDatasource | SubqlCustomDatasource, - ): Promise { + async updateDynamicDs(params: DatasourceParams, dsObj: SubqlDatasource | SubqlCustomDatasource): Promise { if (isCustomDs(dsObj)) { dsObj.processor.options = { ...dsObj.processor.options, @@ -125,11 +111,7 @@ export class BlockchainService forbidNonWhitelisted: false, }); if (errors.length) { - throw new Error( - `Dynamic ds is invalid\n${errors - .map((e) => e.toString()) - .join('\n')}`, - ); + throw new Error(`Dynamic ds is invalid\n${errors.map((e) => e.toString()).join('\n')}`); } } } @@ -145,10 +127,7 @@ export class BlockchainService } async getBlockTimestamp(height: number): Promise { - const block = await this.apiService.api.api.ledgers().ledger(height).call(); - - return new Date( - (block as unknown as Horizon.ServerApi.LedgerRecord).closed_at, - ); + const header = await this.getHeaderForHeight(height); + return header.timestamp; } } diff --git a/packages/node/src/configure/SubqueryProject.ts b/packages/node/src/configure/SubqueryProject.ts index 5d7565b2..b6374ac0 100644 --- a/packages/node/src/configure/SubqueryProject.ts +++ b/packages/node/src/configure/SubqueryProject.ts @@ -1,9 +1,7 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import assert from 'assert'; -import { Injectable } from '@nestjs/common'; -import { validateSemver } from '@subql/common'; +import {Injectable} from '@nestjs/common'; import { StellarProjectNetworkConfig, parseStellarProjectManifest, @@ -12,8 +10,8 @@ import { isRuntimeDs, SubqlStellarDataSource, } from '@subql/common-stellar'; -import { CronFilter, BaseSubqueryProject } from '@subql/node-core'; -import { Reader } from '@subql/types-core'; +import {CronFilter, BaseSubqueryProject} from '@subql/node-core'; +import {Reader} from '@subql/types-core'; import { SubqlDatasource, CustomDatasourceTemplate, @@ -21,24 +19,18 @@ import { StellarBlockFilter, } from '@subql/types-stellar'; -const { version: packageVersion } = require('../../package.json'); +const {version: packageVersion} = require('../../package.json'); export type StellarProjectDs = SubqlStellarDataSource; -export type StellarProjectDsTemplate = - | RuntimeDatasourceTemplate - | CustomDatasourceTemplate; +export type StellarProjectDsTemplate = RuntimeDatasourceTemplate | CustomDatasourceTemplate; export type SubqlProjectBlockFilter = StellarBlockFilter & CronFilter; // This is the runtime type after we have mapped genesisHash to chainId and endpoint/dict have been provided when dealing with deployments -type NetworkConfig = StellarProjectNetworkConfig & { chainId: string }; +type NetworkConfig = StellarProjectNetworkConfig & {chainId: string}; -export type SubqueryProject = BaseSubqueryProject< - StellarProjectDs, - StellarProjectDsTemplate, - NetworkConfig ->; +export type SubqueryProject = BaseSubqueryProject; export async function createSubQueryProject( path: string, @@ -63,9 +55,7 @@ export async function createSubQueryProject( return project; } -export function dsHasSorobanEventHandler( - dataSources: SubqlDatasource[], -): boolean { +export function dsHasSorobanEventHandler(dataSources: SubqlDatasource[]): boolean { return ( dataSources.findIndex(function (ds) { return ( diff --git a/packages/node/src/configure/configure.module.ts b/packages/node/src/configure/configure.module.ts index d930392d..1c40e9ca 100644 --- a/packages/node/src/configure/configure.module.ts +++ b/packages/node/src/configure/configure.module.ts @@ -1,10 +1,10 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { DynamicModule, Global, Module } from '@nestjs/common'; -import { NodeConfig, registerApp } from '@subql/node-core'; -import { yargsOptions } from '../yargs'; -import { createSubQueryProject, SubqueryProject } from './SubqueryProject'; +import {DynamicModule, Global, Module} from '@nestjs/common'; +import {NodeConfig, registerApp} from '@subql/node-core'; +import {yargsOptions} from '../yargs'; +import {createSubQueryProject, SubqueryProject} from './SubqueryProject'; const pjson = require('../../package.json'); @@ -15,22 +15,18 @@ export class ConfigureModule { nodeConfig: NodeConfig; project: SubqueryProject; }> { - const { argv } = yargsOptions; - const { nodeConfig, project } = await registerApp( + const {argv} = yargsOptions; + const {nodeConfig, project} = await registerApp( argv, createSubQueryProject, yargsOptions.showHelp.bind(yargsOptions), pjson, ); - if (argv['soroban-network-endpoint']) { - project.network.sorobanEndpoint = argv['soroban-network-endpoint']; - } - - return { nodeConfig, project }; + return {nodeConfig, project}; } static async register(): Promise { - const { nodeConfig, project } = await ConfigureModule.getInstance(); + const {nodeConfig, project} = await ConfigureModule.getInstance(); return { module: ConfigureModule, @@ -52,12 +48,7 @@ export class ConfigureModule { useValue: null, }, ], - exports: [ - NodeConfig, - 'ISubqueryProject', - 'IProjectUpgradeService', - 'Null', - ], + exports: [NodeConfig, 'ISubqueryProject', 'IProjectUpgradeService', 'Null'], }; } } diff --git a/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.spec.ts b/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.spec.ts index c9158e2c..f88ea3d4 100644 --- a/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.spec.ts +++ b/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.spec.ts @@ -1,24 +1,16 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { NETWORK_FAMILY } from '@subql/common'; -import { DictionaryService, NodeConfig } from '@subql/node-core'; -import { - StellarDatasourceKind, - StellarHandlerKind, - SubqlRuntimeDatasource, -} from '@subql/types-stellar'; -import { MetaData } from '@subql/utils'; -import { - StellarDictionaryV1, - buildDictionaryQueryEntries, -} from './stellarDictionaryV1'; +import {EventEmitter2} from '@nestjs/event-emitter'; +import {NETWORK_FAMILY} from '@subql/common'; +import {DictionaryService, NodeConfig} from '@subql/node-core'; +import {StellarDatasourceKind, StellarHandlerKind, SubqlRuntimeDatasource} from '@subql/types-stellar'; +import {MetaData} from '@subql/utils'; +import {StellarDictionaryV1, buildDictionaryQueryEntries} from './stellarDictionaryV1'; const nodeConfig = { dictionaryTimeout: 10000, - dictionaryRegistry: - 'https://github.com/subquery/templates/raw/main/dist/dictionary.json', + dictionaryRegistry: 'https://github.com/subquery/templates/raw/main/dist/dictionary.json', } as NodeConfig; const project = { network: { @@ -97,44 +89,6 @@ describe('buildDictionaryQueryEntries', () => { ]); }); - it('should correctly build dictionary query entries for effects', () => { - const ds: SubqlRuntimeDatasource = { - kind: StellarDatasourceKind.Runtime, - startBlock: 1, - mapping: { - file: '', - handlers: [ - { - handler: 'handleEffects', - kind: StellarHandlerKind.Effects, - filter: { - account: 'effect_account', - type: 'effect_type', - }, - }, - ], - }, - }; - const result = buildDictionaryQueryEntries([ds]); - expect(result).toEqual([ - { - entity: 'effects', - conditions: [ - { - field: 'type', - value: 'effect_type', - matcher: 'equalTo', - }, - { - field: 'account', - value: 'effect_account', - matcher: 'equalTo', - }, - ], - }, - ]); - }); - it('should return an empty array when no filters are provided', () => { const ds: SubqlRuntimeDatasource = { kind: StellarDatasourceKind.Runtime, @@ -160,25 +114,14 @@ class TestDictionaryService extends DictionaryService { return Promise.resolve(undefined); } async getRegistryDictionaries(chainId: string): Promise { - return this.resolveDictionary( - NETWORK_FAMILY.near, - chainId, - this.nodeConfig.dictionaryRegistry, - ); + return this.resolveDictionary(NETWORK_FAMILY.near, chainId, this.nodeConfig.dictionaryRegistry); } } describe('dictionary v1', () => { let dictionary: StellarDictionaryV1; beforeEach(async () => { - const testDictionaryService = new TestDictionaryService( - project.network.chainId, - nodeConfig, - new EventEmitter2(), - ); - const dictionaryEndpoints = - await testDictionaryService.getRegistryDictionaries( - project.network.chainId, - ); + const testDictionaryService = new TestDictionaryService(project.network.chainId, nodeConfig, new EventEmitter2()); + const dictionaryEndpoints = await testDictionaryService.getRegistryDictionaries(project.network.chainId); dictionary = await StellarDictionaryV1.create( { network: { @@ -186,7 +129,7 @@ describe('dictionary v1', () => { dictionary: dictionaryEndpoints[1], }, } as any, - { dictionaryTimeout: 10000 } as NodeConfig, + {dictionaryTimeout: 10000} as NodeConfig, jest.fn(), dictionaryEndpoints[1], // use endpoint from network ); diff --git a/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.ts b/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.ts index 62fd51b7..8cbbdc11 100644 --- a/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.ts +++ b/packages/node/src/indexer/dictionary/v1/stellarDictionaryV1.ts @@ -6,32 +6,24 @@ import { SubqlStellarDataSource, StellarTransactionFilter, StellarOperationFilter, - StellarEffectFilter, SubqlStellarProcessorOptions, } from '@subql/common-stellar'; -import { - NodeConfig, - DictionaryV1, - getLogger, - DsProcessorService, -} from '@subql/node-core'; +import {NodeConfig, DictionaryV1, getLogger, DsProcessorService} from '@subql/node-core'; import { DictionaryQueryCondition, DictionaryQueryEntry, DictionaryQueryEntry as DictionaryV1QueryEntry, } from '@subql/types-core'; -import { SorobanEventFilter, SubqlDatasource } from '@subql/types-stellar'; -import { sortBy, uniqBy } from 'lodash'; -import { SubqueryProject } from '../../../configure/SubqueryProject'; -import { yargsOptions } from '../../../yargs'; +import {StellarEventFilter, SubqlDatasource} from '@subql/types-stellar'; +import {sortBy, uniqBy} from 'lodash'; +import {SubqueryProject} from '../../../configure/SubqueryProject'; +import {yargsOptions} from '../../../yargs'; type GetDsProcessor = DsProcessorService['getDsProcessor']; const logger = getLogger('DictionaryService'); -function transactionFilterToQueryEntry( - filter: StellarTransactionFilter, -): DictionaryQueryEntry { +function transactionFilterToQueryEntry(filter: StellarTransactionFilter): DictionaryQueryEntry { const conditions: DictionaryQueryCondition[] = []; if (filter.account) { @@ -47,9 +39,7 @@ function transactionFilterToQueryEntry( }; } -function operationFilterToQueryEntry( - filter: StellarOperationFilter, -): DictionaryQueryEntry { +function operationFilterToQueryEntry(filter: StellarOperationFilter): DictionaryQueryEntry { const conditions: DictionaryQueryCondition[] = []; if (filter.type) { @@ -72,32 +62,8 @@ function operationFilterToQueryEntry( }; } -function effectFilterToQueryEntry( - filter: StellarEffectFilter, -): DictionaryQueryEntry { - const conditions: DictionaryQueryCondition[] = []; - - if (filter.type) { - conditions.push({ - field: 'type', - value: filter.type.toLowerCase(), - matcher: 'equalTo', - }); - } - if (filter.account) { - conditions.push({ - field: 'account', - value: filter.account.toLowerCase(), - matcher: 'equalTo', - }); - } - return { - entity: 'effects', - conditions, - }; -} function eventFilterToQueryEntry( - filter: SorobanEventFilter, + filter: StellarEventFilter, dsOptions: SubqlStellarProcessorOptions | SubqlStellarProcessorOptions[], ): DictionaryQueryEntry { const queryAddressLimit = yargsOptions.argv['query-address-limit']; @@ -105,9 +71,7 @@ function eventFilterToQueryEntry( const conditions: DictionaryQueryCondition[] = []; if (Array.isArray(dsOptions)) { - const addresses = dsOptions - .map((option) => option.address) - .filter((address): address is string => !!address); + const addresses = dsOptions.map((option) => option.address).filter((address): address is string => !!address); if (addresses.length > queryAddressLimit) { logger.warn( @@ -159,9 +123,7 @@ type GroupedSubqlProjectDs = SubqlDatasource & { groupedOptions?: SubqlStellarProcessorOptions[]; }; -export function buildDictionaryQueryEntries( - dataSources: SubqlDatasource[], -): DictionaryV1QueryEntry[] { +export function buildDictionaryQueryEntries(dataSources: SubqlDatasource[]): DictionaryV1QueryEntry[] { const queryEntries: DictionaryQueryEntry[] = []; for (const ds of dataSources) { @@ -190,19 +152,10 @@ export function buildDictionaryQueryEntries( } break; } - case StellarHandlerKind.Effects: { - const filter = handler.filter as StellarEffectFilter; - if (filter.account || filter.type) { - queryEntries.push(effectFilterToQueryEntry(filter)); - } else { - return []; - } - break; - } // TODO, event is not provided in current dictionary, // https://github.com/subquery/stellar-subql-dictionaries/blob/main/schema.graphql case StellarHandlerKind.Event: { - const filter = handler.filter as SorobanEventFilter; + const filter = handler.filter as StellarEventFilter; if (ds.options?.address || filter.topics) { queryEntries.push(eventFilterToQueryEntry(filter, ds.options!)); } else { @@ -215,13 +168,7 @@ export function buildDictionaryQueryEntries( } } - return uniqBy( - queryEntries, - (item) => - `${item.entity}|${JSON.stringify( - sortBy(item.conditions, (c) => c.field), - )}`, - ); + return uniqBy(queryEntries, (item) => `${item.entity}|${JSON.stringify(sortBy(item.conditions, (c) => c.field))}`); } export class StellarDictionaryV1 extends DictionaryV1 { @@ -242,20 +189,12 @@ export class StellarDictionaryV1 extends DictionaryV1 { dictionaryUrl: string, chainId?: string, ): Promise { - const dictionary = new StellarDictionaryV1( - project, - nodeConfig, - getDsProcessor, - dictionaryUrl, - chainId, - ); + const dictionary = new StellarDictionaryV1(project, nodeConfig, getDsProcessor, dictionaryUrl, chainId); await dictionary.init(); return dictionary; } - buildDictionaryQueryEntries( - dataSources: SubqlStellarDataSource[], - ): DictionaryV1QueryEntry[] { + buildDictionaryQueryEntries(dataSources: SubqlStellarDataSource[]): DictionaryV1QueryEntry[] { return buildDictionaryQueryEntries(dataSources); } } diff --git a/packages/node/src/indexer/indexer.manager.ts b/packages/node/src/indexer/indexer.manager.ts index 3d9b8c8b..860237ab 100644 --- a/packages/node/src/indexer/indexer.manager.ts +++ b/packages/node/src/indexer/indexer.manager.ts @@ -1,8 +1,8 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { Inject, Injectable } from '@nestjs/common'; -import { Horizon } from '@stellar/stellar-sdk'; +import {Inject, Injectable} from '@nestjs/common'; +import {rpc} from '@stellar/stellar-sdk'; import { isCustomDs, isRuntimeDs, @@ -13,9 +13,7 @@ import { isBlockHandlerProcessor, isTransactionHandlerProcessor, isOperationHandlerProcessor, - isEffectHandlerProcessor, isEventHandlerProcessor, - isSorobanTransactionHandlerProcessor, } from '@subql/common-stellar'; import { NodeConfig, @@ -24,7 +22,6 @@ import { IndexerSandbox, ProcessBlockResponse, BaseIndexerManager, - ApiService, IBlock, SandboxService, DsProcessorService, @@ -34,20 +31,18 @@ import { import { StellarBlockWrapper, SubqlDatasource, - StellarTransaction, - StellarOperation, - StellarEffect, - StellarBlock, StellarBlockFilter, StellarTransactionFilter, StellarOperationFilter, - StellarEffectFilter, - SorobanEvent, - SorobanEventFilter, + StellarEventFilter, + StellarEvent, + StellarOperation, + StellarTransaction, + StellarBlock, } from '@subql/types-stellar'; -import { BlockchainService } from '../blockchain.service'; -import { StellarApi, StellarApiService } from '../stellar'; -import { StellarBlockWrapped } from '../stellar/block.stellar'; +import {BlockchainService} from '../blockchain.service'; +import {StellarApi, StellarApiService} from '../stellar'; +import {StellarBlockWrapped} from '../stellar/block.stellar'; import SafeStellarProvider from '../stellar/safe-api'; const logger = getLogger('indexer'); @@ -95,52 +90,57 @@ export class IndexerManager extends BaseIndexerManager< block: IBlock, dataSources: SubqlStellarDataSource[], ): Promise { - return super.internalIndexBlock(block, dataSources, () => - this.getApi(block.block), - ); + return super.internalIndexBlock(block, dataSources, () => this.getApi(block.block)); } - protected getDsProcessor( - ds: SubqlStellarDataSource, - safeApi: SafeStellarProvider, - ): IndexerSandbox { + protected getDsProcessor(ds: SubqlStellarDataSource, safeApi: SafeStellarProvider): IndexerSandbox { // Expand on the type here to allow for extra injections, we also change the unsafeApi type - const sandbox = this.sandboxService as unknown as SandboxService< - SafeStellarProvider | null, - Horizon.Server - >; - return sandbox.getDsProcessor(ds, safeApi, this.apiService.unsafeApi.api, { - unsafeSorobanApi: this.nodeConfig.unsafe - ? this.apiService.unsafeApi.sorobanClient - : undefined, - }); + const sandbox = this.sandboxService as unknown as SandboxService; + return sandbox.getDsProcessor(ds, safeApi, this.apiService.unsafeApi.rpcClient); } // eslint-disable-next-line @typescript-eslint/require-await - private async getApi( - block: StellarBlockWrapper, - ): Promise { + private async getApi(block: StellarBlockWrapper): Promise { // return this.apiService.safeApi(block.block.sequence); return null; } protected async indexBlockData( - { block, transactions }: StellarBlockWrapper, + {block, transactions}: StellarBlockWrapper, dataSources: SubqlDatasource[], getVM: (d: SubqlDatasource) => Promise, ): Promise { await this.indexBlockContent(block, dataSources, getVM); + const groupedOperations = block.operations.reduce((acc, op) => { + acc[op.transaction.tx.txHash] ??= []; + acc[op.transaction.tx.txHash].push(op); + + return acc; + }, {} as Record); + + const groupedEvents = block.events.reduce((acc, evt) => { + acc[evt.event.txHash] ??= []; + acc[evt.event.txHash].push(evt); + + return acc; + }, {} as Record); + for (const tx of transactions) { await this.indexTransaction(tx, dataSources, getVM); - for (const operation of tx.operations) { + const operationEvents = (groupedEvents[tx.tx.txHash] ?? []).reduce((acc, evt) => { + acc[evt.event.operationIndex] ??= []; + acc[evt.event.operationIndex].push(evt); + + return acc; + }, {} as Record); + + for (const [index, operation] of Object.entries(groupedOperations[tx.tx.txHash])) { await this.indexOperation(operation, dataSources, getVM); - for (const effect of operation.effects) { - await this.indexEffect(effect, dataSources, getVM); - } - for (const event of operation.events) { + const events = operationEvents[index] ?? []; + for (const event of events) { await this.indexEvent(event, dataSources, getVM); } } @@ -163,25 +163,7 @@ export class IndexerManager extends BaseIndexerManager< getVM: (d: SubqlDatasource) => Promise, ): Promise { for (const ds of dataSources) { - await this.indexData( - StellarHandlerKind.Transaction, - transaction, - ds, - getVM, - ); - - if ( - transaction.operations.some( - (op) => op.type.toString() === 'invoke_host_function', - ) - ) { - await this.indexData( - StellarHandlerKind.SorobanTransaction, - transaction, - ds, - getVM, - ); - } + await this.indexData(StellarHandlerKind.Transaction, transaction, ds, getVM); } } @@ -195,18 +177,8 @@ export class IndexerManager extends BaseIndexerManager< } } - private async indexEffect( - effect: StellarEffect, - dataSources: SubqlDatasource[], - getVM: (d: SubqlDatasource) => Promise, - ): Promise { - for (const ds of dataSources) { - await this.indexData(StellarHandlerKind.Effects, effect, ds, getVM); - } - } - private async indexEvent( - event: SorobanEvent, + event: StellarEvent, dataSources: SubqlDatasource[], getVM: (d: SubqlDatasource) => Promise, ): Promise { @@ -215,11 +187,7 @@ export class IndexerManager extends BaseIndexerManager< } } - protected async prepareFilteredData( - kind: StellarHandlerKind, - data: T, - ds: SubqlDatasource, - ): Promise { + protected async prepareFilteredData(kind: StellarHandlerKind, data: T, ds: SubqlDatasource): Promise { return Promise.resolve(data); } } @@ -227,81 +195,30 @@ export class IndexerManager extends BaseIndexerManager< type ProcessorTypeMap = { [StellarHandlerKind.Block]: typeof isBlockHandlerProcessor; [StellarHandlerKind.Transaction]: typeof isTransactionHandlerProcessor; - [StellarHandlerKind.SorobanTransaction]: typeof isSorobanTransactionHandlerProcessor; [StellarHandlerKind.Operation]: typeof isOperationHandlerProcessor; - [StellarHandlerKind.Effects]: typeof isEffectHandlerProcessor; [StellarHandlerKind.Event]: typeof isEventHandlerProcessor; }; const ProcessorTypeMap = { [StellarHandlerKind.Block]: isBlockHandlerProcessor, [StellarHandlerKind.Transaction]: isTransactionHandlerProcessor, - [StellarHandlerKind.SorobanTransaction]: isSorobanTransactionHandlerProcessor, [StellarHandlerKind.Operation]: isOperationHandlerProcessor, - [StellarHandlerKind.Effects]: isEffectHandlerProcessor, [StellarHandlerKind.Event]: isEventHandlerProcessor, }; const FilterTypeMap = { - [StellarHandlerKind.Block]: ( - data: StellarBlock, - filter: StellarBlockFilter, - ds: SubqlStellarDataSource, - ) => - StellarBlockWrapped.filterBlocksProcessor( - data, - filter, - ds.options?.address, - ), - + [StellarHandlerKind.Block]: (data: StellarBlock, filter: StellarBlockFilter, ds: SubqlStellarDataSource) => + StellarBlockWrapped.filterBlocksProcessor(data, filter), [StellarHandlerKind.Transaction]: ( data: StellarTransaction, filter: StellarTransactionFilter, ds: SubqlStellarDataSource, - ) => - StellarBlockWrapped.filterTransactionProcessor( - data, - filter, - ds.options?.address, - ), - - [StellarHandlerKind.SorobanTransaction]: ( - data: StellarTransaction, - filter: StellarTransactionFilter, - ds: SubqlStellarDataSource, - ) => - StellarBlockWrapped.filterTransactionProcessor( - data, - filter, - ds.options?.address, - ), - + ) => StellarBlockWrapped.filterTransactionProcessor(data, filter, ds.options?.address), [StellarHandlerKind.Operation]: ( data: StellarOperation, filter: StellarOperationFilter, ds: SubqlStellarDataSource, - ) => - StellarBlockWrapped.filterOperationProcessor( - data, - filter, - ds.options?.address, - ), - - [StellarHandlerKind.Effects]: ( - data: StellarEffect, - filter: StellarEffectFilter, - ds: SubqlStellarDataSource, - ) => - StellarBlockWrapped.filterEffectProcessor( - data, - filter, - ds.options?.address, - ), - - [StellarHandlerKind.Event]: ( - data: SorobanEvent, - filter: SorobanEventFilter, - ds: SubqlStellarDataSource, - ) => + ) => StellarBlockWrapped.filterOperationProcessor(data, filter), + [StellarHandlerKind.Event]: (data: StellarEvent, filter: StellarEventFilter, ds: SubqlStellarDataSource) => StellarBlockWrapped.filterEventProcessor(data, filter, ds.options?.address), }; diff --git a/packages/node/src/indexer/types.ts b/packages/node/src/indexer/types.ts index 3bcb9655..f388940f 100644 --- a/packages/node/src/indexer/types.ts +++ b/packages/node/src/indexer/types.ts @@ -1,19 +1,10 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { StellarBlockWrapper } from '@subql/types-stellar'; +import {StellarBlockWrapper} from '@subql/types-stellar'; export type BestBlocks = Record; export function getBlockSize(block: StellarBlockWrapper): number { - const { - failed_transaction_count, - operation_count, - successful_transaction_count, - tx_set_operation_count, - } = block.block; - return ( - (tx_set_operation_count ?? - successful_transaction_count + failed_transaction_count) + operation_count - ); + return block.block.transactions.length + block.block.operations.length; } diff --git a/packages/node/src/stellar/api.connection.spec.ts b/packages/node/src/stellar/api.connection.spec.ts index 72219288..2f193036 100644 --- a/packages/node/src/stellar/api.connection.spec.ts +++ b/packages/node/src/stellar/api.connection.spec.ts @@ -9,50 +9,40 @@ import { RateLimitError, TimeoutError, } from '@subql/node-core'; -import { StellarBlock, StellarBlockWrapper } from '@subql/types-stellar'; -import { StellarApiConnection } from './api.connection'; -import { StellarApi } from './api.stellar'; -import { SorobanServer } from './soroban.server'; +import {StellarBlock, StellarBlockWrapper} from '@subql/types-stellar'; +import {StellarApiConnection} from './api.connection'; +import {StellarApi} from './api.stellar'; -const HTTP_ENDPOINT = 'https://horizon-futurenet.stellar.org'; const SOROBAN_ENDPOINT = 'https://rpc-futurenet.stellar.org'; describe('StellarApiConnection', () => { let apiConnection: StellarApiConnection; let unsafeApi: StellarApi; - let sorobanApi: SorobanServer; const mockBlocks: StellarBlockWrapper[] = [ { - block: { sequence: 1, hash: 'hash1' } as unknown as StellarBlock, + block: {sequence: 1, hash: 'hash1'} as unknown as StellarBlock, transactions: [], operations: [], - effects: [], + events: [], }, { - block: { sequence: 2, hash: 'hash2' } as unknown as StellarBlock, + block: {sequence: 2, hash: 'hash2'} as unknown as StellarBlock, transactions: [], operations: [], - effects: [], + events: [], }, ]; const fetchBlockBatches = jest.fn().mockResolvedValue(mockBlocks); beforeEach(async () => { - sorobanApi = new SorobanServer(SOROBAN_ENDPOINT); - unsafeApi = new StellarApi(HTTP_ENDPOINT, sorobanApi); + unsafeApi = new StellarApi(SOROBAN_ENDPOINT); await unsafeApi.init(); apiConnection = new StellarApiConnection(unsafeApi, fetchBlockBatches); }); it('creates a connection', async () => { - expect( - await StellarApiConnection.create( - HTTP_ENDPOINT, - fetchBlockBatches, - sorobanApi, - ), - ).toBeInstanceOf(StellarApiConnection); + expect(await StellarApiConnection.create(fetchBlockBatches, SOROBAN_ENDPOINT)).toBeInstanceOf(StellarApiConnection); }); it('fetches blocks', async () => { diff --git a/packages/node/src/stellar/api.connection.ts b/packages/node/src/stellar/api.connection.ts index 33c8228b..5bf381a4 100644 --- a/packages/node/src/stellar/api.connection.ts +++ b/packages/node/src/stellar/api.connection.ts @@ -12,33 +12,18 @@ import { LargeResponseError, IBlock, } from '@subql/node-core'; -import { - StellarBlockWrapper, - IStellarEndpointConfig, -} from '@subql/types-stellar'; -import { StellarApi } from './api.stellar'; +import {StellarBlockWrapper, IStellarEndpointConfig} from '@subql/types-stellar'; +import {StellarApi} from './api.stellar'; import SafeStellarProvider from './safe-api'; -import { SorobanServer } from './soroban.server'; -type FetchFunc = ( - api: StellarApi, - batch: number[], -) => Promise[]>; +type FetchFunc = (api: StellarApi, batch: number[]) => Promise[]>; export class StellarApiConnection - implements - IApiConnectionSpecific< - StellarApi, - SafeStellarProvider, - IBlock[] - > + implements IApiConnectionSpecific[]> { readonly networkMeta: NetworkMetadataPayload; - constructor( - public unsafeApi: StellarApi, - private fetchBlocksBatches: FetchFunc, - ) { + constructor(public unsafeApi: StellarApi, private fetchBlocksBatches: FetchFunc) { this.networkMeta = { chain: unsafeApi.getChainId(), specName: unsafeApi.getSpecName(), @@ -47,12 +32,11 @@ export class StellarApiConnection } static async create( - endpoint: string, fetchBlockBatches: FetchFunc, - soroban?: SorobanServer, + endpoint: string, config?: IStellarEndpointConfig, ): Promise { - const api = new StellarApi(endpoint, soroban, config); + const api = new StellarApi(endpoint, config); await api.init(); @@ -85,19 +69,12 @@ export class StellarApiConnection formatted_error = new TimeoutError(e); } else if (e.message.startsWith(`disconnected from `)) { formatted_error = new DisconnectionError(e); - } else if ( - e.message.includes(`Rate Limit Exceeded`) || - e.message.includes('Too Many Requests') - ) { + } else if (e.message.includes(`Rate Limit Exceeded`) || e.message.includes('Too Many Requests')) { formatted_error = new RateLimitError(e); } else if (e.message.includes(`limit must not exceed`)) { formatted_error = new LargeResponseError(e); } else { - formatted_error = new ApiConnectionError( - e.name, - e.message, - ApiErrorType.Default, - ); + formatted_error = new ApiConnectionError(e.name, e.message, ApiErrorType.Default); } return formatted_error; } diff --git a/packages/node/src/stellar/api.service.stellar.spec.ts b/packages/node/src/stellar/api.service.stellar.spec.ts index bd3bfdf4..c7e70cb4 100644 --- a/packages/node/src/stellar/api.service.stellar.spec.ts +++ b/packages/node/src/stellar/api.service.stellar.spec.ts @@ -1,29 +1,25 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { INestApplication } from '@nestjs/common'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; -import { Test } from '@nestjs/testing'; -import { scValToNative } from '@stellar/stellar-sdk'; -import { ConnectionPoolService, NodeConfig } from '@subql/node-core'; -import { ConnectionPoolStateManager } from '@subql/node-core/dist'; -import { GraphQLSchema } from 'graphql'; -import { range } from 'lodash'; -import { SubqueryProject } from '../configure/SubqueryProject'; -import { StellarApiService } from './api.service.stellar'; -import { StellarApi } from './api.stellar'; - -const HTTP_ENDPOINT = 'https://horizon-futurenet.stellar.org'; +import {INestApplication} from '@nestjs/common'; +import {EventEmitter2, EventEmitterModule} from '@nestjs/event-emitter'; +import {Test} from '@nestjs/testing'; +import {scValToNative} from '@stellar/stellar-sdk'; +import {ConnectionPoolService, NodeConfig} from '@subql/node-core'; +import {ConnectionPoolStateManager} from '@subql/node-core/dist'; +import {GraphQLSchema} from 'graphql'; +import {range} from 'lodash'; +import {SubqueryProject} from '../configure/SubqueryProject'; +import {StellarApiService} from './api.service.stellar'; +import {StellarApi} from './api.stellar'; + +// const HTTP_ENDPOINT = 'https://horizon-futurenet.stellar.org'; const SOROBAN_ENDPOINT = 'https://rpc-futurenet.stellar.org'; -function testSubqueryProject( - endpoint: string, - sorobanEndpoint: string, -): SubqueryProject { +function testSubqueryProject(endpoint: string): SubqueryProject { return { network: { endpoint: [endpoint], - sorobanEndpoint, chainId: 'Test SDF Future Network ; October 2022', }, dataSources: [], @@ -35,8 +31,7 @@ function testSubqueryProject( } const prepareApiService = async ( - endpoint: string = HTTP_ENDPOINT, - soroban: string = SOROBAN_ENDPOINT, + endpoint: string = SOROBAN_ENDPOINT, project?: SubqueryProject, ): Promise<[StellarApiService, INestApplication]> => { const module = await Test.createTestingModule({ @@ -53,17 +48,12 @@ const prepareApiService = async ( }, { provide: 'ISubqueryProject', - useFactory: () => project ?? testSubqueryProject(endpoint, soroban), + useFactory: () => project ?? testSubqueryProject(endpoint), }, { provide: StellarApiService, useFactory: StellarApiService.create.bind(StellarApiService), - inject: [ - 'ISubqueryProject', - ConnectionPoolService, - EventEmitter2, - NodeConfig, - ], + inject: ['ISubqueryProject', ConnectionPoolService, EventEmitter2, NodeConfig], }, ], imports: [EventEmitterModule.forRoot()], @@ -85,9 +75,7 @@ describe('StellarApiService', () => { }); it('should allow http protocol for soroban endpoint', async () => { - await expect( - prepareApiService(HTTP_ENDPOINT, 'http://rpc-futurenet.stellar.org'), - ).resolves.not.toThrow(); + await expect(prepareApiService('http://rpc-futurenet.stellar.org')).resolves.not.toThrow(); }); it('should instantiate api', () => { @@ -96,42 +84,30 @@ describe('StellarApiService', () => { it('should fetch blocks', async () => { const latestHeight = await apiService.api.getFinalizedBlockHeight(); - const blocks = await apiService.fetchBlocks( - range(latestHeight - 1, latestHeight), - ); + const blocks = await apiService.fetchBlocks(range(latestHeight - 1, latestHeight)); expect(blocks).toBeDefined(); expect(blocks[0].block.block.sequence).toEqual(latestHeight - 1); }); it('should throw error when chainId does not match', async () => { const faultyProject = { - ...testSubqueryProject(HTTP_ENDPOINT, SOROBAN_ENDPOINT), + ...testSubqueryProject(SOROBAN_ENDPOINT), network: { - ...testSubqueryProject(HTTP_ENDPOINT, SOROBAN_ENDPOINT).network, + ...testSubqueryProject(SOROBAN_ENDPOINT).network, chainId: 'Incorrect ChainId', }, }; - await expect( - prepareApiService( - HTTP_ENDPOINT, - SOROBAN_ENDPOINT, - faultyProject as unknown as SubqueryProject, - ), - ).rejects.toThrow(); + await expect(prepareApiService(SOROBAN_ENDPOINT, faultyProject as unknown as SubqueryProject)).rejects.toThrow(); }); it('fails after maximum retries', async () => { const api = apiService.unsafeApi; // Mock the fetchBlocks method to always throw an error - (api as any).fetchBlocks = jest - .fn() - .mockRejectedValue(new Error('Network error')); + (api as any).fetchBlocks = jest.fn().mockRejectedValue(new Error('Network error')); - await expect( - (api as any).fetchBlocks(range(50000, 50100)), - ).rejects.toThrow(); + await expect((api as any).fetchBlocks(range(50000, 50100))).rejects.toThrow(); }); }); @@ -141,14 +117,10 @@ describe.skip('testnet StellarApiService', () => { let apiService: StellarApiService; let app: INestApplication; - function testSubqueryProject2( - endpoint: string, - sorobanEndpoint: string, - ): SubqueryProject { + function testSubqueryProject2(endpoint: string): SubqueryProject { return { network: { endpoint: [endpoint], - sorobanEndpoint, chainId: 'Test SDF Network ; September 2015', }, dataSources: [], @@ -161,12 +133,8 @@ describe.skip('testnet StellarApiService', () => { beforeEach(async () => { [apiService, app] = await prepareApiService( - 'https://horizon-testnet.stellar.org', 'https://soroban-testnet.stellar.org', - testSubqueryProject2( - 'https://horizon-testnet.stellar.org', - 'https://soroban-testnet.stellar.org', - ), + testSubqueryProject2('https://soroban-testnet.stellar.org'), ); }); @@ -175,11 +143,9 @@ describe.skip('testnet StellarApiService', () => { expect(blocks).toBeDefined(); const block228236 = blocks[0]; - const transferEvent = block228236.block.events?.find( - (e) => e.id === '0000980266155778048-0000000001', - ); + const transferEvent = block228236.block.events?.find((e) => e.event.id === '0000980266155778048-0000000001'); - const [sys, from, to] = transferEvent!.topic; + const [sys, from, to] = transferEvent!.event.topic; const decodedFrom = scValToNative(from).toString(); const decodedTo = scValToNative(to).toString(); diff --git a/packages/node/src/stellar/api.service.stellar.ts b/packages/node/src/stellar/api.service.stellar.ts index 2906db74..afc150d6 100644 --- a/packages/node/src/stellar/api.service.stellar.ts +++ b/packages/node/src/stellar/api.service.stellar.ts @@ -1,31 +1,16 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { Inject, Injectable } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { StellarProjectNetworkConfig } from '@subql/common-stellar'; -import { - ApiService, - ConnectionPoolService, - getLogger, - IBlock, - exitWithError, - NodeConfig, -} from '@subql/node-core'; -import { IEndpointConfig } from '@subql/types-core'; -import { - StellarBlockWrapper, - SubqlDatasource, - IStellarEndpointConfig, -} from '@subql/types-stellar'; -import { - SubqueryProject, - dsHasSorobanEventHandler, -} from '../configure/SubqueryProject'; -import { StellarApiConnection } from './api.connection'; -import { StellarApi } from './api.stellar'; +import {Injectable} from '@nestjs/common'; +import {EventEmitter2} from '@nestjs/event-emitter'; +import {StellarProjectNetworkConfig} from '@subql/common-stellar'; +import {ApiService, ConnectionPoolService, getLogger, IBlock, exitWithError, NodeConfig} from '@subql/node-core'; +import {IEndpointConfig} from '@subql/types-core'; +import {StellarBlockWrapper, IStellarEndpointConfig} from '@subql/types-stellar'; +import {SubqueryProject} from '../configure/SubqueryProject'; +import {StellarApiConnection} from './api.connection'; +import {StellarApi} from './api.stellar'; import SafeStellarProvider from './safe-api'; -import { SorobanServer } from './soroban.server'; const logger = getLogger('api'); @@ -37,10 +22,7 @@ export class StellarApiService extends ApiService< StellarApiConnection, IStellarEndpointConfig > { - private constructor( - connectionPoolService: ConnectionPoolService, - eventEmitter: EventEmitter2, - ) { + private constructor(connectionPoolService: ConnectionPoolService, eventEmitter: EventEmitter2) { super(connectionPoolService, eventEmitter); } @@ -52,15 +34,12 @@ export class StellarApiService extends ApiService< ): Promise { let network: StellarProjectNetworkConfig; - const apiService = new StellarApiService( - connectionPoolService, - eventEmitter, - ); + const apiService = new StellarApiService(connectionPoolService, eventEmitter); try { network = project.network; } catch (e) { - exitWithError(new Error(`Failed to init api`, { cause: e }), logger); + exitWithError(new Error(`Failed to init api`, {cause: e}), logger); } if (nodeConfig.primaryNetworkEndpoint) { @@ -68,42 +47,8 @@ export class StellarApiService extends ApiService< (network.endpoint as Record)[endpoint] = config; } - const sorobanEndpoint: string | undefined = network.sorobanEndpoint; - - if (!network.sorobanEndpoint && sorobanEndpoint) { - //update sorobanEndpoint from parent project - project.network.sorobanEndpoint = sorobanEndpoint; - } - - // TOOD if project upgrades introduces new datasoruces this wont work - if ( - dsHasSorobanEventHandler([ - ...project.dataSources, - ...(project.templates as SubqlDatasource[]), - ]) && - !sorobanEndpoint - ) { - throw new Error( - `Soroban network endpoint must be provided for network. chainId="${project.network.chainId}"`, - ); - } - - const { protocol } = new URL(sorobanEndpoint); - const protocolStr = protocol.replace(':', ''); - - const sorobanClient = sorobanEndpoint - ? new SorobanServer(sorobanEndpoint, { - allowHttp: protocolStr === 'http', - }) - : undefined; - await apiService.createConnections(network, (endpoint, config) => - StellarApiConnection.create( - endpoint, - apiService.fetchBlockBatches.bind(apiService), - sorobanClient, - config, - ), + StellarApiConnection.create(apiService.fetchBlockBatches.bind(apiService), endpoint, config), ); return apiService; @@ -120,9 +65,7 @@ export class StellarApiService extends ApiService< get: (target, prop, receiver) => { const originalMethod = target[prop as keyof SafeStellarProvider]; if (typeof originalMethod === 'function') { - return async ( - ...args: any[] - ): Promise> => { + return async (...args: any[]): Promise> => { let retries = 0; let currentApi = target; let throwingError: Error | undefined; @@ -131,18 +74,14 @@ export class StellarApiService extends ApiService< try { return await originalMethod.apply(currentApi, args); } catch (error: any) { - logger.warn( - `Request failed with api at height ${height} (retry ${retries}): ${error.message}`, - ); + logger.warn(`Request failed with api at height ${height} (retry ${retries}): ${error.message}`); throwingError = error; currentApi = this.unsafeApi.getSafeApi(height); retries++; } } - logger.error( - `Maximum retries (${maxRetries}) exceeded for api at height ${height}`, - ); + logger.error(`Maximum retries (${maxRetries}) exceeded for api at height ${height}`); if (!throwingError) { throw new Error('Failed to make request, maximum retries failed'); } @@ -156,10 +95,7 @@ export class StellarApiService extends ApiService< return new Proxy(this.unsafeApi.getSafeApi(height), handler); } - private async fetchBlockBatches( - api: StellarApi, - batch: number[], - ): Promise[]> { + private async fetchBlockBatches(api: StellarApi, batch: number[]): Promise[]> { return api.fetchBlocks(batch); } } diff --git a/packages/node/src/stellar/api.stellar.spec.ts b/packages/node/src/stellar/api.stellar.spec.ts index a8805bec..3552ec0f 100644 --- a/packages/node/src/stellar/api.stellar.spec.ts +++ b/packages/node/src/stellar/api.stellar.spec.ts @@ -1,20 +1,16 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { StellarApi } from './api.stellar'; -import { SorobanServer } from './soroban.server'; +import {StellarApi} from './api.stellar'; +import {StellarBlockWrapped} from './block.stellar'; +import {nativeToScVal, scValToNative, xdr} from '@stellar/stellar-sdk'; -const HTTP_ENDPOINT = 'https://horizon-futurenet.stellar.org'; const SOROBAN_ENDPOINT = 'https://rpc-futurenet.stellar.org'; jest.setTimeout(60000); -const prepareStellarApi = async function ( - stellarEndpoint = HTTP_ENDPOINT, - sorobanEndpoint = SOROBAN_ENDPOINT, -) { - const soroban = new SorobanServer(sorobanEndpoint); - const api = new StellarApi(stellarEndpoint, soroban); +const prepareStellarApi = async function (sorobanEndpoint = SOROBAN_ENDPOINT) { + const api = new StellarApi(sorobanEndpoint); await api.init(); return api; }; @@ -27,9 +23,7 @@ describe('StellarApi', () => { }); it('should initialize chainId', () => { - expect(stellarApi.getChainId()).toEqual( - 'Test SDF Future Network ; October 2022', - ); + expect(stellarApi.getChainId()).toEqual('Test SDF Future Network ; October 2022'); }); it('should get finalized block height', async () => { @@ -44,9 +38,10 @@ describe('StellarApi', () => { expect(height).toBeGreaterThan(0); }); + // Note this test can have a high chance of failure because of load balancers it('should fetch block', async () => { const latestHeight = await stellarApi.getFinalizedBlockHeight(); - const block = (await stellarApi.fetchBlocks([latestHeight]))[0]; + const [block] = await stellarApi.fetchBlocks([latestHeight]); expect(block.getHeader().blockHeight).toEqual(latestHeight); }); @@ -89,40 +84,193 @@ describe('StellarApi', () => { it('handleError - soroban node been reset', async () => { const error = new Error('start is after newest ledger'); - stellarApi.getAndWrapEvents = jest.fn(() => { + stellarApi.getEvents = jest.fn(() => { throw new Error('start is after newest ledger'); }); (stellarApi as any).fetchOperationsForLedger = jest.fn((seq: number) => [ - { type: { toString: () => 'invoke_host_function' } }, + {type: {toString: () => 'invoke_host_function'}}, ]); - await expect((stellarApi as any).fetchAndWrapLedger(100)).rejects.toThrow( - /(Gone|Not Found)/, - ); + await expect((stellarApi as any).fetchAndWrapLedger(100)).rejects.toThrow(/(Gone|Not Found)/); }); - it('handles a transaction with multiple operations and events', async () => { - const api = await prepareStellarApi( - 'https://horizon-testnet.stellar.org', - 'https://soroban-testnet.stellar.org', - ); + it('can extract all events from transactions', async () => { + stellarApi = await prepareStellarApi('https://stellar.api.onfinality.io/public'); + const blockHeight = await stellarApi.getBestBlockHeight(); + const [block] = await stellarApi.fetchBlocks([blockHeight]); - const [block] = await api.fetchBlocks([466592]); + const events = await stellarApi.getEvents(blockHeight); - const tx = block.block.transactions.find( - (tx) => - tx.hash === - '7967828275a8ba2442a0d4d21e8052b77ec87e8601598173e8857ad96c135685', - ); + expect(block.block.events.length).toEqual(events.length); + for (let i = 0; i < events.length; i++) { + expect(block.block.events[i].event).toEqual(events[i]); + } + }); + + it('can extract transactions from ledger', async () => { + stellarApi = await prepareStellarApi('https://stellar.api.onfinality.io/public'); + + // TODO switch back to latest block, transactions get pruned so we cannot call getTransactionsFromLedger on older blocks + const blockHeight = 60619894; //await stellarApi.getBestBlockHeight(); + const [block] = await stellarApi.fetchBlocks([blockHeight]); + + const txs = stellarApi.getTransactionsFromLedger(block.block.block); + + console.log('BLOCK HEIGHT', blockHeight); + console.log('TXS length', txs.length); + console.log('XXXXX', stellarApi.getChainId()); + expect(txs.length).toBeGreaterThan(0); + expect(txs.length).toEqual(block.block.transactions.length); + + for (let i = 0; i < txs.length; i++) { + console.log('TEST INDEX', i, block.block.transactions[i].tx.applicationOrder, txs[i].txHash); + expect(txs[i].txHash).toEqual(block.block.transactions[i].tx.txHash); + expect(txs[i].ledger).toEqual(block.block.transactions[i].tx.ledger); + expect(txs[i].createdAt).toEqual(block.block.transactions[i].tx.createdAt); + expect(txs[i].status).toEqual(block.block.transactions[i].tx.status); + + expect(txs[i].resultXdr.toXDR('base64')).toEqual(block.block.transactions[i].tx.resultXdr.toXDR('base64')); + expect(txs[i].applicationOrder).toEqual(block.block.transactions[i].tx.applicationOrder); + + // Events + expect(txs[i].diagnosticEventsXdr?.length).toEqual(block.block.transactions[i].tx.diagnosticEventsXdr?.length); + expect(txs[i].events.transactionEventsXdr.length).toEqual( + block.block.transactions[i].tx.events.transactionEventsXdr.length, + ); + expect(txs[i].events.contractEventsXdr.length).toEqual( + block.block.transactions[i].tx.events.contractEventsXdr.length, + ); + + expect(txs[i].envelopeXdr.toXDR('base64')).toEqual(block.block.transactions[i].tx.envelopeXdr.toXDR('base64')); + expect(txs[i].feeBump).toEqual(block.block.transactions[i].tx.feeBump); + + // NOT WORKING + // Seems to be non-deterministic, orders of arrays and data is different within these. + if (txs[i].resultMetaXdr.toXDR('base64') !== block.block.transactions[i].tx.resultMetaXdr.toXDR('base64')) { + console.log('MISSMATCH IN RESULT META XDR AT INDEX', i, txs[i].txHash, txs[i].resultMetaXdr.switch()); + + // // Problem with tx 4 index 5 + // expect( + // txs[i].resultMetaXdr + // .v4() + // .diagnosticEvents() + // .map((evt) => evt.toXDR('base64')), + // ).toEqual( + // block.block.transactions[i].tx.resultMetaXdr + // .v4() + // .diagnosticEvents() + // .map((evt) => evt.toXDR('base64')), + // ); + + // // Problem with tx 300, difference is buffer bs ChildUnion + // // - Expected - 8 + // // + Received + 5 + + // // - Object { + // // - "data": Array [ + // // - 0, + // // - 0, + // // - 0, + // // - 0, + // // - ], + // // - "type": "Buffer", + // // + ChildUnion { + // // + "_arm": [Function Void], + // // + "_armType": [Function Void], + // // + "_switch": 0, + // // + "_value": undefined, + // // } + // expect(txs[i].resultMetaXdr.v4().ext()).toEqual( + // block.block.transactions[i].tx.resultMetaXdr.v4().ext().toXDR(), + // ); - expect(tx).toBeDefined(); + expect( + txs[i].resultMetaXdr + .v4() + .operations() + .map((evt) => evt.toXDR('base64')), + ).toEqual( + block.block.transactions[i].tx.resultMetaXdr + .v4() + .operations() + .map((evt) => evt.toXDR('base64')), + ); + } + expect(txs[i].resultMetaXdr.toXDR('base64')).toEqual( + block.block.transactions[i].tx.resultMetaXdr.toXDR('base64'), + ); + } + }); + + it('can decode data', async () => { + // stellarApi = await prepareStellarApi('https://stellar.api.onfinality.io/public'); + // const blockHeight = 60124580; + // const [block] = await stellarApi.fetchBlocks([blockHeight]); + + // const events = await stellarApi.getEvents(60212419); + + // const txEvents = events.filter( + // (event) => (event.txHash = '8efd306b74c4b00629c88bc7d5cba75511c0197ce974ad67cbd78d7f918a4678'), + // ); + + // expect(txEvents.length).toBeGreaterThan(0); + + // const txEvents2 = txEvents.filter((evt) => + // StellarBlockWrapped.filterEventProcessor({event: evt} as any, {topics: ['transfer']}), + // ); + + const xdrData = [ + '0000000f000000087472616e73666572', + '00000012000000030000000035def769fcc519e3363f872e4fec9e5899133343f6c1a9ddc10c0e96c014c173', + '00000012000000000000000037b41577e8bf238055f590f3acf364faf930dc1d9da6d8fd451a99fe194ef20b', + '0000000e00000040524950504c45573a47414a505535454d4551423749354b574945484e46534d43514c564b524845344f433447555434505149554456454934474f484936464a4b', + ]; + + const parsedVals = xdrData.map((d) => xdr.ScVal.fromXDR(Buffer.from(d, 'hex'))); + + // const nativeVals = parsedVals.map((v, idx) => { + // console.log(`XDR VALUE ${idx}`, v); + // return scValToNative(v); + // }); + // console.log('PARSED VALUES', nativeVals); - expect(tx?.operations.length).toEqual(4); - expect(tx?.events.length).toEqual(2); + console.log('FROM VALUE', parsedVals[1].address().claimableBalanceId().value(), parsedVals[1]); + console.log('XXXXX', scValToNative(parsedVals[1])); - // Events should be correctly assigned to operations - expect(tx?.operations[0].events.length).toEqual(1); - expect(tx?.operations[1].events.length).toEqual(1); - expect(tx?.operations[2].events.length).toEqual(0); - expect(tx?.operations[3].events.length).toEqual(0); + const xdrString = '00000012000000030000000035def769fcc519e3363f872e4fec9e5899133343f6c1a9ddc10c0e96c014c173'; + const parsed = xdr.ScVal.fromXDR(Buffer.from(xdrString, 'hex')); + expect(scValToNative(parsed).toString('hex')).toBe('BA25553J7TCRTYZWH6DS4T7MTZMJSEZTIP3MDKO5YEGA5FWACTAXGHZL'); + + // for (const evt of txEvents2) { + // const xdrValues = evt.topic.map((t) => t.toXDR().toString('hex')); + // try { + // evt.topic.map((t) => scValToNative(t)); + // } catch (e) { + // console.log(`Failed to parse event`, xdrValues, e); + // throw e; + // } + // } + + // console.log('TX EVENTS', txEvents.length, txEvents2.length); }); + + // it('handles a transaction with multiple operations and events', async () => { + // const api = await prepareStellarApi('https://soroban-testnet.stellar.org'); + + // const [block] = await api.fetchBlocks([466592]); + + // const tx = block.block.transactions.find( + // (tx) => tx.hash === '7967828275a8ba2442a0d4d21e8052b77ec87e8601598173e8857ad96c135685', + // ); + + // expect(tx).toBeDefined(); + + // expect(tx?.operations.length).toEqual(4); + // expect(tx?.events.length).toEqual(2); + + // // Events should be correctly assigned to operations + // expect(tx?.operations[0].events.length).toEqual(1); + // expect(tx?.operations[1].events.length).toEqual(1); + // expect(tx?.operations[2].events.length).toEqual(0); + // expect(tx?.operations[3].events.length).toEqual(0); + // }); }); diff --git a/packages/node/src/stellar/api.stellar.ts b/packages/node/src/stellar/api.stellar.ts index 91d5874e..847fa735 100644 --- a/packages/node/src/stellar/api.stellar.ts +++ b/packages/node/src/stellar/api.stellar.ts @@ -2,81 +2,78 @@ // SPDX-License-Identifier: GPL-3.0 import assert from 'assert'; -import { Horizon, rpc } from '@stellar/stellar-sdk'; -import { getLogger, IBlock } from '@subql/node-core'; +import {rpc, xdr} from '@stellar/stellar-sdk'; +import {getLogger, IBlock} from '@subql/node-core'; import { - SorobanEvent, - StellarBlock, StellarBlockWrapper, - StellarEffect, - StellarOperation, - StellarTransaction, IStellarEndpointConfig, + StellarBlock, + StellarTransaction, + StellarOperation, + StellarEvent, } from '@subql/types-stellar'; -import { cloneDeep } from 'lodash'; -import { StellarBlockWrapped } from '../stellar/block.stellar'; +import {StellarBlockWrapped} from '../stellar/block.stellar'; import SafeStellarProvider from './safe-api'; -import { SorobanServer } from './soroban.server'; -import { StellarServer } from './stellar.server'; -import { DEFAULT_PAGE_SIZE, formatBlockUtil } from './utils.stellar'; +import { + calculateTxHash, + constructTransaction, + contractEventToEventResponse, + DEFAULT_PAGE_SIZE, + formatBlockUtil, + getBlockTimestamp, +} from './utils.stellar'; const logger = getLogger('api.Stellar'); export class StellarApi { - private stellarClient: StellarServer; - private chainId?: string; private pageLimit = DEFAULT_PAGE_SIZE; + readonly rpcClient: rpc.Server; - constructor( - private endpoint: string, - private _sorobanClient?: SorobanServer, - config?: IStellarEndpointConfig, - ) { - const { hostname, protocol, searchParams } = new URL(this.endpoint); + constructor(endpoint: string, config?: IStellarEndpointConfig) { this.pageLimit = config?.pageLimit || this.pageLimit; - const protocolStr = protocol.replace(':', ''); - - logger.info( - `Api host: ${hostname}, method: ${protocolStr}, pageLimit: ${this.pageLimit}`, - ); - if (protocolStr === 'https' || protocolStr === 'http') { - const options: Horizon.Server.Options = { - allowHttp: protocolStr === 'http', - headers: { - ...config?.headers, - }, - }; - - this.stellarClient = new StellarServer(endpoint, options); - } else { - throw new Error(`Unsupported protocol: ${protocol}`); - } + const {protocol} = new URL(endpoint); + this.rpcClient = new rpc.Server(endpoint, { + allowHttp: protocol === 'http:', + }); } async init(): Promise { + const {passphrase} = await this.rpcClient.getNetwork(); + this.chainId = passphrase; //need archive node for genesis hash - //const genesisLedger = (await this.stellarClient.ledgers().ledger(1).call()).records[0]; - this.chainId = (await this.stellarClient.getNetwork()).network_passphrase; - //this.genesisHash = genesisLedger.hash; } - get sorobanClient(): SorobanServer { - assert(this._sorobanClient, 'Soraban client is not initialized'); - return this._sorobanClient; + async getFinalizedBlock(): Promise { + const latest = await this.rpcClient.getLatestLedger(); + + return this.getLedgerForSequence(latest.sequence); } - async getFinalizedBlock(): Promise { - return (await this.stellarClient.ledgers().order('desc').call()).records[0]; + private async getLedgerForSequence(sequence: number): Promise { + const {ledgers} = await this.rpcClient.getLedgers({ + startLedger: sequence, + pagination: { + limit: 1, + }, + }); + + if (!ledgers.length) { + throw new Error(`Failed to get finalized block: ${sequence}`); + } + + return ledgers[0]; } async getFinalizedBlockHeight(): Promise { - return (await this.getFinalizedBlock()).sequence; + const {sequence} = await this.rpcClient.getLatestLedger(); + return sequence; } async getBestBlockHeight(): Promise { - return (await this.getFinalizedBlockHeight()) + 1; + // Cannot find any documentation about block finality + return this.getFinalizedBlockHeight(); } getRuntimeChain(): string { @@ -98,304 +95,334 @@ export class StellarApi { return 'Stellar'; } - private async fetchTransactionsForLedger( - sequence: number, - ): Promise { - const txs: Horizon.ServerApi.TransactionRecord[] = []; - let txsPage = await this.api - .transactions() - .forLedger(sequence) - .limit(this.pageLimit) - .call(); - while (txsPage.records.length !== 0) { - txs.push(...txsPage.records); - txsPage = await txsPage.next(); - } + private async fetchTransactionsForLedger(ledger: number): Promise { + try { + const rpcTxs: rpc.Api.TransactionInfo[] = []; + let cursor: string | undefined; + for (;;) { + const page = await this.rpcClient.getTransactions( + cursor + ? { + pagination: { + limit: this.pageLimit, + cursor, + }, + } + : { + startLedger: ledger, + pagination: { + limit: this.pageLimit, + }, + }, + ); + cursor = page.cursor; + const ledgerTxs = page.transactions.filter((tx) => tx.ledger === ledger); + if (!ledgerTxs.length) { + break; + } + rpcTxs.push(...ledgerTxs); + } - return txs; + return rpcTxs; + } catch (e: any) { + // The error throw here generally is not an instance of Error, so we need to convert it for better logging + if (!(e instanceof Error) && e.message) { + const error = new Error(e.message); + (error as any).code = e.code; + throw error; + } + throw e; + } } - private async fetchOperationsForLedger( - sequence: number, - ): Promise { - const operations: Horizon.ServerApi.OperationRecord[] = []; - let operationsPage = await this.api - .operations() - .forLedger(sequence) - .limit(this.pageLimit) - .call(); - while (operationsPage.records.length !== 0) { - operations.push(...operationsPage.records); - operationsPage = await operationsPage.next(); - } + async getEvents(height: number): Promise { + const rpcEvents: rpc.Api.EventResponse[] = []; + let cursor: string | undefined; + + for (;;) { + const res = await this.rpcClient.getEvents( + cursor + ? { + cursor, + filters: [], + limit: this.pageLimit, + } + : { + startLedger: height, // Inclusive + endLedger: height + 1, // Exclusive, setting to height gives no results + filters: [], + limit: this.pageLimit, + }, + ); - return operations; - } + cursor = res.cursor; + rpcEvents.push(...res.events.filter((event) => event.ledger === height)); - private async fetchEffectsForLedger( - sequence: number, - ): Promise { - const effects: Horizon.ServerApi.EffectRecord[] = []; - - let effectsPage = await this.api - .effects() - .forLedger(sequence) - .limit(this.pageLimit) - .call(); - while (effectsPage.records.length !== 0) { - effects.push(...effectsPage.records); - effectsPage = await effectsPage.next(); + // The last page contains events from the next ledger, so we need to stop if we see that + if (!res.events.length || res.events[res.events.length - 1].ledger > height) { + break; + } } - return effects; + return rpcEvents; } - async getAndWrapEvents(height: number): Promise { - const { events: events } = await this.sorobanClient.getEvents({ - startLedger: height, - filters: [], - limit: this.pageLimit, - }); - return events.map((event) => { - const wrappedEvent = { - ...event, - ledger: null, - transaction: null, - operation: null, - } as SorobanEvent; - - return wrappedEvent; - }); - } + /** + * Converts the events data from transaction info into the same response as getEvents + **/ + private extractEventsFromTransactions(txs: rpc.Api.TransactionInfo[], timestamp: Date): rpc.Api.EventResponse[] { + const beforeAllTxEvents: rpc.Api.EventResponse[] = []; + const allContractEvents: rpc.Api.EventResponse[] = []; // Including AfterTx transactionEvents + const afterAllTxEvents: rpc.Api.EventResponse[] = []; + let count = 0; + let afterCount = 0; + for (const tx of txs) { + tx.events.transactionEventsXdr.forEach((evt) => { + switch (evt.stage()) { + case xdr.TransactionEventStage.transactionEventStageBeforeAllTxes(): + beforeAllTxEvents.push(contractEventToEventResponse(evt.event(), tx, 0, 0, count++, timestamp)); + break; + case xdr.TransactionEventStage.transactionEventStageAfterAllTxes(): + afterAllTxEvents.push( + contractEventToEventResponse( + evt.event(), + tx, + 1048575, //tx.tx.applicationOrder - 1, + 0, + afterCount++, + timestamp, + ), + ); + break; + case xdr.TransactionEventStage.transactionEventStageAfterTx(): + allContractEvents.push( + contractEventToEventResponse(evt.event(), tx, tx.applicationOrder - 1, 0, count++, timestamp), + ); + break; + default: + throw new Error(`Unknown transaction event stage: ${evt.stage().name}`); + } + }); + const eventsFromContracts = [ + ...tx.events.contractEventsXdr.flatMap((evts, opIdx) => + evts.map((evt, eventIdx) => + contractEventToEventResponse(evt, tx, tx.applicationOrder, opIdx, eventIdx, timestamp), + ), + ), + ]; + allContractEvents.push(...eventsFromContracts); + } + // Combine all the events in the correct order + const allBlockEvents = [...beforeAllTxEvents, ...allContractEvents, ...afterAllTxEvents]; - private wrapEffect(effect: Horizon.ServerApi.EffectRecord): StellarEffect { - return { - ...effect, - ledger: null, - transaction: null, - operation: null, - }; + return allBlockEvents; } - private wrapOperationsForTx( - operations: Horizon.ServerApi.OperationRecord[], - effectsForSequence: Record, - events: SorobanEvent[], - ): StellarOperation[] { - const groupedEvts = events.reduce((acc, evt) => { - // Types are not correct, the evt also has a transactionIndex - const operationIndex = (evt as any).operationIndex as number; - acc[operationIndex] ??= []; - acc[operationIndex].push(evt); - - return acc; - }, {} as Record); - - return operations.map((op, index) => { - const effects = (effectsForSequence[op.id] ?? []).map( - this.wrapEffect.bind(this), - ); - // const effects = this.wrapEffectsForOperation(index, effectsForSequence); - - const wrappedOp: StellarOperation = { - ...op, - ledger: null, - transaction: null, - effects: [], - events: groupedEvts[index] ?? [], - }; - - const clonedOp = cloneDeep(wrappedOp); - - effects.forEach((effect) => { - effect.operation = clonedOp; - wrappedOp.effects.push(effect); + private parseLedgerV0Meta(ledger: rpc.Api.LedgerResponse, meta: xdr.LedgerCloseMetaV0): rpc.Api.TransactionInfo[] { + // Not tested on a block that is v0 + return meta + .txSet() + .txes() + .map((tx, idx) => { + return constructTransaction(ledger, meta, tx, idx); }); - - return wrappedOp; - }); } - private wrapTransactionsForLedger( - transactions: Horizon.ServerApi.TransactionRecord[], - operationsForSequence: Horizon.ServerApi.OperationRecord[], - effectsForSequence: Horizon.ServerApi.EffectRecord[], - eventsForSequence: SorobanEvent[], - ): StellarTransaction[] { - return transactions.map((tx) => { - const wrappedTx: StellarTransaction = { - ...tx, - ledger: null, - operations: [] as StellarOperation[], - effects: [] as StellarEffect[], - events: [] as SorobanEvent[], - }; - - // Effects grouped by the operation id - const groupedEffects = effectsForSequence.reduce((acc, item) => { - // BigInt conversion is for stripping leading 0s - const key = BigInt(item.id.split('-')[0]).toString(); - acc[key] ??= []; - acc[key].push(item); - - return acc; - }, {} as Record); - - // Operations grouped by the transaction hash - const groupedOperations = operationsForSequence.reduce((acc, item) => { - acc[item.transaction_hash] ??= []; - acc[item.transaction_hash].push(item); + private parseLedgerV1V2Meta( + ledger: rpc.Api.LedgerResponse, + meta: xdr.LedgerCloseMetaV2 | xdr.LedgerCloseMetaV1, + ): rpc.Api.TransactionInfo[] { + const ver = meta.txSet().switch(); + if (ver !== 1) { + throw new Error('Unsupported tx set version'); + } + // Extract all the envelopes and index by hash, this is really slow calculating the hash for each tx, + // but it is needed to get the order right to match with the txProcessing info. + const envelopes: Record = meta + .txSet() + .v1TxSet() + .phases() + .flatMap((phase) => { + switch (phase.switch()) { + case 0: { + return phase.v0Components().flatMap((comp) => { + if (comp.switch() !== xdr.TxSetComponentType.txsetCompTxsMaybeDiscountedFee()) { + throw new Error(`Unhandled component type: ${comp.switch().name}`); + } + + return comp.txsMaybeDiscountedFee().txes(); + }); + } + case 1: { + return phase + .parallelTxsComponent() + .executionStages() + .flatMap((stage) => { + return stage.flatMap((envelopes) => envelopes); + }); + } + default: { + throw new Error(`Unsupported phase type: ${phase.switch()}`); + } + } + }) + .reduce((acc, value) => { + const hash = calculateTxHash(value, this.getChainId()); + acc[hash] = value; return acc; - }, {} as Record); - - // Events grouped by the transaction hash - const groupedEvents = eventsForSequence.reduce((acc, item) => { - acc[item.txHash] ??= []; - acc[item.txHash].push(item); + }, {}); - return acc; - }, {} as Record); - - const clonedTx = cloneDeep(wrappedTx); - const operations = this.wrapOperationsForTx( - // TODO, this include other attribute from HorizonApi.TransactionResponse, but type assertion incorrect - // TransactionRecord extends Omit - groupedOperations[tx.hash] ?? [], - groupedEffects, - groupedEvents[tx.hash] ?? [], - ).map((op) => { - op.transaction = clonedTx; - op.effects = op.effects.map((effect) => { - effect.transaction = clonedTx; - return effect; - }); - op.events = op.events.map((event) => { - event.transaction = clonedTx; - return event; - }); - return op; - }); + return meta.txProcessing().map((txp, index) => { + const txHash = txp.result().transactionHash().toString('hex'); + const txEnvelope = envelopes[txHash]; + if (!txEnvelope) { + throw new Error(`Unable to find envelope for hash: ${txHash}`); + } + return constructTransaction(ledger, meta, txEnvelope, index); + }); - wrappedTx.operations.push(...operations); - operations.forEach((op) => { - wrappedTx.effects.push(...op.effects); - wrappedTx.events.push(...op.events); + let idx = 0; + // Itterate over the transactions, but they are not in the application order. + // TODO awaiting clarification in Stellar TG chat + return meta + .txSet() + .v1TxSet() + .phases() + .flatMap((phase) => { + switch (phase.switch()) { + case 0: { + return phase.v0Components().flatMap((comp) => { + if (comp.switch() !== xdr.TxSetComponentType.txsetCompTxsMaybeDiscountedFee()) { + throw new Error(`Unhandled component type: ${comp.switch().name}`); + } + + return comp + .txsMaybeDiscountedFee() + .txes() + .map((txEnvelope) => { + idx++; + return constructTransaction(ledger, meta, txEnvelope, idx - 1); + }); + }); + } + case 1: { + return phase + .parallelTxsComponent() + .executionStages() + .flatMap((stage) => { + return stage.flatMap((envelopes) => { + return envelopes.map((txEnvelope) => { + idx++; + return constructTransaction(ledger, meta, txEnvelope, idx - 1); + }); + }); + }); + } + default: { + throw new Error(`Unsupported phase type: ${phase.switch()}`); + } + } }); + } - return wrappedTx; - }); + getTransactionsFromLedger(ledger: rpc.Api.LedgerResponse): rpc.Api.TransactionInfo[] { + switch (ledger.metadataXdr.switch()) { + case 0: + // NOTE: this is untested + return this.parseLedgerV0Meta(ledger, ledger.metadataXdr.v0()); + case 1: + case 2: + return this.parseLedgerV1V2Meta(ledger, ledger.metadataXdr.v2()); + default: { + throw new Error(`Unsupported ledger version: ${ledger.metadataXdr.switch()}`); + } + } } - private async fetchAndWrapLedger( - sequence: number, - ): Promise> { - const [ledger, transactions, operations, effects] = await Promise.all([ - this.api.ledgers().ledger(sequence).call(), + private async fetchAndWrapLedger(sequence: number): Promise> { + // TODO stop using fetchTransactionsForLedger and instead use getTransactionsFromLedger, this requires more work. + const [ledger, transactions] = await Promise.all([ + this.getLedgerForSequence(sequence), this.fetchTransactionsForLedger(sequence), - this.fetchOperationsForLedger(sequence), - this.fetchEffectsForLedger(sequence), + // this.getEvents(sequence), ]); - let eventsForSequence: SorobanEvent[] = []; - - //check if there is InvokeHostFunctionOp operation - //If yes then, there are soroban transactions and we should we fetch soroban events - const hasInvokeHostFunctionOp = operations.some( - (op) => op.type.toString() === 'invoke_host_function', - ); - - if (this.sorobanClient && hasInvokeHostFunctionOp) { - try { - eventsForSequence = await this.getAndWrapEvents(sequence); - } catch (e: any) { - if (e.message === 'start is after newest ledger') { - const latestLedger = (await this.sorobanClient.getLatestLedger()) - .sequence; - throw new Error(`The requested events for ledger number ${sequence} is not available on the current soroban node. - This is because you're trying to access a ledger that is after the latest ledger number ${latestLedger} stored in this node. - To resolve this issue, please check you endpoint node start height`); - } - - if (e.message === 'start is before oldest ledger') { - throw new Error(`The requested events for ledger number ${sequence} is not available on the current soroban node. - This is because you're trying to access a ledger that is older than the oldest ledger stored in this node. - To resolve this issue, you can either: - 1. Increase the start ledger to a more recent one, or - 2. Connect to a different node that might have a longer history of ledgers.`); - } - - throw e; - } - } + const events = this.extractEventsFromTransactions(transactions, getBlockTimestamp(ledger)); const wrappedLedger: StellarBlock = { - ...(ledger as unknown as Horizon.ServerApi.LedgerRecord), - transactions: [] as StellarTransaction[], - operations: [] as StellarOperation[], - effects: [] as StellarEffect[], - events: eventsForSequence, + ...ledger, + transactions: [], + operations: [], + events: [], }; - const wrapperTxs = this.wrapTransactionsForLedger( - transactions, - operations, - effects, - eventsForSequence, - ); + wrappedLedger.transactions = transactions.map((tx, index) => { + return { + tx, + events: events.filter((event) => event.transactionIndex === index), + } satisfies StellarTransaction; + }); - const clonedLedger = cloneDeep(wrappedLedger); - - wrapperTxs.forEach((tx) => { - tx.ledger = clonedLedger; - tx.operations = tx.operations.map((op) => { - op.ledger = clonedLedger; - op.effects = op.effects.map((effect) => { - effect.ledger = clonedLedger; - return effect; - }); - op.events = op.events.map((event) => { - event.ledger = clonedLedger; - return event; - }); - return op; + wrappedLedger.operations = transactions.flatMap((tx, txIndex) => { + const extractOperations = (tx: rpc.Api.TransactionInfo): xdr.Operation[] => { + switch (tx.envelopeXdr.switch()) { + case xdr.EnvelopeType.envelopeTypeTxV0(): { + return tx.envelopeXdr.v0().tx().operations(); + } + case xdr.EnvelopeType.envelopeTypeTx(): { + return tx.envelopeXdr.v1().tx().operations(); + } + case xdr.EnvelopeType.envelopeTypeTxFeeBump(): { + return tx.envelopeXdr.feeBump().tx().innerTx().v1().tx().operations(); + } + default: { + logger.warn( + `Unable to extract operations for transaction ${tx.txHash} with type ${tx.envelopeXdr.switch().name}`, + ); + return []; + } + } + }; + return extractOperations(tx).map((op, index) => { + return { + index, + // TODO feeBump transactions can have an inner transaction, should this be set here instead? + transaction: wrappedLedger.transactions[txIndex], + operation: op, + } satisfies StellarOperation; }); + }); - wrappedLedger.transactions.push(tx); - wrappedLedger.operations.push(...tx.operations); + wrappedLedger.events = events.map((event) => { + const tx = transactions.find((tx) => tx.txHash === event.txHash); + if (!tx) { + throw new Error(`Unable to find matching transaction for squence ${sequence}, index ${event.transactionIndex}`); + } - tx.operations.forEach((op) => { - wrappedLedger.effects.push(...op.effects); - }); + return { + event, + block: wrappedLedger, + tx, + } satisfies StellarEvent; }); const wrappedLedgerInstance = new StellarBlockWrapped( wrappedLedger, wrappedLedger.transactions, wrappedLedger.operations, - wrappedLedger.effects, wrappedLedger.events, ); return formatBlockUtil(wrappedLedgerInstance); } - async fetchBlocks( - bufferBlocks: number[], - ): Promise[]> { - const ledgers = await Promise.all( - bufferBlocks.map((sequence) => this.fetchAndWrapLedger(sequence)), - ); - return ledgers; - } - - get api(): Horizon.Server { - return this.stellarClient; + async fetchBlocks(bufferBlocks: number[]): Promise[]> { + return Promise.all(bufferBlocks.map((sequence) => this.fetchAndWrapLedger(sequence))); } getSafeApi(blockHeight: number): SafeStellarProvider { - //safe api not implemented yet - return new SafeStellarProvider(this.sorobanClient, blockHeight); + return new SafeStellarProvider(this.rpcClient, blockHeight); } // eslint-disable-next-line @typescript-eslint/require-await diff --git a/packages/node/src/stellar/block.stellar.spec.ts b/packages/node/src/stellar/block.stellar.spec.ts index e42ca1ba..db9b9f3f 100644 --- a/packages/node/src/stellar/block.stellar.spec.ts +++ b/packages/node/src/stellar/block.stellar.spec.ts @@ -1,27 +1,78 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { nativeToScVal, Contract, xdr, Horizon } from '@stellar/stellar-sdk'; +import {nativeToScVal, Contract, xdr, rpc, Operation} from '@stellar/stellar-sdk'; import { - StellarBlock, StellarBlockFilter, - StellarEffect, - StellarEffectFilter, - SorobanEvent, - SorobanEventFilter, - StellarOperation, StellarOperationFilter, StellarTransaction, StellarTransactionFilter, + StellarOperation, + StellarEvent, + StellarBlock, } from '@subql/types-stellar'; -import { StellarBlockWrapped } from './block.stellar'; +import {StellarBlockWrapped} from './block.stellar'; const testAddress = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; +const testSourceAccount = 'GAJ7ZNAZOWGPIFOEHBPQZIHQQM265OIUY55E6CBFRO56JKGBB7J7U7XI'; describe('StellarBlockWrapped', () => { + let tx: StellarTransaction; + + beforeEach(async () => { + const server = new rpc.Server('https://rpc-futurenet.stellar.org'); + + // Use a simple valid XDR string for testing + const metaV4Xdr = + 'AAAAAgAAAAIAAAADAtL5awAAAAAAAAAAS0CFMhOtWUKJWerx66zxkxORaiH6/3RUq7L8zspD5RoAAAAAAcm9QAKVkpMAAHpMAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAC0vi5AAAAAGTB02oAAAAAAAAAAQLS+WsAAAAAAAAAAEtAhTITrVlCiVnq8eus8ZMTkWoh+v90VKuy/M7KQ+UaAAAAAAHJvUAClZKTAAB6TQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL5awAAAABkwdd1AAAAAAAAAAEAAAAGAAAAAwLS+VQAAAACAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAAEySDkgAAAAAAAAAAkJVU0lORVNTAAAAAAAAAAC3JfDeo9vreItKNPoe74EkFIqWybeUQNFvLvURhHtskAAAAAAeQtHTL5f6TAAAXH0AAAAAAAAAAAAAAAAAAAABAtL5awAAAAIAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAAATJIOSAAAAAAAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAB5C0dNHf4CAAACLCQAAAAAAAAAAAAAAAAAAAAMC0vlUAAAAAQAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAAJCVVNJTkVTUwAAAAAAAAAAtyXw3qPb63iLSjT6Hu+BJBSKlsm3lEDRby71EYR7bJAAAAAAAABAL3//////////AAAAAQAAAAEAE3H3TnhnuQAAAAAAAAAAAAAAAAAAAAAAAAABAtL5awAAAAEAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAAAAQC9//////////wAAAAEAAAABABNx9J6Z4RkAAAAAAAAAAAAAAAAAAAAAAAAAAwLS+WsAAAAAAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAH37+zXQCXdRTAAASZAAAApIAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAABbBXKIigAAABhZWyiOAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL0awAAAABkwbqrAAAAAAAAAAEC0vlrAAAAAAAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAB9+/s10Al3UUwAAEmQAAAKSAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAWwVyiIoAAAAYWVsojgAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAALS9GsAAAAAZMG6qwAAAAAAAAAA'; + + // only injected in the success case + // + // this data was picked from a random transaction in horizon: + // aa6a8e198abe53c7e852e4870413b29fe9ef04da1415a97a5de1a4ae489e11e2 + + const rawTxInfo = { + status: rpc.Api.GetTransactionStatus.SUCCESS, + txHash: 'ae9f315c048d87a5f853bc15bf284a2c3c89eb0e1cb38c10409b77a877b830a8', + latestLedger: 100, + latestLedgerCloseTime: 12345, + oldestLedger: 50, + oldestLedgerCloseTime: 500, + ledger: 1234, + createdAt: 123456789010, + applicationOrder: 2, + feeBump: false, + envelopeXdr: + 'AAAAAgAAAAAT/LQZdYz0FcQ4Xwyg8IM17rkUx3pPCCWLu+SowQ/T+gBLB24poiQa9iwAngAAAAEAAAAAAAAAAAAAAABkwdeeAAAAAAAAAAEAAAABAAAAAC/9E8hDhnktyufVBS5tqA734Yz5XrLX2XNgBgH/YEkiAAAADQAAAAAAAAAAAAA1/gAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA1/gAAAAQAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AAAACUEFMTEFESVVNAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAAAAAAAACwQ/T+gAAAEA+ztVEKWlqHXNnqy6FXJeHr7TltHzZE6YZm5yZfzPIfLaqpp+5cyKotVkj3d89uZCQNsKsZI48uoyERLne+VwL/2BJIgAAAEA7323gPSaezVSa7Vi0J4PqsnklDH1oHLqNBLwi5EWo5W7ohLGObRVQZ0K0+ufnm4hcm9J4Cuj64gEtpjq5j5cM', + resultXdr: + 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAUAAAACZ4W6fmN63uhVqYRcHET+D2NEtJvhCIYflFh9GqtY+AwAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAYW0toL2gAAAAAAAAAAAAANf4AAAACcgyAkXD5kObNTeRYciLh7R6ES/zzKp0n+cIK3Y6TjBkAAAABU0dYAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGlGnIJrXAAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGFtLaC9oAAAAApmc7UgUBInrDvij8HMSridx2n1w3I8TVEn4sLr1LSpmAAAAAlBBTExBRElVTQAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAIUz88EqYAAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABpRpyCa1wAAAAKYUsaaCZ233xB1p+lG7YksShJWfrjsmItbokiR3ifa0gAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAJQQUxMQURJVU0AAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AACFM/PBKmAAAAAJnhbp+Y3re6FWphFwcRP4PY0S0m+EIhh+UWH0aq1j4DAAAAAAAAAAAAAA9pAAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA9pAAAAAA=', + resultMetaXdr: metaV4Xdr, + events: { + contractEventsXdr: [], + transactionEventsXdr: [], + }, + }; + // const txInfo: rpc.Api.TransactionInfo = { + // status: rpc.Api.GetTransactionStatus.SUCCESS, + // txHash: 'ae9f315c048d87a5f853bc15bf284a2c3c89eb0e1cb38c10409b77a877b830a8', + // ...parseTransactionInfo(), + // }; + + jest.spyOn(server, '_getTransactions').mockResolvedValue({transactions: [rawTxInfo]} as any); + + const { + transactions: [txInfo], + } = await server.getTransactions({} as any); + + tx = { + tx: txInfo, + // operations: [], + events: [], + }; + }); describe('filterBlocksProcessor', () => { it('should filter by modulo', () => { - const block: StellarBlock = { sequence: 5 } as unknown as StellarBlock; - const filter: StellarBlockFilter = { modulo: 2 }; + const block = {sequence: 5} as unknown as StellarBlock; + const filter: StellarBlockFilter = {modulo: 2}; const result = StellarBlockWrapped.filterBlocksProcessor(block, filter); @@ -31,177 +82,112 @@ describe('StellarBlockWrapped', () => { describe('filterTransactionProcessor', () => { it('should filter by account', () => { - const transaction: StellarTransaction = { - source_account: 'account1', - } as unknown as StellarTransaction; - const filter: StellarTransactionFilter = { account: 'account2' }; + const filter: StellarTransactionFilter = {account: 'BadAccount'}; - const result = StellarBlockWrapped.filterTransactionProcessor( - transaction, - filter, - ); + const result = StellarBlockWrapped.filterTransactionProcessor(tx, filter); expect(result).toBe(false); }); it('should pass when account filter condition is fulfilled', () => { - const transaction: StellarTransaction = { - source_account: 'account1', - } as unknown as StellarTransaction; - const filter: StellarTransactionFilter = { account: 'account1' }; + const filter: StellarTransactionFilter = {account: testSourceAccount}; - const result = StellarBlockWrapped.filterTransactionProcessor( - transaction, - filter, - ); + const result = StellarBlockWrapped.filterTransactionProcessor(tx, filter); expect(result).toBe(true); }); it('should pass when there is no account filter', () => { - const transaction: StellarTransaction = { - source_account: 'account1', - } as unknown as StellarTransaction; const filter: StellarTransactionFilter = {}; - const result = StellarBlockWrapped.filterTransactionProcessor( - transaction, - filter, - ); + const result = StellarBlockWrapped.filterTransactionProcessor(tx, filter); expect(result).toBe(true); }); }); describe('filterOperationProcessor', () => { + let operation: StellarOperation; + + beforeEach(() => { + const op = Operation.createAccount({ + destination: testSourceAccount, + startingBalance: '50', + }); + + operation = { + index: 0, + operation: op, + transaction: tx, + }; + }); + it('should filter by source_account and type', () => { - const operation: StellarOperation = { - source_account: 'account1', - type: 'type1', - } as unknown as StellarOperation; const filter: StellarOperationFilter = { sourceAccount: 'account2', - type: 'create_account' as Horizon.HorizonApi.OperationResponseType, + type: xdr.OperationType.createAccount().name, }; - const result = StellarBlockWrapped.filterOperationProcessor( - operation, - filter, - ); + const result = StellarBlockWrapped.filterOperationProcessor(operation, filter); expect(result).toBe(false); }); it('should pass when source_account and type filter conditions are fulfilled', () => { - const operation: StellarOperation = { - source_account: 'account1', - type: Horizon.HorizonApi.OperationResponseType.createAccount, - } as unknown as StellarOperation; const filter: StellarOperationFilter = { - sourceAccount: 'account1', - type: Horizon.HorizonApi.OperationResponseType.createAccount, + sourceAccount: testSourceAccount, + type: xdr.OperationType.createAccount().name, }; - const result = StellarBlockWrapped.filterOperationProcessor( - operation, - filter, - ); + const result = StellarBlockWrapped.filterOperationProcessor(operation, filter); expect(result).toBe(true); }); it('should pass when there are no filter conditions', () => { - const operation: StellarOperation = { - source_account: 'account1', - type: 'type1', - } as unknown as StellarOperation; const filter: StellarOperationFilter = {}; - const result = StellarBlockWrapped.filterOperationProcessor( - operation, - filter, - ); - - expect(result).toBe(true); - }); - }); - - describe('filterEffectProcessor', () => { - it('should filter by account and type', () => { - const effect: StellarEffect = { - account: 'account1', - type: 'type1', - } as unknown as StellarEffect; - const filter: StellarEffectFilter = { - account: 'account2', - type: 'type2', - }; - - const result = StellarBlockWrapped.filterEffectProcessor(effect, filter); - - expect(result).toBe(false); - }); - - it('should pass when account and type filter conditions are fulfilled', () => { - const effect: StellarEffect = { - account: 'account1', - type: 'type1', - } as unknown as StellarEffect; - const filter: StellarEffectFilter = { - account: 'account1', - type: 'type1', - }; - - const result = StellarBlockWrapped.filterEffectProcessor(effect, filter); - - expect(result).toBe(true); - }); - - it('should pass when there are no filter conditions', () => { - const effect: StellarEffect = { - account: 'account1', - type: 'type1', - } as unknown as StellarEffect; - const filter: StellarEffectFilter = {}; - - const result = StellarBlockWrapped.filterEffectProcessor(effect, filter); + const result = StellarBlockWrapped.filterOperationProcessor(operation, filter); expect(result).toBe(true); }); }); + // TODO these tests need to be fixed, the mockEvent has changed so all the filters need updating describe('StellarBlockWrapped', function () { - const topic1 = nativeToScVal('topic1'); - const topic2 = nativeToScVal('topic2'); - - const mockEvent: SorobanEvent = { - txHash: '', - type: 'contract', - ledger: null, - transaction: null, - operation: null, - ledgerClosedAt: '', - contractId: new Contract(testAddress), - id: 'mockId', - pagingToken: '', - inSuccessfulContractCall: true, - topic: [topic1, topic2], - value: {} as xdr.ScVal, - }; - - const mockEventFilterValid: SorobanEventFilter = { - topics: ['topic1', 'topic2'], - }; - - const mockEventFilterInvalid: SorobanEventFilter = { - topics: ['topics3'], + const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; + const topicVals = [ + nativeToScVal('transfer', {type: 'symbol'}), + nativeToScVal(contractId, {type: 'address'}), + nativeToScVal(1234), + ]; + + const mockEvent: StellarEvent = { + event: { + type: 'contract', + ledger: 2, + ledgerClosedAt: '2022-11-16T16:10:41Z', + transactionIndex: 0, + operationIndex: 0, + contractId: new Contract(contractId), + id: '0164090849041387521-0000000003', + // cursor: '164090849041387521-3', + inSuccessfulContractCall: true, + topic: topicVals.slice(0, 2), + value: nativeToScVal('wassup'), + txHash: 'd7d09af2ca4f2929ee701cf86d05e4ca5f849a726d0db344785a8f9894e79e6c', + }, + block: undefined as any, + tx: undefined as any, }; it('should pass filter - valid address and topics', function () { expect( StellarBlockWrapped.filterEventProcessor( mockEvent, - mockEventFilterValid, + { + topics: ['transfer', contractId], + }, testAddress, ), ).toEqual(true); @@ -209,10 +195,9 @@ describe('StellarBlockWrapped', () => { it('should pass filter - no address and valid topics', function () { expect( - StellarBlockWrapped.filterEventProcessor( - mockEvent, - mockEventFilterValid, - ), + StellarBlockWrapped.filterEventProcessor(mockEvent, { + topics: ['transfer', contractId], + }), ).toEqual(true); }); @@ -220,39 +205,36 @@ describe('StellarBlockWrapped', () => { expect( StellarBlockWrapped.filterEventProcessor( mockEvent, - mockEventFilterInvalid, + { + topics: ['topics3'], + }, testAddress, ), ).toEqual(false); }); it('should fail filter - event not found', function () { - mockEventFilterInvalid.topics = ['topic1', 'topic2', 'topic3']; expect( - StellarBlockWrapped.filterEventProcessor( - mockEvent, - mockEventFilterInvalid, - ), + StellarBlockWrapped.filterEventProcessor(mockEvent, { + topics: ['topic1', 'topic2', 'topic3'], + }), ).toEqual(false); }); it('should pass filter - skip null topics', function () { - mockEventFilterValid.topics = ['', 'topic2']; expect( - StellarBlockWrapped.filterEventProcessor( - mockEvent, - mockEventFilterValid, - ), + StellarBlockWrapped.filterEventProcessor(mockEvent, { + topics: ['', contractId], + }), ).toEqual(true); }); it('should pass filer - valid contractId', function () { - mockEventFilterValid.contractId = testAddress; expect( - StellarBlockWrapped.filterEventProcessor( - mockEvent, - mockEventFilterValid, - ), + StellarBlockWrapped.filterEventProcessor(mockEvent, { + topics: ['transfer', testAddress], + contractId: testAddress, + }), ).toEqual(true); }); @@ -268,7 +250,9 @@ describe('StellarBlockWrapped', () => { expect( StellarBlockWrapped.filterEventProcessor( mockEvent, - mockEventFilterValid, + { + topics: ['topic1', 'topic2'], + }, 'invalidaddress', ), ).toEqual(false); diff --git a/packages/node/src/stellar/block.stellar.ts b/packages/node/src/stellar/block.stellar.ts index cd2abc54..a711a049 100644 --- a/packages/node/src/stellar/block.stellar.ts +++ b/packages/node/src/stellar/block.stellar.ts @@ -1,31 +1,30 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { scValToNative } from '@stellar/stellar-sdk'; -import { filterBlockTimestamp } from '@subql/node-core'; +import {scValToNative} from '@stellar/stellar-sdk'; +import {filterBlockTimestamp} from '@subql/node-core'; import { - StellarBlock, StellarBlockFilter, StellarBlockWrapper, - StellarEffect, - StellarEffectFilter, - SorobanEvent, - SorobanEventFilter, - StellarOperation, + StellarEventFilter, StellarOperationFilter, - StellarTransaction, StellarTransactionFilter, + StellarBlock, + StellarTransaction, + StellarOperation, + StellarEvent, + getTransactionSourceAccount, } from '@subql/types-stellar'; -import { SubqlProjectBlockFilter } from '../configure/SubqueryProject'; -import { stringNormalizedEq } from '../utils/string'; +import {SubqlProjectBlockFilter} from '../configure/SubqueryProject'; +import {stringNormalizedEq} from '../utils/string'; +import {getBlockTimestamp} from './utils.stellar'; export class StellarBlockWrapped implements StellarBlockWrapper { constructor( private _block: StellarBlock, private _transactions: StellarTransaction[], private _operations: StellarOperation[], - private _effects: StellarEffect[], - private _events: SorobanEvent[], + private _events: StellarEvent[], ) {} get block(): StellarBlock { @@ -40,29 +39,16 @@ export class StellarBlockWrapped implements StellarBlockWrapper { return this._operations; } - get effects(): StellarEffect[] { - return this._effects; - } - - get events(): SorobanEvent[] { + get events(): StellarEvent[] { return this._events; } - static filterBlocksProcessor( - block: StellarBlock, - filter: StellarBlockFilter, - address?: string, - ): boolean { + static filterBlocksProcessor(block: StellarBlock, filter: StellarBlockFilter): boolean { if (!filter) return true; if (filter?.modulo && block.sequence % filter.modulo !== 0) { return false; } - if ( - !filterBlockTimestamp( - new Date(block.closed_at).getTime(), - filter as SubqlProjectBlockFilter, - ) - ) { + if (!filterBlockTimestamp(getBlockTimestamp(block).getTime(), filter as SubqlProjectBlockFilter)) { return false; } return true; @@ -74,60 +60,37 @@ export class StellarBlockWrapped implements StellarBlockWrapper { address?: string, ): boolean { if (!filter) return true; - if (filter.account && filter.account !== (tx as any).source_account) { - return false; - } - return true; - } - - static filterOperationProcessor( - op: StellarOperation, - filter: StellarOperationFilter, - address?: string, - ): boolean { - if (!filter) return true; - if (filter.sourceAccount && filter.sourceAccount !== op.source_account) { - return false; - } - if (filter.type && filter.type !== op.type) { + const sourceAccount = getTransactionSourceAccount(tx.tx); + if (filter.account && filter.account !== sourceAccount) { return false; } return true; } - static filterEffectProcessor( - effect: StellarEffect, - filter: StellarEffectFilter, - address?: string, - ): boolean { + static filterOperationProcessor(op: StellarOperation, filter: StellarOperationFilter): boolean { if (!filter) return true; - if (filter.account && filter.account !== effect.account) { + + if (filter.type && filter.type !== op.operation.body().switch().name) { return false; } - if (filter.type && filter.type !== effect.type) { + const sourceAccount = getTransactionSourceAccount(op.transaction.tx); + if (filter.sourceAccount && sourceAccount !== null && filter.sourceAccount !== sourceAccount) { return false; } return true; } - static filterEventProcessor( - event: SorobanEvent, - filter: SorobanEventFilter, - address?: string, - ): boolean { - if (address && !stringNormalizedEq(address, event.contractId?.toString())) { + static filterEventProcessor(event: StellarEvent, filter: StellarEventFilter, address?: string): boolean { + if (address && !stringNormalizedEq(address, event.event.contractId?.toString())) { return false; } if (!filter) return true; - if ( - filter.contractId && - filter.contractId !== event.contractId?.toString() - ) { + if (filter.contractId && filter.contractId !== event.event.contractId?.toString()) { return false; } @@ -138,10 +101,10 @@ export class StellarBlockWrapped implements StellarBlockWrapper { continue; } - if (!event.topic[i]) { + if (!event.event.topic[i]) { return false; } - if (topic !== scValToNative(event.topic[i])) { + if (topic !== scValToNative(event.event.topic[i])) { return false; } } diff --git a/packages/node/src/stellar/safe-api.ts b/packages/node/src/stellar/safe-api.ts index c3db09a7..ae9dd569 100644 --- a/packages/node/src/stellar/safe-api.ts +++ b/packages/node/src/stellar/safe-api.ts @@ -1,23 +1,12 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { - Account, - Address, - Contract, - FeeBumpTransaction, - Transaction, - xdr, - rpc, -} from '@stellar/stellar-sdk'; -import { getLogger } from '@subql/node-core'; - -// import { Durability } from 'soroban-client/lib/server'; -import { SorobanServer } from './soroban.server'; +import {Account, Address, Contract, FeeBumpTransaction, Transaction, xdr, rpc} from '@stellar/stellar-sdk'; +import {getLogger} from '@subql/node-core'; const logger = getLogger('safe.api.stellar'); -export default class SafeStellarProvider extends SorobanServer { +export default class SafeStellarProvider extends rpc.Server { private blockHeight: number; private baseApi: rpc.Server; @@ -48,9 +37,7 @@ export default class SafeStellarProvider extends SorobanServer { // @ts-ignore // eslint-disable-next-line @typescript-eslint/require-await - async getLedgerEntries( - keys: xdr.LedgerKey[], - ): Promise { + async getLedgerEntries(keys: xdr.LedgerKey[]): Promise { throw new Error('Method getLedgerEntries is not implemented.'); } @@ -59,12 +46,16 @@ export default class SafeStellarProvider extends SorobanServer { throw new Error('Method getTransaction is not implemented.'); } - async getEvents( - request: rpc.Server.GetEventsRequest, - ): Promise { + async getEvents(request: rpc.Server.GetEventsRequest): Promise { + if (request.cursor) { + return this.baseApi.getEvents(request); + } + + const {cursor, ...rest} = request; return this.baseApi.getEvents({ + ...rest, startLedger: this.blockHeight, - filters: [], + endLedger: Math.max(this.blockHeight, request.endLedger!), }); } @@ -86,24 +77,17 @@ export default class SafeStellarProvider extends SorobanServer { } //eslint-disable-next-line @typescript-eslint/require-await - async prepareTransaction( - transaction: Transaction | FeeBumpTransaction, - ): Promise { + async prepareTransaction(transaction: Transaction | FeeBumpTransaction): Promise { throw new Error('Method prepareTransaction is not implemented.'); } //eslint-disable-next-line @typescript-eslint/require-await - async sendTransaction( - transaction: Transaction | FeeBumpTransaction, - ): Promise { + async sendTransaction(transaction: Transaction | FeeBumpTransaction): Promise { throw new Error('Method sendTransaction is not implemented.'); } //eslint-disable-next-line @typescript-eslint/require-await - async requestAirdrop( - address: string | Pick, - friendbotUrl?: string, - ): Promise { + async requestAirdrop(address: string | Pick, friendbotUrl?: string): Promise { throw new Error('Method requestAirdrop is not implemented.'); } } diff --git a/packages/node/src/stellar/soroban.server.spec.ts b/packages/node/src/stellar/soroban.server.spec.ts deleted file mode 100644 index bd789f5d..00000000 --- a/packages/node/src/stellar/soroban.server.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0 - -import { rpc } from '@stellar/stellar-sdk'; -import { SorobanServer } from './soroban.server'; -import { DEFAULT_PAGE_SIZE } from './utils.stellar'; - -describe('SorobanServer', () => { - let server: SorobanServer; - const url = 'https://example.com'; - let spy: jest.SpyInstance; - - beforeEach(() => { - server = new SorobanServer(url); - spy = jest.spyOn(rpc.Server.prototype, 'getEvents'); - }); - - afterEach(() => { - spy.mockRestore(); - }); - - test('should handle no events', async () => { - spy.mockResolvedValue({ events: [] }); - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response).toEqual({ events: [] }); - expect(spy).toHaveBeenCalledTimes(1); - }); - - test('should handle events from different ledgers', async () => { - // Should be BaseEventResponse type - spy.mockResolvedValue({ - events: [ - { id: '1', ledger: 1, pagingToken: '1' }, - { id: '2', ledger: 2, pagingToken: '2' }, - ], - latestLedger: 5, - }); - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response).toEqual({ - events: [{ id: '1', ledger: 1, pagingToken: '1' }], - latestLedger: 5, - }); - expect(spy).toHaveBeenCalledTimes(1); - }); - - test('should handle response length less than DEFAULT_PAGE_SIZE', async () => { - spy.mockResolvedValue({ - events: Array.from({ length: DEFAULT_PAGE_SIZE }, (_, i) => ({ - id: `${i}`, - ledger: i < DEFAULT_PAGE_SIZE - 1 ? 1 : 2, - })), - }); - - const response = await server.getEvents({ - startLedger: 1, - filters: [], - }); - - expect(response.events.length).toBe(DEFAULT_PAGE_SIZE - 1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - test('should handle no matching ledger', async () => { - spy.mockResolvedValue({ - events: [ - { id: '1', ledger: 2, pagingToken: '1' }, - { id: '1', ledger: 3, pagingToken: '2' }, - ], - }); - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response).toEqual({ events: [] }); - expect(spy).toHaveBeenCalledTimes(1); - }); - - test('should return cached events for given startLedger', async () => { - (server as any).eventsCache[1] = { - events: [{ ledger: 1, pagingToken: '1' }], - }; - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response).toEqual({ events: [{ ledger: 1, pagingToken: '1' }] }); - expect(spy).toHaveBeenCalledTimes(0); - }); - - test('should handle startLedger events greater than DEFAULT_PAGE_SIZE', async () => { - spy - .mockResolvedValueOnce({ - events: Array.from({ length: DEFAULT_PAGE_SIZE }, (_, i) => ({ - id: `${i}`, - ledger: 1, - })), - }) - .mockResolvedValueOnce({ - events: Array.from({ length: DEFAULT_PAGE_SIZE }, (_, i) => ({ - id: `${i + DEFAULT_PAGE_SIZE}`, - ledger: i < 5 ? 1 : 2, - })), - }); - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response.events.length).toBe(DEFAULT_PAGE_SIZE + 5); - expect(spy).toHaveBeenCalledTimes(2); - }); - - test('should handle last block of otherEvents on next page', async () => { - spy - .mockResolvedValueOnce({ - events: [ - ...Array.from({ length: DEFAULT_PAGE_SIZE - 1 }, (_, i) => ({ - id: `${i}`, - ledger: 1, - pagingToken: `${i}`, - })), - { id: '2-1', ledger: 2, pagingToken: '1' }, - ], - }) - .mockResolvedValueOnce({ - events: [{ id: '2-2', ledger: 2, pagingToken: '2' }], - }); - - const response = await server.getEvents({ - startLedger: 1, - } as rpc.Server.GetEventsRequest); - - expect(response).toEqual({ - events: [ - ...Array.from({ length: DEFAULT_PAGE_SIZE - 1 }, (_, i) => ({ - id: `${i}`, - ledger: 1, - pagingToken: `${i}`, - })), - ], - }); - expect((server as any).eventsCache[2]).toBeUndefined(); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it.skip('does pagination correctly', async () => { - const legerNum = 58627181; - server = new SorobanServer('https://stellar.api.onfinality.io/public/rpc'); - const spy = jest.spyOn(server as any, 'fetchEventsForSequence'); - const events = await server.getEvents({ - startLedger: legerNum, - filters: [], - }); - - expect(events).toBeDefined(); - expect(events.events.every((evt) => evt.ledger === legerNum)).toBeTruthy(); - expect(events.events.length).toEqual(160); - - expect(spy).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/node/src/stellar/soroban.server.ts b/packages/node/src/stellar/soroban.server.ts deleted file mode 100644 index 1357b91f..00000000 --- a/packages/node/src/stellar/soroban.server.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0 - -import { rpc } from '@stellar/stellar-sdk'; -import { SorobanRpcEventResponse } from '@subql/types-stellar'; -import { compact, groupBy, last } from 'lodash'; -import { DEFAULT_PAGE_SIZE } from './utils.stellar'; - -type CachedEventsResponse = Pick< - rpc.Api.GetEventsResponse, - 'events' | 'latestLedger' ->; - -export class SorobanServer extends rpc.Server { - private eventsCache: { [key: number]: rpc.Api.GetEventsResponse } = {}; - - private async fetchEventsForSequence( - sequence: number, - request: rpc.Server.GetEventsRequest, - accEvents: SorobanRpcEventResponse[] = [], - ): Promise<{ - events: CachedEventsResponse; - eventsToCache: CachedEventsResponse; - }> { - const pageLimit = request.limit ?? DEFAULT_PAGE_SIZE; - const response = await super.getEvents(request); - - // Separate the events for the current sequence and the subsequent sequences - const groupedEvents = groupBy(response.events, (event) => - event.ledger === sequence ? 'events' : 'eventsToCache', - ); - const events = compact(groupedEvents.events); - let eventsToCache = compact(groupedEvents.eventsToCache); - - // Update the accumulated events with the events from the current sequence - const newEvents = accEvents.concat(events); - - // Gone over the current sequence, we must have all events - if (eventsToCache?.length) { - // Exclude the events to cache from the last sequence, we probably don't have all the events for that sequence so we discard them - if (response.events.length === pageLimit) { - const lastSequence = last(response.events)!.ledger; - eventsToCache = eventsToCache.filter( - (event) => event.ledger !== lastSequence, - ); - } - return { - events: { events: newEvents, latestLedger: response.latestLedger }, - eventsToCache: { - events: eventsToCache, - latestLedger: response.latestLedger, - }, - }; - } - // We cannot check response.events.length < pageLimit here because the server may have a pageLimit below ours that it will use. - if (response.events.length === 0) { - return { - events: { events: newEvents, latestLedger: response.latestLedger }, - eventsToCache: { events: [], latestLedger: response.latestLedger }, - }; - } - - // Prepare the next request - const nextRequest = { - ...request, - cursor: response.cursor, - startLedger: undefined, - endLedger: undefined, - }; - - // Continue fetching events for the sequence - return this.fetchEventsForSequence(sequence, nextRequest, newEvents); - } - - private updateEventCache( - response: CachedEventsResponse, - ignoreHeight?: number, - ): void { - response.events.forEach((event) => { - if (ignoreHeight && ignoreHeight === event.ledger) return; - const ledger = event.ledger; - if (!this.eventsCache[ledger]) { - this.eventsCache[ledger] = { - events: [], - latestLedger: response.latestLedger, - } as unknown as rpc.Api.GetEventsResponse; - } - const eventExists = this.eventsCache[ledger].events.some( - (existingEvent) => existingEvent.id === event.id, - ); - if (!eventExists) { - this.eventsCache[ledger].events.push(event); - } - }); - } - - async getEvents( - request: rpc.Server.GetEventsRequest, - ): Promise { - const sequence = request.startLedger; - - if (sequence === undefined) { - throw new Error(`Get soraban event failed, block sequence is missing`); - } - - // Set a limit on the request range, endLedger is exclusive - request.endLedger = request.endLedger ?? sequence + 1; - - if (this.eventsCache[sequence]) { - const cachedEvents = this.eventsCache[sequence]; - delete this.eventsCache[sequence]; - return cachedEvents; - } - - const response = await this.fetchEventsForSequence(sequence, request); - this.updateEventCache(response.eventsToCache, sequence); - - return response.events as rpc.Api.GetEventsResponse; - } -} diff --git a/packages/node/src/stellar/stellar.server.ts b/packages/node/src/stellar/stellar.server.ts deleted file mode 100644 index 75e4769e..00000000 --- a/packages/node/src/stellar/stellar.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0 - -import { Horizon } from '@stellar/stellar-sdk'; -import { getLogger } from '@subql/node-core'; - -const logger = getLogger('stellar-server'); - -export interface StellarNetwork { - network_passphrase: string; - history_latest_ledger: string; - name: string; -} - -export class StellarServer extends Horizon.Server { - async getNetwork(): Promise { - const network: StellarNetwork = ( - await Horizon.AxiosClient.get(new URL(this.serverURL as any).toString()) - ).data; - return network; - } -} diff --git a/packages/node/src/stellar/utils.stellar.ts b/packages/node/src/stellar/utils.stellar.ts index 0085ad66..0cf48f96 100644 --- a/packages/node/src/stellar/utils.stellar.ts +++ b/packages/node/src/stellar/utils.stellar.ts @@ -1,28 +1,20 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { Horizon } from '@stellar/stellar-sdk'; -import { Header, IBlock } from '@subql/node-core'; -import { StellarBlock, StellarBlockWrapper } from '@subql/types-stellar'; +import {Contract, encodeMuxedAccountToAddress, hash, rpc, StrKey, xdr} from '@stellar/stellar-sdk'; +import {Header, IBlock} from '@subql/node-core'; +import {StellarBlock, StellarBlockWrapper, StellarTransaction} from '@subql/types-stellar'; -export function stellarBlockToHeader( - block: StellarBlock | Horizon.ServerApi.LedgerRecord, -): Header { +export function stellarBlockToHeader(block: StellarBlock | rpc.Api.LedgerResponse): Header { return { blockHeight: block.sequence, - // Stellar has instant finalization and there is no RPC for getting blocks by hash. - // For these reasons we use the block numbers for hashes so that unfinalized blocks works. - blockHash: block.sequence.toString(), - parentHash: (block.sequence - 1).toString(), - // blockHash: block.hash.toString(), - // parentHash: block.prev_hash.toString(), - timestamp: new Date(block.closed_at), + blockHash: block.hash, + parentHash: block.headerXdr.header().previousLedgerHash().toString('hex'), + timestamp: getBlockTimestamp(block), }; } -export function formatBlockUtil< - B extends StellarBlockWrapper = StellarBlockWrapper, ->(block: B): IBlock { +export function formatBlockUtil(block: B): IBlock { return { block, getHeader: () => stellarBlockToHeader(block.block), @@ -30,7 +22,169 @@ export function formatBlockUtil< } export function calcInterval(): number { - return 6000; + return 4000; // Stellar on average produces a block every 4 seconds } export const DEFAULT_PAGE_SIZE = 150; + +export function getBlockTimestamp(block: rpc.Api.LedgerResponse): Date { + return new Date(parseInt(block.ledgerCloseTime, 10) * 1000); +} + +// Reference: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0035.md#specification +export function makeTOID(sequence: number, txIndex: number, opIndex: number): string { + const L = BigInt(sequence); + const T = BigInt(txIndex); + const O = BigInt(opIndex); + + const L_mask = (1n << 32n) - 1n; + const T_mask = (1n << 20n) - 1n; + const O_mask = (1n << 12n) - 1n; + + return (((L & L_mask) << 32n) | ((T & T_mask) << 12n) | ((O & O_mask) << 0n)).toString().padStart(19, '0'); +} + +export function toidToParts(toidInput: bigint | string): {ledgerSeq: number; txIndex: number; opIndex: number} { + const toid = typeof toidInput === 'string' ? BigInt(toidInput) : toidInput; + + const ledgerSeq = Number(toid >> 32n); // top 32 bits + const txIndex = Number((toid >> 12n) & ((1n << 20n) - 1n)); // next 20 bits + const opIndex = Number(toid & ((1n << 12n) - 1n)); // low 12 bits + + return {ledgerSeq, txIndex, opIndex}; +} + +/** + * Converts a ContractEvent from TransactionInfo into an EventResponse + * For transactionEventsXdr the txIndex and opIndex are always 0, eventIndex is the applicationOrder - 1 + */ +export function contractEventToEventResponse( + evt: xdr.ContractEvent, + tx: rpc.Api.TransactionInfo, + txIndex: number, + opIndex: number, + eventIndex: number, + ledgerClosedAt: Date, +): rpc.Api.EventResponse { + const rawContractId = evt.contractId(); + const contract = rawContractId ? new Contract(StrKey.encodeContract(rawContractId as any)) : undefined; + + const TOID = makeTOID(tx.ledger, txIndex, opIndex); + + return { + type: rawContractId ? 'contract' : 'system', // TODO test system case + ledger: tx.ledger, + ledgerClosedAt: ledgerClosedAt.toISOString().replace(`.000Z`, 'Z'), // Strip MS + id: `${TOID}-${String(eventIndex).padStart(10, '0')}`, + operationIndex: opIndex, + transactionIndex: txIndex, + txHash: tx.txHash, + inSuccessfulContractCall: tx.status === rpc.Api.GetTransactionStatus.SUCCESS, // TODO test with non successful values + topic: evt.body().value().topics(), + value: evt.body().value().data(), + contractId: contract, + } satisfies rpc.Api.EventResponse; +} + +export function getResultEvents(meta: xdr.TransactionMeta): { + events: rpc.Api.TransactionEvents; + diagnosticEventsXdr?: xdr.DiagnosticEvent[]; +} { + // Cases are un tested, exept for v4. This is a matter of finding block with older versions + // NOTE: waiting for clarification from Stellar TG whether events existed and need to be requested from other RPC requests + switch (meta.switch()) { + // case 0: { + // } + // case 1: { + // } + // case 2: { + // } + // case 3: { + // } + case 4: { + return { + diagnosticEventsXdr: meta.v4().diagnosticEvents().length ? meta.v4().diagnosticEvents() : undefined, + events: { + transactionEventsXdr: meta.v4().events(), + contractEventsXdr: meta + .v4() + .operations() + .map((op) => op.events()), + }, + }; + } + default: { + throw new Error(`Unsupported transaction meta version: ${meta.switch()}`); + } + } +} + +export function constructTransaction( + ledger: rpc.Api.LedgerResponse, + meta: xdr.LedgerCloseMetaV0 | xdr.LedgerCloseMetaV1 | xdr.LedgerCloseMetaV2, + txEnvelope: xdr.TransactionEnvelope, + idx: number, +): rpc.Api.TransactionInfo { + const txp = meta.txProcessing()[idx]; + + const status = + txp.result().result().result().switch() === xdr.TransactionResultCode.txSuccess() || + txp.result().result().result().switch() === xdr.TransactionResultCode.txFeeBumpInnerSuccess() + ? rpc.Api.GetTransactionStatus.SUCCESS + : rpc.Api.GetTransactionStatus.FAILED; + + return { + ...getResultEvents(txp.txApplyProcessing()), + status, + ledger: ledger.sequence, + createdAt: parseInt(ledger.ledgerCloseTime, 10), + applicationOrder: idx + 1, // Starts at 1 + feeBump: + txEnvelope.switch() === xdr.EnvelopeType.envelopeTypeTxFeeBump() && + status === rpc.Api.GetTransactionStatus.SUCCESS, + txHash: txp.result().transactionHash().toString('hex'), + envelopeXdr: txEnvelope, + resultXdr: txp.result().result(), + resultMetaXdr: txp.txApplyProcessing(), // There needs to be some ordering here + } satisfies rpc.Api.TransactionInfo; +} + +export function calculateTxHash(txEnvelope: xdr.TransactionEnvelope, networkPassphrase: string): string { + let taggedTransaction: xdr.TransactionSignaturePayloadTaggedTransaction; + + switch (txEnvelope.switch()) { + case xdr.EnvelopeType.envelopeTypeTxV0(): { + let tx = xdr.Transaction.fromXDR( + Buffer.concat([ + // TransactionV0 is a transaction with the AccountID discriminant + // stripped off, we need to put it back to build a valid transaction + // which we can use to build a TransactionSignaturePayloadTaggedTransaction + (xdr.PublicKeyType.publicKeyTypeEd25519() as any).toXDR(), // TODO missing type property? + txEnvelope.v0().tx().toXDR(), + ]), + ); + taggedTransaction = xdr.TransactionSignaturePayloadTaggedTransaction.envelopeTypeTx(tx); + break; + } + case xdr.EnvelopeType.envelopeTypeTx(): { + taggedTransaction = xdr.TransactionSignaturePayloadTaggedTransaction.envelopeTypeTx(txEnvelope.v1().tx()); + break; + } + case xdr.EnvelopeType.envelopeTypeTxFeeBump(): { + taggedTransaction = xdr.TransactionSignaturePayloadTaggedTransaction.envelopeTypeTxFeeBump( + txEnvelope.feeBump().tx(), + ); + break; + } + default: { + throw new Error(`Unsupported transaction envelope type: ${txEnvelope.switch()}`); + } + } + + const txSignature = new xdr.TransactionSignaturePayload({ + networkId: xdr.Hash.fromXDR(hash(Buffer.from(networkPassphrase, 'utf-8'))), + taggedTransaction, + }); + + return hash(txSignature.toXDR()).toString('hex'); +} diff --git a/packages/node/src/subcommands/testing.service.ts b/packages/node/src/subcommands/testing.service.ts index 62320c63..53625f95 100644 --- a/packages/node/src/subcommands/testing.service.ts +++ b/packages/node/src/subcommands/testing.service.ts @@ -1,8 +1,8 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { Inject, Injectable } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; +import {Inject, Injectable} from '@nestjs/common'; +import {NestFactory} from '@nestjs/core'; import { NodeConfig, TestingService as BaseTestingService, @@ -10,15 +10,11 @@ import { NestLogger, ProjectService, } from '@subql/node-core'; -import { - BlockWrapper, - StellarBlockWrapper, - SubqlDatasource, -} from '@subql/types-stellar'; -import { SubqueryProject } from '../configure/SubqueryProject'; -import { StellarApi } from '../stellar'; +import {StellarBlockWrapper, SubqlDatasource} from '@subql/types-stellar'; +import {SubqueryProject} from '../configure/SubqueryProject'; +import {StellarApi} from '../stellar'; import SafeStellarProvider from '../stellar/safe-api'; -import { TestingModule } from './testing.module'; +import {TestingModule} from './testing.module'; @Injectable() export class TestingService extends BaseTestingService< StellarApi, @@ -26,30 +22,19 @@ export class TestingService extends BaseTestingService< StellarBlockWrapper, SubqlDatasource > { - constructor( - nodeConfig: NodeConfig, - @Inject('ISubqueryProject') project: SubqueryProject, - ) { + constructor(nodeConfig: NodeConfig, @Inject('ISubqueryProject') project: SubqueryProject) { super(nodeConfig, project); } async getTestRunner(): Promise< [ close: () => Promise, - runner: TestRunner< - StellarApi, - SafeStellarProvider, - BlockWrapper, - SubqlDatasource - >, + runner: TestRunner, ] > { - const testContext = await NestFactory.createApplicationContext( - TestingModule, - { - logger: new NestLogger(), - }, - ); + const testContext = await NestFactory.createApplicationContext(TestingModule, { + logger: new NestLogger(), + }); await testContext.init(); diff --git a/packages/node/src/yargs.ts b/packages/node/src/yargs.ts index b7525245..44e63c54 100644 --- a/packages/node/src/yargs.ts +++ b/packages/node/src/yargs.ts @@ -1,33 +1,27 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { yargsBuilder } from '@subql/node-core/yargs'; +import {yargsBuilder} from '@subql/node-core/yargs'; export const yargsOptions = yargsBuilder({ initTesting: () => { // lazy import to make sure logger is instantiated before all other services // eslint-disable-next-line @typescript-eslint/no-var-requires - const { testingInit } = require('./subcommands/testing.init'); + const {testingInit} = require('./subcommands/testing.init'); return testingInit(); }, initForceClean: () => { // lazy import to make sure logger is instantiated before all other services // eslint-disable-next-line @typescript-eslint/no-var-requires - const { forceCleanInit } = require('./subcommands/forceClean.init'); + const {forceCleanInit} = require('./subcommands/forceClean.init'); return forceCleanInit(); }, initReindex: (targetHeight: number) => { // lazy import to make sure logger is instantiated before all other services // eslint-disable-next-line @typescript-eslint/no-var-requires - const { reindexInit } = require('./subcommands/reindex.init'); + const {reindexInit} = require('./subcommands/reindex.init'); return reindexInit(targetHeight); }, - rootOptions: { - 'soroban-network-endpoint': { - demandOption: false, - type: 'string', - describe: 'Blockchain network endpoint to connect', - }, - }, + rootOptions: {}, }); diff --git a/packages/types/package.json b/packages/types/package.json index 6db69b62..edb59e40 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -17,7 +17,7 @@ "/dist" ], "dependencies": { - "@stellar/stellar-sdk": "^14.1.0", + "@stellar/stellar-sdk": "^14.3.3", "@subql/types-core": "^2.1.0" } } diff --git a/packages/types/src/global.ts b/packages/types/src/global.ts index 6f959a1b..a465fc32 100644 --- a/packages/types/src/global.ts +++ b/packages/types/src/global.ts @@ -1,7 +1,7 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {Horizon, rpc} from '@stellar/stellar-sdk'; +import {rpc} from '@stellar/stellar-sdk'; import '@subql/types-core/dist/global'; declare global { @@ -12,6 +12,5 @@ declare global { * WARNING: It is not scoped to the current block so it will query current chain state. This can lead to a project being non-deterministic. * To have access to the unsafeApi please use the `unsafe` flag in your project configuration. */ - const unsafeApi: Horizon.Server | undefined; - const unsafeSorobanApi: rpc.Server | undefined; + const unsafeApi: rpc.Server | undefined; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1c69fc73..c328556f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,6 +1,6 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -export * from './interfaces'; export * from './project'; export * from './stellar'; +export * from './utils'; diff --git a/packages/types/src/interfaces.ts b/packages/types/src/interfaces.ts deleted file mode 100644 index 75b572bd..00000000 --- a/packages/types/src/interfaces.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0 - -import {StellarBlock, StellarEffect, SorobanEvent, StellarOperation, StellarTransaction} from './stellar'; - -export interface BlockWrapper< - B extends StellarBlock = StellarBlock, - T extends StellarTransaction = StellarTransaction, - O extends StellarOperation = StellarOperation, - EF extends StellarEffect = StellarEffect, - E extends SorobanEvent = SorobanEvent -> { - block: B; - transactions: T[]; - operations: O[]; - effects: EF[]; - events?: E[]; -} - -export type DynamicDatasourceCreator = (name: string, args: Record) => Promise; diff --git a/packages/types/src/project.ts b/packages/types/src/project.ts index 08ecbc70..f7b2e11c 100644 --- a/packages/types/src/project.ts +++ b/packages/types/src/project.ts @@ -1,7 +1,7 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {Horizon} from '@stellar/stellar-sdk'; +import type {rpc} from '@stellar/stellar-sdk'; import { BaseTemplateDataSource, IProjectNetworkConfig, @@ -19,16 +19,14 @@ import { IEndpointConfig, } from '@subql/types-core'; import { - StellarBlock, StellarBlockFilter, - StellarEffect, - StellarEffectFilter, - SorobanEvent, - SorobanEventFilter, - StellarOperation, + StellarEventFilter, StellarOperationFilter, - StellarTransaction, StellarTransactionFilter, + StellarOperation, + StellarTransaction, + StellarBlock, + StellarEvent, } from './stellar'; export type RuntimeDatasourceTemplate = BaseTemplateDataSource; @@ -60,18 +58,10 @@ export enum StellarHandlerKind { * Handler for Stellar Transactions. */ Transaction = 'stellar/TransactionHandler', - /** - * Handler for Soroban Transactions. - */ - SorobanTransaction = 'soroban/TransactionHandler', /** * Handler for Stellar Operations. */ Operation = 'stellar/OperationHandler', - /** - * Handler for Stellar Effects. - */ - Effects = 'stellar/EffectHandler', /** * Handler for Soroban Events. */ @@ -81,19 +71,15 @@ export enum StellarHandlerKind { export type StellarRuntimeHandlerInputMap = { [StellarHandlerKind.Block]: StellarBlock; [StellarHandlerKind.Transaction]: StellarTransaction; - [StellarHandlerKind.SorobanTransaction]: StellarTransaction; [StellarHandlerKind.Operation]: StellarOperation; - [StellarHandlerKind.Effects]: StellarEffect; - [StellarHandlerKind.Event]: SorobanEvent; + [StellarHandlerKind.Event]: StellarEvent; }; type StellarRuntimeFilterMap = { [StellarHandlerKind.Block]: StellarBlockFilter; [StellarHandlerKind.Transaction]: StellarTransactionFilter; - [StellarHandlerKind.SorobanTransaction]: StellarTransactionFilter; - [StellarHandlerKind.Effects]: StellarEffectFilter; [StellarHandlerKind.Operation]: StellarOperationFilter; - [StellarHandlerKind.Event]: SorobanEventFilter; + [StellarHandlerKind.Event]: StellarEventFilter; }; /** @@ -116,15 +102,15 @@ export interface SubqlTransactionHandler { filter?: StellarTransactionFilter; } -/** - * Represents a handler for Soroban transactions. - * @type {SubqlCustomHandler} - */ -export interface SubqlSorobanTransactionHandler { - handler: string; - kind: StellarHandlerKind.SorobanTransaction; - filter?: StellarTransactionFilter; -} +// /** +// * Represents a handler for Soroban transactions. +// * @type {SubqlCustomHandler} +// */ +// export interface SubqlSorobanTransactionHandler { +// handler: string; +// kind: StellarHandlerKind.SorobanTransaction; +// filter?: StellarTransactionFilter; +// } /** * Represents a handler for Stellar operations. @@ -136,24 +122,14 @@ export interface SubqlOperationHandler { filter?: StellarOperationFilter; } -/** - * Represents a handler for Stellar effects. - * @type {SubqlCustomHandler} - */ -export interface SubqlEffectHandler { - handler: string; - kind: StellarHandlerKind.Effects; - filter?: StellarEffectFilter; -} - /** * Represents a handler for Soroban event. - * @type {SubqlCustomHandler} + * @type {SubqlCustomHandler} */ export interface SubqlEventHandler { handler: string; kind: StellarHandlerKind.Event; - filter?: SorobanEventFilter; + filter?: StellarEventFilter; } /** @@ -165,9 +141,9 @@ export interface SubqlEventHandler { export interface SubqlCustomHandler> extends BaseHandler { /** - * The kind of handler. For `stellar/Runtime` datasources this is either `Block`, `Transaction`, `Operation`, `Effect` or `Event` kinds. + * The kind of handler. For `stellar/Runtime` datasources this is either `Block`, `Transaction`, `Operation` or `Event` kinds. * The value of this will determine the filter options as well as the data provided to your handler function - * @type {StellarHandlerKind.Block | StellarHandlerKind.Transaction | StellarHandlerKind.SorobanTransaction | StellarHandlerKind.Operation | StellarHandlerKind.Effects | SubstrateHandlerKind.Event | string } + * @type {StellarHandlerKind.Block | StellarHandlerKind.Transaction | StellarHandlerKind.SorobanTransaction | StellarHandlerKind.Operation | SubstrateHandlerKind.Event | string } * @example * kind: StellarHandlerFind.Block // Defined with an enum, this is used for runtime datasources */ @@ -183,15 +159,13 @@ export interface SubqlCustomHandler; /** - * Represents a filter for Stellar runtime handlers, which can be a block filter, transaction filter, operation filter, effects filter or event filter. + * Represents a filter for Stellar runtime handlers, which can be a block filter, transaction filter, operation filter or event filter. * @type {SubstrateBlockFilter | SubstrateCallFilter | SubstrateEventFilter} */ export type SubqlHandlerFilter = - | SorobanEventFilter + | StellarEventFilter | StellarTransactionFilter | StellarOperationFilter - | StellarEffectFilter | StellarBlockFilter; /** @@ -289,24 +262,8 @@ export type SecondLayerHandlerProcessor< E, DS extends SubqlCustomDatasource = SubqlCustomDatasource > = - | SecondLayerHandlerProcessor_0_0_0< - K, - StellarRuntimeHandlerInputMap, - StellarRuntimeFilterMap, - F, - E, - DS, - Horizon.Server - > - | SecondLayerHandlerProcessor_1_0_0< - K, - StellarRuntimeHandlerInputMap, - StellarRuntimeFilterMap, - F, - E, - DS, - Horizon.Server - >; + | SecondLayerHandlerProcessor_0_0_0 + | SecondLayerHandlerProcessor_1_0_0; export type SecondLayerHandlerProcessorArray< K extends string, F extends Record, @@ -315,9 +272,7 @@ export type SecondLayerHandlerProcessorArray< > = | SecondLayerHandlerProcessor | SecondLayerHandlerProcessor - | SecondLayerHandlerProcessor | SecondLayerHandlerProcessor - | SecondLayerHandlerProcessor | SecondLayerHandlerProcessor; export type SubqlDatasourceProcessor< @@ -328,7 +283,7 @@ export type SubqlDatasourceProcessor< string, SecondLayerHandlerProcessorArray > -> = DsProcessor; +> = DsProcessor; export interface IStellarEndpointConfig extends IEndpointConfig { /** @@ -346,9 +301,7 @@ export interface IStellarEndpointConfig extends IEndpointConfig { * Represents a Stellar subquery network configuration, which is based on the CommonSubqueryNetworkConfig template. * @type {IProjectNetworkConfig} */ -export type StellarNetworkConfig = IProjectNetworkConfig & { - sorobanEndpoint?: string; -}; +export type StellarNetworkConfig = IProjectNetworkConfig; /** * Represents a Stellar project configuration based on the CommonSubqueryProject template. diff --git a/packages/types/src/stellar/interfaces.ts b/packages/types/src/stellar/interfaces.ts index 490d3d03..6be0e6be 100644 --- a/packages/types/src/stellar/interfaces.ts +++ b/packages/types/src/stellar/interfaces.ts @@ -1,49 +1,34 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {Horizon, rpc} from '@stellar/stellar-sdk'; -import {BlockWrapper} from '../interfaces'; +import type {Operation, rpc, xdr} from '@stellar/stellar-sdk'; -export type StellarBlock = Omit & { - effects: StellarEffect[]; - operations: StellarOperation[]; +export type StellarBlock = rpc.Api.LedgerResponse & { transactions: StellarTransaction[]; - events: SorobanEvent[]; -}; - -export type StellarTransaction = Omit< - Horizon.ServerApi.TransactionRecord, - 'effects' | 'ledger' | 'operations' | 'precedes' | 'self' | 'succeeds' -> & { - effects: StellarEffect[]; - ledger: StellarBlock | null; operations: StellarOperation[]; - events: SorobanEvent[]; + events: StellarEvent[]; }; -export type StellarOperation = - Omit & { - effects: StellarEffect[]; - transaction: StellarTransaction | null; - ledger: StellarBlock | null; - events: SorobanEvent[]; - }; - -export type StellarEffect = Omit< - T, - 'operation' -> & { - operation: StellarOperation | null; - transaction: StellarTransaction | null; - ledger: StellarBlock | null; +export type StellarTransaction = { + tx: rpc.Api.TransactionInfo; + events: rpc.Api.EventResponse[]; }; -export type SorobanRpcEventResponse = rpc.Api.EventResponse; - -export type SorobanEvent = Omit & { - ledger: StellarBlock | null; - transaction: StellarTransaction | null; - operation: StellarOperation | null; +export type StellarOperation = { + /** + * The index of the operation within the transaction + **/ + index: number; + /** + * The xdr operation as you would get from the stellar RPC + **/ + operation: xdr.Operation; + transaction: StellarTransaction; +}; +export type StellarEvent = { + event: rpc.Api.EventResponse; + block: StellarBlock; + tx: rpc.Api.TransactionInfo; }; export interface StellarBlockFilter { @@ -56,24 +41,18 @@ export interface StellarTransactionFilter { } export interface StellarOperationFilter { - type?: Horizon.HorizonApi.OperationResponseType; + type?: xdr.OperationType['name']; sourceAccount?: string; } -export interface StellarEffectFilter { - type?: string; - account?: string; -} - -export interface SorobanEventFilter { +export interface StellarEventFilter { contractId?: string; topics?: string[]; } -export type StellarBlockWrapper = BlockWrapper< - StellarBlock, - StellarTransaction, - StellarOperation, - StellarEffect, - SorobanEvent ->; +export type StellarBlockWrapper = { + block: StellarBlock; + transactions: StellarTransaction[]; + operations: StellarOperation[]; + events: StellarEvent[]; +}; diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts new file mode 100644 index 00000000..b0661d3d --- /dev/null +++ b/packages/types/src/utils.ts @@ -0,0 +1,20 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +// These imports must be as granular/narrow as possible to ensure they work in the sandbox +import type {rpc} from '@stellar/stellar-sdk'; +import {encodeMuxedAccountToAddress, StrKey, xdr} from '@stellar/stellar-base'; + +export function getTransactionSourceAccount(tx: rpc.Api.TransactionInfo): string { + // Use the name in the switch to avoid using the types directly as there could be conflicting package versions + switch (tx.envelopeXdr.switch().name) { + case xdr.EnvelopeType.envelopeTypeTxV0().name: + return StrKey.encodeEd25519PublicKey(tx.envelopeXdr.v0().tx().sourceAccountEd25519()); + case xdr.EnvelopeType.envelopeTypeTx().name: + return encodeMuxedAccountToAddress(tx.envelopeXdr.v1().tx().sourceAccount(), true); + case xdr.EnvelopeType.envelopeTypeTxFeeBump().name: + return encodeMuxedAccountToAddress(tx.envelopeXdr.feeBump().tx().feeSource(), true); + default: + throw new Error(`Unknown Transaction envelope type ${tx.envelopeXdr.switch().name}`); + } +} diff --git a/yarn.lock b/yarn.lock index e1449c3b..06604c69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2663,9 +2663,9 @@ __metadata: languageName: node linkType: hard -"@stellar/stellar-base@npm:^14.0.0": - version: 14.0.0 - resolution: "@stellar/stellar-base@npm:14.0.0" +"@stellar/stellar-base@npm:^14.0.2": + version: 14.0.2 + resolution: "@stellar/stellar-base@npm:14.0.2" dependencies: "@noble/curves": "npm:^1.9.6" "@stellar/js-xdr": "npm:^3.1.2" @@ -2673,23 +2673,23 @@ __metadata: bignumber.js: "npm:^9.3.1" buffer: "npm:^6.0.3" sha.js: "npm:^2.4.12" - checksum: 10/795bdbbabde4b4da266fb0dc07a6335fba107b5a1c4ffc496dfd8e53c85dd9dc4026b1cd8e1eb6fba51bd0b3134c6939fd4b708432f56594c953b68ea0ffd829 + checksum: 10/7446ffdbe50c64d9a57d320e2c159a3ebc53763b9a2378e66ed03ea570ac2416ecd0552b7a829c41c109c2aac0aadbf122975c811ab8bf2c957ff536b3894913 languageName: node linkType: hard -"@stellar/stellar-sdk@npm:^14.1.0": - version: 14.1.0 - resolution: "@stellar/stellar-sdk@npm:14.1.0" +"@stellar/stellar-sdk@npm:^14.3.3": + version: 14.3.3 + resolution: "@stellar/stellar-sdk@npm:14.3.3" dependencies: - "@stellar/stellar-base": "npm:^14.0.0" - axios: "npm:^1.8.4" + "@stellar/stellar-base": "npm:^14.0.2" + axios: "npm:^1.12.2" bignumber.js: "npm:^9.3.1" eventsource: "npm:^2.0.2" feaxios: "npm:^0.0.23" randombytes: "npm:^2.1.0" toml: "npm:^3.0.0" urijs: "npm:^1.19.1" - checksum: 10/cb964bc8e6fba4e163cfe9589f4ea1a51f2b36e74604985cfcee790fa147dc4bfe414f4da3d4cdd1c0e79cbebb7aaa9f3a507c4ede7e74a6ab53ec823f38c1e0 + checksum: 10/e161552210d34ac50ccf163b864f6251d0eaaebc27472ecaf2ac0a251a1475e00cb6123c6f360bbff4c3a5be8ca00e7bf5ccd578b4272094682c8c3fd5a2756a languageName: node linkType: hard @@ -2697,7 +2697,7 @@ __metadata: version: 0.0.0-use.local resolution: "@subql/common-stellar@workspace:packages/common-stellar" dependencies: - "@stellar/stellar-sdk": "npm:^14.1.0" + "@stellar/stellar-sdk": "npm:^14.3.3" "@subql/common": "npm:^5.7.0" "@subql/types-stellar": "workspace:*" "@types/js-yaml": "npm:^4.0.4" @@ -2771,7 +2771,7 @@ __metadata: "@nestjs/schedule": "npm:^5.0.1" "@nestjs/schematics": "npm:^11.0.2" "@nestjs/testing": "npm:^11.0.11" - "@stellar/stellar-sdk": "npm:^14.1.0" + "@stellar/stellar-sdk": "npm:^14.3.3" "@subql/common": "npm:^5.7.0" "@subql/common-stellar": "workspace:*" "@subql/node-core": "npm:^18.2.0" @@ -2832,7 +2832,7 @@ __metadata: version: 0.0.0-use.local resolution: "@subql/types-stellar@workspace:packages/types" dependencies: - "@stellar/stellar-sdk": "npm:^14.1.0" + "@stellar/stellar-sdk": "npm:^14.3.3" "@subql/types-core": "npm:^2.1.0" languageName: unknown linkType: soft @@ -3964,7 +3964,18 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.8.4, axios@npm:^1.9.0": +"axios@npm:^1.12.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/ae4e06dcd18289f2fd18179256d550d27f9a53ecb2f9c59f2ccc4efd1d7151839ba8c3e0fb533dac793e4a59a576ca8689a19244dce5c396680837674a47a867 + languageName: node + linkType: hard + +"axios@npm:^1.9.0": version: 1.12.1 resolution: "axios@npm:1.12.1" dependencies: @@ -11893,13 +11904,20 @@ __metadata: languageName: node linkType: hard -"validator@npm:^13.7.0, validator@npm:^13.9.0": +"validator@npm:^13.7.0": version: 13.15.20 resolution: "validator@npm:13.15.20" checksum: 10/498f9b201dda7b09207bbc13606e6f90595987eab9e3a60fc5b8d8d96141c9351192aea56d392c78640e2462770eedccf545cf13b2ef43e87d73a755ba485c5d languageName: node linkType: hard +"validator@npm:^13.9.0": + version: 13.12.0 + resolution: "validator@npm:13.12.0" + checksum: 10/db6eb0725e2b67d60d30073ae8573982713b5903195d031dc3c7db7e82df8b74e8c13baef8e2106d146d979599fd61a06cde1fec5c148e4abd53d52817ff0fd9 + languageName: node + linkType: hard + "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2"