Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle new RPC simulation response variations #132

Merged
merged 17 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export class Server {
: this.getNetwork(),
this.simulateTransaction(transaction),
]);
if (simResponse.error) {
if (SorobanRpc.isSimulationError(simResponse)) {
throw simResponse.error;
}
if (!simResponse.result) {
Expand Down
107 changes: 94 additions & 13 deletions src/soroban_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,90 @@ export namespace SorobanRpc {
retval: xdr.ScVal;
}

export interface SimulateTransactionResponse {
/**
* Simplifies {@link RawSimulateTransactionResponse} into separate interfaces
* based on status:
* - on success, this includes all fields, though `result` is only present
* if an invocation was simulated (since otherwise there's nothing to
* "resultify")
* - if there was an expiration error, this includes error and restoration
* fields
* - for all other errors, this only includes error fields
*
* @see https://soroban.stellar.org/api/methods/simulateTransaction#returns
*/
export type SimulateTransactionResponse =
| SimulateTransactionSuccessResponse
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
| SimulateTransactionRestoreResponse
| SimulateTransactionErrorResponse;

interface BaseSimulateTransactionResponse {
/** always present: the JSON-RPC request ID */
id: string;
error?: string;
transactionData: SorobanDataBuilder;

/** always present: the LCL known to the server when responding */
latestLedger: number;

/**
* The field is always present, but may be empty in cases where:
* - you didn't simulate an invocation,
* - there were no events, or
* - you are communicating with an RPC server w/o diagnostic events on
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
*
* @see {@link humanizeEvents}
*/
events: xdr.DiagnosticEvent[];
}

/** Includes simplified fields only present on success. */
export interface SimulateTransactionSuccessResponse
extends BaseSimulateTransactionResponse {
transactionData: SorobanDataBuilder;
minResourceFee: string;
// only present if error isn't
result?: SimulateHostFunctionResult;
latestLedger: number;
cost: Cost;

/** present only for invocation simulation */
result?: SimulateHostFunctionResult;
}

/** Includes details about why the simulation failed */
export interface SimulateTransactionErrorResponse
extends BaseSimulateTransactionResponse {
error: string;
events: xdr.DiagnosticEvent[];
}

export interface SimulateTransactionRestoreResponse
extends SimulateTransactionSuccessResponse {
result: SimulateHostFunctionResult; // not optional now

/**
* Indicates that a restoration is necessary prior to submission.
*
* In other words, seeing a restoration preamble means that your invocation
* was executed AS IF the required ledger entries were present, and this
* field includes information about what you need to restore for the
* simulation to succeed.
*/
restorePreamble: {
minResourceFee: string;
transactionData: SorobanDataBuilder;
}
}

export function isSimulationError(sim: SimulateTransactionResponse):
sim is SimulateTransactionErrorResponse {
return 'error' in sim;
}

export function isSimulationSuccess(sim: SimulateTransactionResponse):
sim is SimulateTransactionSuccessResponse {
return 'transactionData' in sim;
}

export function isSimulationRestore(sim: SimulateTransactionResponse):
sim is SimulateTransactionRestoreResponse {
return isSimulationSuccess(sim) && 'restorePreamble' in sim;
}

export interface RawSimulateHostFunctionResult {
Expand All @@ -181,17 +255,24 @@ export namespace SorobanRpc {
xdr: string;
}

/** @see https://soroban.stellar.org/api/methods/simulateTransaction#returns */
export interface RawSimulateTransactionResponse {
id: string;
latestLedger: number;
error?: string;
// this is SorobanTransactionData XDR in base64
transactionData: string;
events: string[];
minResourceFee: string;
// This will only contain a single element, because only a single
// this is an xdr.SorobanTransactionData in base64
transactionData?: string;
// 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.
results?: RawSimulateHostFunctionResult[];
latestLedger: number;
cost: Cost;
cost?: Cost;
// present if succeeded but has expired ledger entries
restorePreamble?: {
minResourceFee: string;
transactionData: string;
}
}
}
100 changes: 70 additions & 30 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export function assembleTransaction(
);
}

const coalesced = parseRawSimulation(simulation);
if (!coalesced.result) {
throw new Error(`simulation incorrect: ${JSON.stringify(coalesced)}`);
let success = parseRawSimulation(simulation);
Copy link
Contributor

Choose a reason for hiding this comment

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

minor nit, your call, it's not success yet, maybe simulationResponse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but it should be! heh. slightly more annoying to read later lines if it's something like parsed, and reassigning after the if (!isSuccess...) check is also slightly more annoying 🙃

if (!SorobanRpc.isSimulationSuccess(success)) {
throw new Error(`simulation incorrect: ${JSON.stringify(success)}`);
}

const classicFeeNum = parseInt(raw.fee) || 0;
const minResourceFeeNum = parseInt(coalesced.minResourceFee) || 0;
const minResourceFeeNum = parseInt(success.minResourceFee) || 0;
const txnBuilder = TransactionBuilder.cloneFrom(raw, {
// automatically update the tx fee that will be set on the resulting tx to
// the sum of 'classic' fee provided from incoming tx.fee and minResourceFee
Expand All @@ -70,7 +70,7 @@ export function assembleTransaction(
// soroban transaction will be equal to incoming tx.fee + minResourceFee.
fee: (classicFeeNum + minResourceFeeNum).toString(),
// apply the pre-built Soroban Tx Data from simulation onto the Tx
sorobanData: coalesced.transactionData.build(),
sorobanData: success.transactionData.build(),
networkPassphrase
});

Expand All @@ -90,7 +90,7 @@ export function assembleTransaction(
//
// the intuition is "if auth exists, this tx has probably been
// simulated before"
auth: existingAuth.length > 0 ? existingAuth : coalesced.result.auth,
auth: existingAuth.length > 0 ? existingAuth : success.result!.auth,
})
);
break;
Expand Down Expand Up @@ -134,40 +134,80 @@ export function parseRawSimulation(
// Gordon Ramsey in shambles
return sim;
}
return {

// shared across all responses
let base = {
id: sim.id,
minResourceFee: sim.minResourceFee,
latestLedger: sim.latestLedger,
cost: sim.cost,
transactionData: new SorobanDataBuilder(sim.transactionData),
events: (sim.events ?? []).map((event) =>
xdr.DiagnosticEvent.fromXDR(event, "base64")
),
...(sim.error !== undefined && { error: sim.error }), // only if present
// ^ XOR v
...((sim.results ?? []).length > 0 && {
result: sim.results?.map((result) => {
return {
auth: (result.auth ?? []).map((entry) =>
xdr.SorobanAuthorizationEntry.fromXDR(entry, "base64")
),
retval: xdr.ScVal.fromXDR(result.xdr, "base64"),
};
})[0], // only if present
}),
events: sim.events?.map(
evt => xdr.DiagnosticEvent.fromXDR(evt, 'base64')
) ?? [],
};

return (typeof sim.error === 'string')
// error type: just has error string
? {
...base,
error: sim.error,
}
// success type: might have a result (if invoking) and might have a
// restoration hint (if invoking AND some state is expired)
: {
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
...base,
transactionData: new SorobanDataBuilder(sim.transactionData!),
minResourceFee: sim.minResourceFee!,
cost: sim.cost!,
...(
// coalesce 0-or-1-element results[] list into a single result struct
// with decoded fields if present
(sim.results?.length ?? 0 > 0) &&
{
result: sim.results!.map(row => {
return {
auth: (row.auth ?? []).map((entry) =>
xdr.SorobanAuthorizationEntry.fromXDR(entry, 'base64')),
// if return value is missing ("falsy") we coalesce to void
retval: !!row.xdr
? xdr.ScVal.fromXDR(row.xdr, 'base64')
: xdr.ScVal.scvVoid()
}
})[0],
}
),
...(
sim.restorePreamble !== undefined &&
{
restorePreamble: {
minResourceFee: sim.restorePreamble!.minResourceFee,
transactionData: new SorobanDataBuilder(
sim.restorePreamble!.transactionData
),
}
}
),
};
}

function isSimulationRaw(
sim:
| SorobanRpc.SimulateTransactionResponse
| SorobanRpc.RawSimulateTransactionResponse
): sim is SorobanRpc.RawSimulateTransactionResponse {
// lazy check to determine parameter type
const asGud = sim as SorobanRpc.SimulateTransactionRestoreResponse;
const asRaw = sim as SorobanRpc.RawSimulateTransactionResponse;

// lazy checks to determine type: check existence of parsed-only fields note
return (
(sim as SorobanRpc.SimulateTransactionResponse).result === undefined ||
(typeof sim.transactionData === "string" ||
((sim as SorobanRpc.RawSimulateTransactionResponse).results ?? [])
.length > 0)
asRaw.restorePreamble !== undefined ||
!(
asGud.restorePreamble !== undefined ||
asGud.result !== undefined ||
typeof asGud.transactionData !== 'string'
) ||
(asRaw.error !== undefined && (
!asRaw.events?.length ||
typeof asRaw.events![0] === 'string'
)) ||
(asRaw.results ?? []).length > 0
);
}
4 changes: 2 additions & 2 deletions test/test-nodejs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ global.axios = require('axios');
global.AxiosClient = SorobanClient.AxiosClient;
global.serverUrl = 'https://horizon-live.stellar.org:1337/api/v1/jsonrpc';

var chaiAsPromised = require('chai-as-promised');
var chaiHttp = require('chai-http');
const chaiAsPromised = require('chai-as-promised');
const chaiHttp = require('chai-http');
global.chai = require('chai');
global.chai.should();
global.chai.use(chaiAsPromised);
Expand Down
Loading
Loading