diff --git a/CHANGELOG.md b/CHANGELOG.md index 0684b8da..1134ecad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ A breaking change should be clearly marked in this log. ### Breaking Changes * The minimum supported NodeJS version is now Node 16. +<<<<<<< HEAD * `Server.prepareTransaction` now returns a `TransactionBuilder` instance rather than an immutable `Transaction`, in order to facilitate modifying your transaction after assembling it alongside the simulation response ([#127](https://github.com/stellar/js-soroban-client/pull/127)). - The intent is to avoid cloning the transaction again (via `TransactionBuilder.cloneFrom`) if you need to modify parameters such as the storage access footprint. - To migrate your code, just call `.build()` on the return value. diff --git a/src/server.ts b/src/server.ts index cd02aa7d..591b5d7b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -29,8 +29,8 @@ export interface GetEventsRequest { * Specifies the durability namespace of contract-related ledger entries. */ export enum Durability { - Temporary = 'temporary', - Persistent = 'persistent', + Temporary = "temporary", + Persistent = "persistent", } /** @@ -178,7 +178,7 @@ export class Server { ): Promise { // coalesce `contract` param variants to an ScAddress let scAddress: xdr.ScAddress; - if (typeof contract === 'string') { + if (typeof contract === "string") { scAddress = new Contract(contract).address().toScAddress(); } else if (contract instanceof Address) { scAddress = contract.toScAddress(); @@ -207,24 +207,30 @@ export class Server { contract: scAddress, key, durability: xdrDurability, - bodyType: xdr.ContractEntryBodyType.dataEntry() // expirationExtension is internal - }) + bodyType: xdr.ContractEntryBodyType.dataEntry(), // expirationExtension is internal + }), ).toXDR("base64"); - return jsonrpc.post( - this.serverURL.toString(), - "getLedgerEntries", - [contractKey], - ).then(response => { + return jsonrpc + .post( + this.serverURL.toString(), + "getLedgerEntries", + [contractKey], + ) + .then((response) => { const ledgerEntries = response.entries ?? []; - if (ledgerEntries.length !== 1) { - return Promise.reject({ - code: 404, - message: `Contract data not found. Contract: ${Address.fromScAddress(scAddress).toString()}, Key: ${key.toXDR("base64")}, Durability: ${durability}`, - }); - } - return ledgerEntries[0]; - }); + if (ledgerEntries.length !== 1) { + return Promise.reject({ + code: 404, + message: `Contract data not found. Contract: ${Address.fromScAddress( + scAddress, + ).toString()}, Key: ${key.toXDR( + "base64", + )}, Durability: ${durability}`, + }); + } + return ledgerEntries[0]; + }); } /** @@ -283,18 +289,55 @@ export class Server { * * @param {string} hash - The hex-encoded hash of the transaction to check. * - * @returns {Promise} Returns a - * promise to the {@link SorobanRpc.GetTransactionResponse} object - * with the status, result, and other details about the transaction. + * @returns {Promise} Returns a promise to + * the {@link SorobanRpc.GetTransactionResponse} object with the status, + * result, and other details about the transaction. Raw XDR fields are + * parsed into their appropriate structures wherever possible. */ public async getTransaction( hash: string, ): Promise { - return await jsonrpc.post( + const raw = await jsonrpc.post( this.serverURL.toString(), "getTransaction", hash, ); + + let successInfo: Omit< + SorobanRpc.GetSuccessfulTransactionResponse, + keyof SorobanRpc.GetFailedTransactionResponse + > = {} as any; + + if (raw.status === SorobanRpc.GetTransactionStatus.SUCCESS) { + const meta = xdr.TransactionMeta.fromXDR(raw.resultMetaXdr!, "base64"); + successInfo = { + 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 && { + returnValue: meta.v3().sorobanMeta()?.returnValue(), + }), + }; + } + + const result: SorobanRpc.GetTransactionResponse = { + status: raw.status, + latestLedger: raw.latestLedger, + latestLedgerCloseTime: raw.latestLedgerCloseTime, + oldestLedger: raw.oldestLedger, + oldestLedgerCloseTime: raw.oldestLedgerCloseTime, + ...successInfo, + }; + + return result; } /** @@ -343,8 +386,6 @@ export class Server { // is an ScSymbol and the last is a U32. // // The difficulty comes in matching up the correct integer primitives. - // - // It also means this library will rely on the XDR definitions. return await jsonrpc.postObject(this.serverURL.toString(), "getEvents", { filters: request.filters ?? [], pagination: { @@ -445,11 +486,13 @@ export class Server { public async simulateTransaction( transaction: Transaction | FeeBumpTransaction, ): Promise { - return await jsonrpc.post( - this.serverURL.toString(), - "simulateTransaction", - transaction.toXDR(), - ).then((raw) => parseRawSimulation(raw)); + return await jsonrpc + .post( + this.serverURL.toString(), + "simulateTransaction", + transaction.toXDR(), + ) + .then((raw) => parseRawSimulation(raw)); } /** diff --git a/src/soroban_rpc.ts b/src/soroban_rpc.ts index 2ba796b9..70d272b0 100644 --- a/src/soroban_rpc.ts +++ b/src/soroban_rpc.ts @@ -54,9 +54,48 @@ export namespace SorobanRpc { protocolVersion: string; } - export type GetTransactionStatus = "SUCCESS" | "NOT_FOUND" | "FAILED"; + export enum GetTransactionStatus { + SUCCESS = "SUCCESS", + NOT_FOUND = "NOT_FOUND", + FAILED = "FAILED" + } + + export type GetTransactionResponse = + | GetSuccessfulTransactionResponse + | GetFailedTransactionResponse + | GetMissingTransactionResponse; + + interface GetAnyTransactionResponse { + status: GetTransactionStatus; + latestLedger: number; + latestLedgerCloseTime: number; + oldestLedger: number; + oldestLedgerCloseTime: number; + } + + export interface GetMissingTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.NOT_FOUND; + } + + export interface GetFailedTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.FAILED; + } + + export interface GetSuccessfulTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.SUCCESS; + + ledger: number; + createdAt: number; + applicationOrder: number; + feeBump: boolean; + envelopeXdr: xdr.TransactionEnvelope; + resultXdr: xdr.TransactionResult; + resultMetaXdr: xdr.TransactionMeta; + + returnValue?: xdr.ScVal; // present iff resultMeta is a v3 + } - export interface GetTransactionResponse { + export interface RawGetTransactionResponse { status: GetTransactionStatus; latestLedger: number; latestLedgerCloseTime: number; diff --git a/test/unit/server/get_transaction_test.js b/test/unit/server/get_transaction_test.js index aad2b290..a0872d6e 100644 --- a/test/unit/server/get_transaction_test.js +++ b/test/unit/server/get_transaction_test.js @@ -1,14 +1,24 @@ +const { + xdr, + Keypair, + Account, + Server, + TransactionBuilder, + nativeToScVal, + XdrLargeInt +} = SorobanClient; + describe('Server#getTransaction', function () { - let keypair = SorobanClient.Keypair.random(); + let keypair = Keypair.random(); let account = new SorobanClient.Account( keypair.publicKey(), '56199647068161' ); beforeEach(function () { - this.server = new SorobanClient.Server(serverUrl); + this.server = new Server(serverUrl); this.axiosMock = sinon.mock(AxiosClient); - let transaction = new SorobanClient.TransactionBuilder(account, { + let transaction = new TransactionBuilder(account, { fee: 100, networkPassphrase: SorobanClient.Networks.TESTNET, v1: true @@ -28,6 +38,17 @@ describe('Server#getTransaction', function () { this.transaction = transaction; this.hash = this.transaction.hash().toString('hex'); this.blob = transaction.toEnvelope().toXDR().toString('base64'); + this.prepareAxios = (result) => { + this.axiosMock + .expects('post') + .withArgs(serverUrl, { + jsonrpc: '2.0', + id: 1, + method: 'getTransaction', + params: [this.hash] + }) + .returns(Promise.resolve({ data: { id: 1, result } })); + }; }); afterEach(function () { @@ -36,32 +57,108 @@ describe('Server#getTransaction', function () { }); it('transaction not found', function (done) { - const result = { - status: 'NOT_FOUND', - latestLedger: 100, - latestLedgerCloseTime: 12345, - oldestLedger: 50, - oldestLedgerCloseTime: 500 - }; - this.axiosMock - .expects('post') - .withArgs(serverUrl, { - jsonrpc: '2.0', - id: 1, - method: 'getTransaction', - params: [this.hash] + const result = makeTxResult('NOT_FOUND'); + this.prepareAxios(result); + + this.server + .getTransaction(this.hash) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); }) - .returns(Promise.resolve({ data: { id: 1, result } })); + .catch((err) => done(err)); + }); + + it('transaction success', function (done) { + const result = makeTxResult('SUCCESS', true); + this.prepareAxios(result); - this.server.getTransaction(this.hash).then(function (response) { - expect(response).to.be.deep.equal(result); - done(); + let expected = JSON.parse(JSON.stringify(result)); + [ + ['envelopeXdr', xdr.TransactionEnvelope], + ['resultXdr', xdr.TransactionResult], + ['resultMetaXdr', xdr.TransactionMeta] + ].forEach(([field, struct]) => { + expected[field] = struct.fromXDR(result[field], 'base64'); }); + expected.returnValue = expected.resultMetaXdr + .v3() + .sorobanMeta() + .returnValue(); + + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(Object.keys(resp)).to.eql(Object.keys(expected)); + expect(resp).to.eql(expected); + expect(resp.returnValue).to.eql(new XdrLargeInt('u64', 1234).toScVal()); + done(); + }) + .catch((err) => done(err)); }); - xit('transaction pending', function (done) {}); + xit('non-Soroban transaction success', function (done) { + const result = makeTxResult('SUCCESS', false); + this.prepareAxios(result); - xit('transaction success', function (done) {}); + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(resp).to.be.deep.equal(result); + done(); + }) + .catch((err) => done(err)); + }); + xit('transaction pending', function (done) {}); xit('transaction error', function (done) {}); }); + +function makeTxResult(status, addSoroban = true) { + const metaV3 = new xdr.TransactionMeta( + 3, + new xdr.TransactionMetaV3({ + ext: new xdr.ExtensionPoint(0), + txChangesBefore: [], + operations: [], + txChangesAfter: [], + sorobanMeta: new xdr.SorobanTransactionMeta({ + ext: new xdr.ExtensionPoint(0), + events: [], + diagnosticEvents: [], + returnValue: nativeToScVal(1234) + }) + }) + ); + + // only injected in the success case + // + // this data was picked from a random transaction in horizon: + // aa6a8e198abe53c7e852e4870413b29fe9ef04da1415a97a5de1a4ae489e11e2 + const successInfo = { + 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: metaV3.toXDR('base64') + }; + + if (!addSoroban) { + // replace the V3 Soroban meta with a "classic" V2 version + successInfo.resultMetaXdr = + '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'; + } + + return { + status, + latestLedger: 100, + latestLedgerCloseTime: 12345, + oldestLedger: 50, + oldestLedgerCloseTime: 500, + ...(status === 'SUCCESS' && successInfo) + }; +}