diff --git a/CHANGELOG.md b/CHANGELOG.md index 6167053b8..9176304d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ A breaking change will get clearly marked in this log. ## Unreleased +Added: + +- `rpc.server.simulateTransaction` now supports optional stateChanges as mentioned below ([#963](https://github.com/stellar/js-stellar-sdk/pull/963)) +- If `Before` is omitted, it constitutes a creation, if `After` is omitted, it constitutes a deletions, note that `Before` and `After` cannot be be omitted at the same time. + + +``` +/** State Difference information */ + stateChanges?: LedgerEntryChange[]; + +interface LedgerEntryChange{ + type: number; + key: xdr.LedgerKey; + before: xdr.LedgerEntry | null; + after: xdr.LedgerEntry | null; + } + +``` ## [v12.0.0-rc.3](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.0-rc.3) diff --git a/src/rpc/api.ts b/src/rpc/api.ts index 3c8d7806f..25591b379 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -1,15 +1,8 @@ -import { AssetType, Contract, SorobanDataBuilder, xdr } from '@stellar/stellar-base'; +import { Contract, SorobanDataBuilder, xdr } from '@stellar/stellar-base'; /* tslint:disable-next-line:no-namespace */ /** @namespace Api */ export namespace Api { - export interface Balance { - asset_type: AssetType.credit4 | AssetType.credit12; - asset_code: string; - asset_issuer: string; - classic: string; - smart: string; - } export interface Cost { cpuInsns: string; @@ -175,8 +168,21 @@ export namespace Api { value: string; } - export interface RequestAirdropResponse { - transaction_id: string; + interface RawLedgerEntryChange { + type: number; + /** This is LedgerKey in base64 */ + key: string; + /** This is xdr.LedgerEntry in base64 */ + before: string | null; + /** This is xdr.LedgerEntry in base64 */ + after: string | null; + } + + export interface LedgerEntryChange { + type: number; + key: xdr.LedgerKey; + before: xdr.LedgerEntry | null; + after: xdr.LedgerEntry | null; } export type SendTransactionStatus = @@ -264,6 +270,9 @@ export namespace Api { /** present only for invocation simulation */ result?: SimulateHostFunctionResult; + + /** State Difference information */ + stateChanges?: LedgerEntryChange[]; } /** Includes details about why the simulation failed */ @@ -333,19 +342,23 @@ export namespace Api { id: string; latestLedger: number; error?: string; - // this is an xdr.SorobanTransactionData in base64 + /** This is an xdr.SorobanTransactionData in base64 */ transactionData?: string; - // these are xdr.DiagnosticEvents in base64 + /** These are xdr.DiagnosticEvents in base64 */ events?: string[]; minResourceFee?: string; - // This will only contain a single element if present, because only a single - // invokeHostFunctionOperation is supported per transaction. + /** This will only contain a single element if present, because only a single + * invokeHostFunctionOperation is supported per transaction. + * */ results?: RawSimulateHostFunctionResult[]; cost?: Cost; - // present if succeeded but has expired ledger entries + /** Present if succeeded but has expired ledger entries */ restorePreamble?: { minResourceFee: string; transactionData: string; }; + + /** State Difference information */ + stateChanges?: RawLedgerEntryChange[]; } } diff --git a/src/rpc/parsers.ts b/src/rpc/parsers.ts index 8ff1d1c6d..a54a83434 100644 --- a/src/rpc/parsers.ts +++ b/src/rpc/parsers.ts @@ -139,7 +139,19 @@ function parseSuccessful( : xdr.ScVal.scvVoid() }; })[0] - }) + }), + + ...(sim.stateChanges?.length ?? 0 > 0) && { + stateChanges: sim.stateChanges?.map((entryChange) => { + return { + type: entryChange.type, + key: xdr.LedgerKey.fromXDR(entryChange.key, 'base64'), + before: entryChange.before ? xdr.LedgerEntry.fromXDR(entryChange.before, 'base64') : null, + after: entryChange.after ? xdr.LedgerEntry.fromXDR(entryChange.after, 'base64') : null, + }; + }) + } + }; if (!sim.restorePreamble || sim.restorePreamble.transactionData === '') { diff --git a/test/unit/server/soroban/simulate_transaction_test.js b/test/unit/server/soroban/simulate_transaction_test.js index e35322e2d..863ad6b13 100644 --- a/test/unit/server/soroban/simulate_transaction_test.js +++ b/test/unit/server/soroban/simulate_transaction_test.js @@ -18,6 +18,14 @@ describe("Server#simulateTransaction", async function (done) { let contract = new StellarSdk.Contract(contractId); let address = contract.address().toScAddress(); + const accountId = + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const accountKey = xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ); + const simulationResponse = await invokeSimulationResponse(address); const parsedSimulationResponse = { id: simulationResponse.id, @@ -32,6 +40,22 @@ describe("Server#simulateTransaction", async function (done) { retval: xdr.ScVal.fromXDR(simulationResponse.results[0].xdr, "base64"), }, cost: simulationResponse.cost, + stateChanges: [ + { + type: 2, + key: accountKey, + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + }, + ], _parsed: true, }; @@ -166,6 +190,53 @@ describe("Server#simulateTransaction", async function (done) { ); }); + it("works with state changes", async function () { + return invokeSimulationResponseWithStateChanges(address).then( + (simResponse) => { + const expected = cloneSimulation(parsedSimulationResponse); + expected.stateChanges = [ + { + type: 2, + key: accountKey, + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + }, + { + type: 1, + key: accountKey, + before: null, + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + }, + { + type: 3, + key: accountKey, + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + after: null, + }, + ] + + const parsed = parseRawSimulation(simResponse); + expect(parsed).to.be.deep.equal(expected); + }, + ); + }); + it("works with errors", function () { let simResponse = simulationResponseError(); @@ -175,6 +246,7 @@ describe("Server#simulateTransaction", async function (done) { delete expected.cost; delete expected.transactionData; delete expected.minResourceFee; + delete expected.stateChanges; expected.error = "This is an error"; expected.events = []; @@ -200,6 +272,7 @@ function cloneSimulation(sim) { retval: xdr.ScVal.fromXDR(sim.result.retval.toXDR()), }, cost: sim.cost, + stateChanges: sim.stateChanges, _parsed: sim._parsed, }; } @@ -251,6 +324,8 @@ function simulationResponseError(events) { } function baseSimulationResponse(results) { + const accountId = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + return { id: 1, events: [], @@ -262,6 +337,26 @@ function baseSimulationResponse(results) { cpuInsns: "1", memBytes: "2", }, + stateChanges: [ + { + type: 2, + key: xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ).toXDR("base64"), + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + } + ], }; } @@ -275,6 +370,64 @@ async function invokeSimulationResponseWithRestoration(address) { }; } +async function invokeSimulationResponseWithStateChanges(address) { + const accountId = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + + return { + + ...(await invokeSimulationResponse(address)), + stateChanges: [ + { + type: 2, + key: xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ).toXDR("base64"), + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + }, + { + type: 1, + key: xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ).toXDR("base64"), + before: null, + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + }, + { + type: 3, + key: xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ).toXDR("base64"), + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }).toXDR("base64"), + after: null, + }, + ], + }; +} + + describe("works with real responses", function () { const schema = { transactionData: