Skip to content

Commit

Permalink
RPC: Add support for getTransactions endpoint (#1037)
Browse files Browse the repository at this point in the history
* Add getTransactions endpoint - 1

* Add unittest

* remove pagination test

* Fix linter

* Update src/rpc/server.ts

Co-authored-by: George <[email protected]>

* Fix review comments

* fix diagnosticEvents parsing

* small change

* small change - 2

* add diagnosticEvents to GetTransaction response

* Add CHANGELOG

---------

Co-authored-by: George <[email protected]>
  • Loading branch information
aditya1702 and Shaptic authored Sep 13, 2024
1 parent 581b32a commit e6cda07
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A breaking change will get clearly marked in this log.


### Added
- Add `getTransactions` to RPC server. ([#1037](https://github.com/stellar/js-stellar-sdk/pull/1037))
- `rpc.Server` now has a `getVersionInfo` method which reports version information of the RPC instance it is connected to. ([#997](https://github.com/stellar/js-stellar-sdk/issues/997)):

```typescript
Expand Down
52 changes: 52 additions & 0 deletions src/rpc/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export namespace Api {
envelopeXdr: xdr.TransactionEnvelope;
resultXdr: xdr.TransactionResult;
resultMetaXdr: xdr.TransactionMeta;
diagnosticEventsXdr?: xdr.DiagnosticEvent[];
}

export interface GetSuccessfulTransactionResponse
Expand All @@ -108,6 +109,7 @@ export namespace Api {
envelopeXdr: xdr.TransactionEnvelope;
resultXdr: xdr.TransactionResult;
resultMetaXdr: xdr.TransactionMeta;
diagnosticEventsXdr?: xdr.DiagnosticEvent[];

returnValue?: xdr.ScVal; // present iff resultMeta is a v3
}
Expand All @@ -127,6 +129,56 @@ export namespace Api {
resultMetaXdr?: string;
ledger?: number;
createdAt?: number;
diagnosticEventsXdr?: string[];
}

export interface GetTransactionsRequest {
startLedger: number;
cursor?: string;
limit?: number;
}

export interface RawTransactionInfo {
status: GetTransactionStatus;
ledger: number;
createdAt: number;
applicationOrder: number;
feeBump: boolean;
envelopeXdr?: string;
resultXdr?: string;
resultMetaXdr?: string;
diagnosticEventsXdr?: string[];
}

export interface TransactionInfo {
status: GetTransactionStatus;
ledger: number;
createdAt: number;
applicationOrder: number;
feeBump: boolean;
envelopeXdr: xdr.TransactionEnvelope;
resultXdr: xdr.TransactionResult;
resultMetaXdr: xdr.TransactionMeta;
returnValue?: xdr.ScVal;
diagnosticEventsXdr?: xdr.DiagnosticEvent[];
}

export interface GetTransactionsResponse {
transactions: TransactionInfo[];
latestLedger: number;
latestLedgerCloseTimestamp: number;
oldestLedger: number;
oldestLedgerCloseTimestamp: number;
cursor: string;
}

export interface RawGetTransactionsResponse {
transactions: RawTransactionInfo[];
latestLedger: number;
latestLedgerCloseTimestamp: number;
oldestLedger: number;
oldestLedgerCloseTimestamp: number;
cursor: string;
}

export type EventType = 'contract' | 'system' | 'diagnostic';
Expand Down
34 changes: 34 additions & 0 deletions src/rpc/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,40 @@ export function parseRawSendTransaction(
return { ...r } as Api.BaseSendTransactionResponse;
}

export function parseTransactionInfo(raw: Api.RawTransactionInfo | Api.RawGetTransactionResponse): Omit<Api.TransactionInfo, 'status'> {
const meta = xdr.TransactionMeta.fromXDR(raw.resultMetaXdr!, 'base64');
const info: Omit<Api.TransactionInfo, 'status'> = {
ledger: raw.ledger!,
createdAt: raw.createdAt!,
applicationOrder: raw.applicationOrder!,
feeBump: raw.feeBump!,
envelopeXdr: xdr.TransactionEnvelope.fromXDR(raw.envelopeXdr!, 'base64'),
resultXdr: xdr.TransactionResult.fromXDR(raw.resultXdr!, 'base64'),
resultMetaXdr: meta,
};

if (meta.switch() === 3 && meta.v3().sorobanMeta() !== null) {
info.returnValue = meta.v3().sorobanMeta()?.returnValue();
}

if ('diagnosticEventsXdr' in raw && raw.diagnosticEventsXdr) {
info.diagnosticEventsXdr = raw.diagnosticEventsXdr.map(
diagnosticEvent => xdr.DiagnosticEvent.fromXDR(diagnosticEvent, 'base64')
);
}

return info;
}

export function parseRawTransactions(
r: Api.RawTransactionInfo
): Api.TransactionInfo {
return {
status: r.status,
...parseTransactionInfo(r),
};
}

export function parseRawEvents(
r: Api.RawGetEventsResponse
): Api.GetEventsResponse {
Expand Down
66 changes: 44 additions & 22 deletions src/rpc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {
parseRawSendTransaction,
parseRawSimulation,
parseRawLedgerEntries,
parseRawEvents
parseRawEvents,
parseRawTransactions,
parseTransactionInfo,
} from './parsers';

export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000;
Expand Down Expand Up @@ -439,30 +441,13 @@ export class Server {
hash: string
): Promise<Api.GetTransactionResponse> {
return this._getTransaction(hash).then((raw) => {
let foundInfo: Omit<
Api.GetSuccessfulTransactionResponse,
keyof Api.GetMissingTransactionResponse
const foundInfo: Omit<
Api.GetSuccessfulTransactionResponse,
keyof Api.GetMissingTransactionResponse
> = {} as any;

if (raw.status !== Api.GetTransactionStatus.NOT_FOUND) {
const meta = xdr.TransactionMeta.fromXDR(raw.resultMetaXdr!, 'base64');
foundInfo = {
ledger: raw.ledger!,
createdAt: raw.createdAt!,
applicationOrder: raw.applicationOrder!,
feeBump: raw.feeBump!,
envelopeXdr: xdr.TransactionEnvelope.fromXDR(
raw.envelopeXdr!,
'base64'
),
resultXdr: xdr.TransactionResult.fromXDR(raw.resultXdr!, 'base64'),
resultMetaXdr: meta,
...(meta.switch() === 3 &&
meta.v3().sorobanMeta() !== null &&
raw.status === Api.GetTransactionStatus.SUCCESS && {
returnValue: meta.v3().sorobanMeta()?.returnValue()
})
};
Object.assign(foundInfo, parseTransactionInfo(raw));
}

const result: Api.GetTransactionResponse = {
Expand All @@ -485,6 +470,43 @@ export class Server {
return jsonrpc.postObject(this.serverURL.toString(), 'getTransaction', {hash});
}

/**
* Fetch transactions starting from a given start ledger or a cursor. The end ledger is the latest ledger
* in that RPC instance.
*
* @param {Api.GetTransactionsRequest} request - The request parameters.
* @returns {Promise<Api.GetTransactionsResponse>} - A promise that resolves to the transactions response.
*
* @see https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransactions
* @example
* server.getTransactions({
* startLedger: 10000,
* limit: 10,
* }).then((response) => {
* console.log("Transactions:", response.transactions);
* console.log("Latest Ledger:", response.latestLedger);
* console.log("Cursor:", response.cursor);
* });
*/
public async getTransactions(request: Api.GetTransactionsRequest): Promise<Api.GetTransactionsResponse> {
return this._getTransactions(request).then((raw: Api.RawGetTransactionsResponse) => {
const result: Api.GetTransactionsResponse = {
transactions: raw.transactions.map(parseRawTransactions),
latestLedger: raw.latestLedger,
latestLedgerCloseTimestamp: raw.latestLedgerCloseTimestamp,
oldestLedger: raw.oldestLedger,
oldestLedgerCloseTimestamp: raw.oldestLedgerCloseTimestamp,
cursor: raw.cursor,
}
return result
});
}

// Add this private method to the Server class
private async _getTransactions(request: Api.GetTransactionsRequest): Promise<Api.RawGetTransactionsResponse> {
return jsonrpc.postObject(this.serverURL.toString(), 'getTransactions', request);
}

/**
* Fetch all events that match a given set of filters.
*
Expand Down
174 changes: 174 additions & 0 deletions test/unit/server/soroban/get_transactions_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const {
xdr,
Keypair,
Account,
TransactionBuilder,
nativeToScVal,
XdrLargeInt,
} = StellarSdk;
const { Server, AxiosClient } = StellarSdk.rpc;

describe("Server#getTransactions", function () {
beforeEach(function () {
this.server = new Server(serverUrl);
this.axiosMock = sinon.mock(AxiosClient);
this.prepareAxios = (result) => {
this.axiosMock
.expects("post")
.withArgs(serverUrl, {
jsonrpc: "2.0",
id: 1,
method: "getTransactions",
params: {
startLedger: 1234,
limit: 10,
},
})
.returns(Promise.resolve({ data: { id: 1, result } }));
};
});

afterEach(function () {
this.axiosMock.verify();
this.axiosMock.restore();
});

it("fetches transactions successfully", function (done) {
const rawResult = makeGetTransactionsResult();
this.prepareAxios(rawResult);

let expected = JSON.parse(JSON.stringify(rawResult));
expected.transactions = expected.transactions.map((tx) => {
let parsedTx = { ...tx };
[
["envelopeXdr", xdr.TransactionEnvelope],
["resultXdr", xdr.TransactionResult],
["resultMetaXdr", xdr.TransactionMeta],
].forEach(([field, struct]) => {
parsedTx[field] = struct.fromXDR(tx[field], "base64");
});
if (tx.status === "SUCCESS") {
parsedTx.returnValue = parsedTx.resultMetaXdr
.v3()
.sorobanMeta()
.returnValue();
}
return parsedTx;
});

this.server
.getTransactions({ startLedger: 1234, limit: 10 })
.then((resp) => {
expect(Object.keys(resp)).to.eql(Object.keys(expected));
expect(resp.transactions.length).to.equal(expected.transactions.length);
expect(resp).to.eql(expected);
expect(resp.transactions[0].returnValue).to.eql(
new XdrLargeInt("u64", 1234).toScVal(),
);
expect(resp.transactions[1].returnValue).to.eql(
new XdrLargeInt("u64", 1235).toScVal(),
);
done();
})
.catch((err) => done(err));
});

it("empty transaction list", function (done) {
const result = {
transactions: [],
latestLedger: 100,
oldestLedger: 1,
oldestLedgerCloseTimestamp: 123456789,
latestLedgerCloseTimestamp: 987654321,
cursor: "123456",
};
this.prepareAxios(result);

this.server
.getTransactions({ startLedger: 1234, limit: 10 })
.then((resp) => {
expect(resp).to.deep.equal(result);
expect(resp.transactions).to.be.an("array").that.is.empty;
done();
})
.catch((err) => done(err));
});

it("handles errors", function (done) {
const errorResponse = {
code: -32600,
message: "Invalid request",
data: {
extras: {
reason: "Invalid startLedger",
},
},
};

this.axiosMock
.expects("post")
.withArgs(serverUrl, {
jsonrpc: "2.0",
id: 1,
method: "getTransactions",
params: { startLedger: -1, limit: 10 },
})
.returns(Promise.reject({ response: { data: errorResponse } }));

this.server
.getTransactions({ startLedger: -1, limit: 10 })
.then(() => {
done(new Error("Expected method to reject."));
})
.catch((err) => {
expect(err.response.data).to.eql(errorResponse);
done();
});
});
});

function makeGetTransactionsResult(count = 2) {
const transactions = [];
for (let i = 0; i < count; i++) {
transactions.push(makeTxResult(1234 + i, i + 1, "SUCCESS"));
}
return {
transactions,
latestLedger: 100,
latestLedgerCloseTimestamp: 987654321,
oldestLedger: 1,
oldestLedgerCloseTimestamp: 123456789,
cursor: "123456",
};
}

function makeTxResult(ledger, applicationOrder, status) {
const metaV3 = new xdr.TransactionMeta(
3,
new xdr.TransactionMetaV3({
ext: new xdr.ExtensionPoint(0),
txChangesBefore: [],
operations: [],
txChangesAfter: [],
sorobanMeta: new xdr.SorobanTransactionMeta({
ext: new xdr.SorobanTransactionMetaExt(0),
events: [],
diagnosticEvents: [],
returnValue: nativeToScVal(ledger),
}),
}),
);

return {
status: status,
ledger: ledger,
createdAt: ledger * 25 + 100,
applicationOrder: applicationOrder,
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: metaV3.toXDR("base64"),
};
}

0 comments on commit e6cda07

Please sign in to comment.