Skip to content

Commit

Permalink
Prepare v1.0.0-beta.0 (testnet) for release (#142)
Browse files Browse the repository at this point in the history
* Upgrade the codebase to work with testnet XDR (#135)
* Using [email protected]
* Prepare `v1.0.0-beta.0` for release (#143)
* Handle new RPC simulation response variations (#132)
* Add Core build reference to changelog for clarity
  • Loading branch information
Shaptic committed Sep 13, 2023
1 parent 6256073 commit 00f3d10
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 340 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ A breaking change should be clearly marked in this log.
## Unreleased


## v1.0.0-beta.0
**Note:** This version is currently only compatible with Stellar networks running `[email protected]`, which corresponds to Preview 11, the final Protocol 20 preview (using stellar/stellar-xdr@9ac0264).

### Breaking Changes
* The XDR has been upgraded to the final testnet version via the `stellar-base` dependency ([v10.0.0-beta.0](https://github.com/stellar/js-stellar-base/releases/tag/v10.0.0-beta.0), [#135](https://github.com/stellar/js-soroban-client/pull/135)).
* The `simulateTransaction` endpoint will now return a `restorePreamble` structure containing the recommended footprint and minimum resource fee for an `Operation.restoreFootprint` which would have made the simulation succeed ([#132](https://github.com/stellar/js-soroban-client/pull/132)).

### Fixed
* Result types are now handled correctly by `ContractSpec` ([#138](https://github.com/stellar/js-soroban-client/pull/138)).


## v0.11.2

### Fixed
Expand Down
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "soroban-client",
"version": "0.11.2",
"version": "1.0.0-beta.0",
"description": "A library for working with Stellar's Soroban RPC servers.",
"author": "Stellar Development Foundation <[email protected]>",
"homepage": "https://github.com/stellar/js-soroban-client",
Expand Down Expand Up @@ -77,30 +77,30 @@
]
},
"devDependencies": {
"@babel/cli": "^7.22.10",
"@babel/core": "^7.22.11",
"@babel/eslint-parser": "^7.22.11",
"@babel/cli": "^7.22.15",
"@babel/core": "^7.22.17",
"@babel/eslint-parser": "^7.22.15",
"@babel/eslint-plugin": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/preset-typescript": "^7.22.11",
"@babel/register": "^7.22.5",
"@definitelytyped/dtslint": "^0.0.176",
"@babel/preset-env": "^7.22.15",
"@babel/preset-typescript": "^7.22.15",
"@babel/register": "^7.22.15",
"@definitelytyped/dtslint": "^0.0.177",
"@istanbuljs/nyc-config-babel": "3.0.0",
"@stellar/tsconfig": "^1.0.2",
"@types/chai": "^4.3.5",
"@types/chai": "^4.3.6",
"@types/mocha": "^10.0.1",
"@types/node": "^20.5.4",
"@types/node": "^20.6.0",
"@types/sinon": "^10.0.16",
"@types/urijs": "^1.19.6",
"@typescript-eslint/parser": "^6.4.1",
"axios-mock-adapter": "^1.21.5",
"@typescript-eslint/parser": "^6.7.0",
"axios-mock-adapter": "^1.22.0",
"babel-loader": "^9.1.2",
"babel-plugin-istanbul": "^6.1.1",
"chai": "^4.3.8",
"chai-as-promised": "^7.1.1",
"chai-http": "^4.4.0",
"cross-env": "^7.0.3",
"eslint": "^8.47.0",
"eslint": "^8.49.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
Expand Down Expand Up @@ -139,7 +139,7 @@
"axios": "^1.4.0",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"stellar-base": "10.0.0-soroban.8",
"stellar-base": "v10.0.0-beta.0",
"urijs": "^1.19.1"
}
}
5 changes: 2 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ export class Server {
new xdr.LedgerKeyContractData({
key,
contract: scAddress,
durability: xdrDurability,
bodyType: xdr.ContractEntryBodyType.dataEntry(),
durability: xdrDurability
}),
);

Expand Down Expand Up @@ -572,7 +571,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: 93 additions & 14 deletions src/soroban_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,36 +164,115 @@ 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
| SimulateTransactionRestoreResponse
| SimulateTransactionErrorResponse;

export 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 or
* - there were no events
* @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 interface RawSimulateHostFunctionResult {
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;
}

interface RawSimulateHostFunctionResult {
// each string is SorobanAuthorizationEntry XDR in base64
auth?: string[];
// invocation return value: the ScVal in base64
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;
}
}
}
116 changes: 86 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);
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 @@ -118,41 +118,97 @@ export function parseRawSimulation(
// Gordon Ramsey in shambles
return sim;
}
return {

// shared across all responses
let base: SorobanRpc.BaseSimulateTransactionResponse = {
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')
) ?? [],
};

// error type: just has error string
if (typeof sim.error === 'string') {
return {
...base,
error: sim.error,
};
}

return parseSuccessful(sim, base);
}

function parseSuccessful(
sim: SorobanRpc.RawSimulateTransactionResponse,
partial: SorobanRpc.BaseSimulateTransactionResponse
):
| SorobanRpc.SimulateTransactionRestoreResponse
| SorobanRpc.SimulateTransactionSuccessResponse {

// success type: might have a result (if invoking) and...
const success: SorobanRpc.SimulateTransactionSuccessResponse = {
...partial,
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],
}
)
};

if (!sim.restorePreamble) {
return success;
}

// ...might have a restoration hint (if some state is expired)
return {
...success,
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
);
}

Expand Down
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

0 comments on commit 00f3d10

Please sign in to comment.