Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Add the sorabanTxs field to transaction information(#112)

## [4.6.0] - 2025-03-05
### Changed
- Support http soroban endpoints (#109)
Expand Down
7 changes: 6 additions & 1 deletion packages/node/src/stellar/api.service.stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
StellarBlockWrapper,
SubqlDatasource,
IStellarEndpointConfig,
StellarHandlerKind,
} from '@subql/types-stellar';
import {
SubqueryProject,
Expand Down Expand Up @@ -52,6 +53,10 @@ export class StellarApiService extends ApiService<
): Promise<StellarApiService> {
let network: StellarProjectNetworkConfig;

const needSorobanTxMeta = !!project.dataSources.find(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to take into account project upgrades and dynamic datasoruces.

Does the user requesting this feature also need to access the transactions from soroban events?

(handler) => handler.kind === StellarHandlerKind.SorobanTransaction,
);

const apiService = new StellarApiService(
connectionPoolService,
eventEmitter,
Expand Down Expand Up @@ -102,7 +107,7 @@ export class StellarApiService extends ApiService<
endpoint,
apiService.fetchBlockBatches.bind(apiService),
sorobanClient,
config,
{ sorobanTxMeta: needSorobanTxMeta, ...config },
),
);

Expand Down
19 changes: 18 additions & 1 deletion packages/node/src/stellar/api.stellar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ jest.setTimeout(60000);

const prepareStellarApi = async function () {
const soroban = new SorobanServer(SOROBAN_ENDPOINT);
const api = new StellarApi(HTTP_ENDPOINT, soroban);
const api = new StellarApi(HTTP_ENDPOINT, soroban, {
sorobanTxMeta: true,
});
await api.init();
return api;
};
Expand Down Expand Up @@ -96,4 +98,19 @@ describe('StellarApi', () => {
/(Gone|Not Found)/,
);
});

it('Able to correctly return sorabanTxs', async () => {
const latestHeight = await stellarApi.getFinalizedBlockHeight();
const { transactions: sorobanTxs } =
await stellarApi.sorobanClient.getTransactions({
startLedger: latestHeight - 100000,
});

// Get ledgers with transactions
const startLedger = sorobanTxs[0].ledger;
const block = (await stellarApi.fetchBlocks([startLedger]))[0];

expect(block.block.transactions[0].sorobanTxs).toBeDefined();
expect(block.block.transactions[0].sorobanTxs!.ledger).toEqual(startLedger);
});
});
79 changes: 72 additions & 7 deletions packages/node/src/stellar/api.stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// SPDX-License-Identifier: GPL-3.0

import assert from 'assert';
import { Horizon } from '@stellar/stellar-sdk';
import { Horizon, xdr } from '@stellar/stellar-sdk';
import { Api } from '@stellar/stellar-sdk/lib/rpc';
import { getLogger, IBlock } from '@subql/node-core';
import {
ApiWrapper,
Expand All @@ -29,6 +30,7 @@ export class StellarApi implements ApiWrapper {

private chainId?: string;
private pageLimit = DEFAULT_PAGE_SIZE;
private sorobanTxMeta: boolean;

constructor(
private endpoint: string,
Expand All @@ -37,6 +39,7 @@ export class StellarApi implements ApiWrapper {
) {
const { hostname, protocol, searchParams } = new URL(this.endpoint);
this.pageLimit = config?.pageLimit || this.pageLimit;
this.sorobanTxMeta = !!config?.sorobanTxMeta;

const protocolStr = protocol.replace(':', '');

Expand Down Expand Up @@ -117,6 +120,60 @@ export class StellarApi implements ApiWrapper {
return txs;
}

private async getSorobanTxsForLedger(
sequence: number,
): Promise<Api.TransactionInfo[]> {
if (!this.sorobanTxMeta) {
return [];
}

const transactionMetas: Api.TransactionInfo[] = [];
let existOtherLedger = false;
let cursor: undefined | string;
try {
do {
// There is an issue with the type definition of the input parameters; pagination is missing, so as any is used.
const requestBody = cursor
? {
pagination: {
cursor,
limit: this.pageLimit,
},
}
: {
startLedger: sequence,
pagination: {
limit: this.pageLimit,
},
};
const txMetaPage = await this.sorobanClient.getTransactions(
requestBody as any,
);

for (const txMeta of txMetaPage.transactions) {
if (txMeta.ledger !== sequence) {
existOtherLedger = true;
break;
}
transactionMetas.push(txMeta);
}
cursor = txMetaPage.cursor;
} while (!existOtherLedger);
} catch (e: any) {
if (e.message.includes("(reading 'map')")) {
// This is a workaround for the issue where the soroban client returns an empty array for transactions
// when there are no transactions for the specified ledger.
logger.warn(
`No transactions found for ledger ${sequence}. Soroban client may not be fully synced.`,
);
return [];
}
throw e;
}

return transactionMetas;
}

private async fetchOperationsForLedger(
sequence: number,
): Promise<Horizon.ServerApi.OperationRecord[]> {
Expand Down Expand Up @@ -245,14 +302,19 @@ export class StellarApi implements ApiWrapper {
operationsForSequence: Horizon.ServerApi.OperationRecord[],
effectsForSequence: Horizon.ServerApi.EffectRecord[],
eventsForSequence: SorobanEvent[],
sorobanTxs: Api.TransactionInfo[],
): StellarTransaction[] {
const sorabanTxMap = new Map(
sorobanTxs.map((sorobanTx) => [sorobanTx.txHash, sorobanTx]),
);
return transactions.map((tx) => {
const wrappedTx: StellarTransaction = {
...tx,
ledger: null,
operations: [] as StellarOperation[],
effects: [] as StellarEffect[],
events: [] as SorobanEvent[],
sorobanTxs: sorabanTxMap.get(tx.id),
};

const clonedTx = cloneDeep(wrappedTx);
Expand Down Expand Up @@ -289,12 +351,14 @@ export class StellarApi implements ApiWrapper {
private async fetchAndWrapLedger(
sequence: number,
): Promise<IBlock<StellarBlockWrapper>> {
const [ledger, transactions, operations, effects] = await Promise.all([
this.api.ledgers().ledger(sequence).call(),
this.fetchTransactionsForLedger(sequence),
this.fetchOperationsForLedger(sequence),
this.fetchEffectsForLedger(sequence),
]);
const [ledger, transactions, operations, effects, sorobanTxs] =
await Promise.all([
this.api.ledgers().ledger(sequence).call(),
this.fetchTransactionsForLedger(sequence),
this.fetchOperationsForLedger(sequence),
this.fetchEffectsForLedger(sequence),
this.getSorobanTxsForLedger(sequence),
]);

let eventsForSequence: SorobanEvent[] = [];

Expand Down Expand Up @@ -342,6 +406,7 @@ export class StellarApi implements ApiWrapper {
operations,
effects,
eventsForSequence,
sorobanTxs,
);

const clonedLedger = cloneDeep(wrappedLedger);
Expand Down
2 changes: 2 additions & 0 deletions packages/types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Expose the global parameter unsafeApi.

## [4.3.0] - 2025-03-05
### Changed
Expand Down
5 changes: 3 additions & 2 deletions packages/types/src/global.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import {rpc} from '@stellar/stellar-sdk';
import {Horizon, rpc} from '@stellar/stellar-sdk';
import '@subql/types-core/dist/global';

declare global {
const api: rpc.Server;
const api: undefined;
const unsafeApi: {sorobanClient: rpc.Server; stellarClient: Horizon.Server};
}
5 changes: 5 additions & 0 deletions packages/types/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ export interface IStellarEndpointConfig extends IEndpointConfig {
* The page limit for the number of records to fetch per request. Default value is 150
*/
pageLimit?: number;

/**
* Need soroban meta data
*/
sorobanTxMeta?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be determined based on the project datasources not on a user setting

}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/types/src/stellar/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import {Horizon, rpc} from '@stellar/stellar-sdk';
import {Horizon, rpc, xdr} from '@stellar/stellar-sdk';
import {Api} from '@stellar/stellar-sdk/lib/rpc';
import {BlockWrapper} from '../interfaces';

export type StellarBlock = Omit<Horizon.ServerApi.LedgerRecord, 'effects' | 'operations' | 'self' | 'transactions'> & {
Expand All @@ -19,6 +20,7 @@ export type StellarTransaction = Omit<
ledger: StellarBlock | null;
operations: StellarOperation[];
events: SorobanEvent[];
sorobanTxs?: Api.TransactionInfo;
};

export type StellarOperation<T extends Horizon.HorizonApi.BaseOperationResponse = Horizon.ServerApi.OperationRecord> =
Expand Down
Loading