Skip to content

Commit

Permalink
Automatically decode XDR structures in simulation responses (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shaptic authored Aug 16, 2023
1 parent 3fde863 commit 9816725
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 106 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ A breaking change should be clearly marked in this log.
## Unreleased


### Breaking Changes
* `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 ([https://github.com/stellar/js-soroban-client/pull/127](#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.
* The RPC response schemas for simulation have been upgraded to parse the base64-encoded XDR automatically. The full interface changes are in the pull request ([https://github.com/stellar/js-soroban-client/pull/127](#127)), but succinctly:
- `SimulateTransactionResponse` -> `RawSimulateTransactionResponse`
- `SimulateHostFunctionResult` -> `RawSimulateHostFunctionResult`
- Now, `SimulateTransactionResponse` and `SimulateHostFunctionResult` now include the full, decoded XDR structures instead of raw, base64-encoded strings for the relevant fields (e.g. `SimulateTransactionResponse.transactionData` is now an instance of `SorobanDataBuilder`, `events` is now an `xdr.DiagnosticEvent[]` [try out `humanizeEvents` for a friendlier representation of this field])
- The `SimulateTransactionResponse.results[]` field has been moved to `SimulateTransactionResponse.result?`, since it will always be exactly zero or one result.

Not all schemas have been broken in this manner in order to facilitate user feedback on this approach. Please add your :+1: or :-1: to [#128](https://github.com/stellar/js-soroban-client/issues/128) to provide your perspective on whether or not we should do this for the other response schemas.


## v0.10.1

### Fixed
Expand Down
18 changes: 9 additions & 9 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import AxiosClient from "./axios";
import { Friendbot } from "./friendbot";
import * as jsonrpc from "./jsonrpc";
import { SorobanRpc } from "./soroban_rpc";
import { assembleTransaction } from "./transaction";
import { assembleTransaction, parseRawSimulation } from "./transaction";

export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000;

Expand Down Expand Up @@ -122,9 +122,8 @@ export class Server {
ledgerEntryData,
"base64",
).account();
const { high, low } = accountEntry.seqNum();
const sequence = BigInt(high) * BigInt(4294967296) + BigInt(low);
return new Account(address, sequence.toString());

return new Account(address, accountEntry.seqNum().toString());
}

/**
Expand Down Expand Up @@ -431,7 +430,7 @@ export class Server {
*
* server.simulateTransaction(transaction).then(sim => {
* console.log("cost:", sim.cost);
* console.log("results:", sim.results);
* console.log("result:", sim.result);
* console.log("error:", sim.error);
* console.log("latestLedger:", sim.latestLedger);
* });
Expand All @@ -450,11 +449,11 @@ export class Server {
public async simulateTransaction(
transaction: Transaction | FeeBumpTransaction,
): Promise<SorobanRpc.SimulateTransactionResponse> {
return await jsonrpc.post(
return await jsonrpc.post<SorobanRpc.RawSimulateTransactionResponse>(
this.serverURL.toString(),
"simulateTransaction",
transaction.toXDR(),
);
).then((raw) => parseRawSimulation(raw));
}

/**
Expand Down Expand Up @@ -546,10 +545,11 @@ export class Server {
if (simResponse.error) {
throw simResponse.error;
}
if (!simResponse.results || simResponse.results.length !== 1) {
if (!simResponse.result) {
throw new Error("transaction simulation failed");
}
return assembleTransaction(transaction, passphrase, simResponse);

return assembleTransaction(transaction, passphrase, simResponse).build();
}

/**
Expand Down
30 changes: 24 additions & 6 deletions src/soroban_rpc.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AssetType } from "stellar-base";
import { AssetType, SorobanDataBuilder, xdr } from "stellar-base";

// TODO: Better parsing for hashes, and base64-encoded xdr
// TODO: Better parsing for hashes

/* tslint:disable-next-line:no-namespace */
/* @namespace SorobanRpc
/**
* @namespace SorobanRpc
*/
export namespace SorobanRpc {
export interface Balance {
Expand Down Expand Up @@ -118,13 +119,30 @@ export namespace SorobanRpc {
}

export interface SimulateHostFunctionResult {
auth: xdr.SorobanAuthorizationEntry[];
retval: xdr.ScVal;
}

export interface SimulateTransactionResponse {
id: string;
error?: string;
transactionData: SorobanDataBuilder;
events: xdr.DiagnosticEvent[];
minResourceFee: string;
// only present if error isn't
result?: SimulateHostFunctionResult;
latestLedger: number;
cost: Cost;
}

export interface RawSimulateHostFunctionResult {
// each string is SorobanAuthorizationEntry XDR in base64
auth?: string[];
// function response as SCVal XDR in base64
// invocation return value: the ScVal in base64
xdr: string;
}

export interface SimulateTransactionResponse {
export interface RawSimulateTransactionResponse {
id: string;
error?: string;
// this is SorobanTransactionData XDR in base64
Expand All @@ -133,7 +151,7 @@ export namespace SorobanRpc {
minResourceFee: string;
// This will only contain a single element, because only a single
// invokeHostFunctionOperation is supported per transaction.
results: SimulateHostFunctionResult[];
results?: RawSimulateHostFunctionResult[];
latestLedger: number;
cost: Cost;
}
Expand Down
89 changes: 73 additions & 16 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
Operation,
Transaction,
TransactionBuilder,
xdr,
SorobanDataBuilder,
xdr
} from "stellar-base";

import { SorobanRpc } from "./soroban_rpc";
Expand All @@ -28,8 +29,10 @@ import { SorobanRpc } from "./soroban_rpc";
export function assembleTransaction(
raw: Transaction | FeeBumpTransaction,
networkPassphrase: string,
simulation: SorobanRpc.SimulateTransactionResponse
): Transaction {
simulation:
| SorobanRpc.SimulateTransactionResponse
| SorobanRpc.RawSimulateTransactionResponse
): TransactionBuilder {
if ("innerTransaction" in raw) {
// TODO: Handle feebump transactions
return assembleTransaction(
Expand All @@ -47,12 +50,13 @@ export function assembleTransaction(
);
}

if (simulation.results.length !== 1) {
throw new Error(`simulation results invalid: ${simulation.results}`);
const coalesced = parseRawSimulation(simulation);
if (!coalesced.result) {
throw new Error(`simulation incorrect: ${JSON.stringify(coalesced)}`);
}

const classicFeeNum = parseInt(raw.fee, 10) || 0;
const minResourceFeeNum = parseInt(simulation.minResourceFee, 10) || 0;
const classicFeeNum = parseInt(raw.fee);
const minResourceFeeNum = parseInt(coalesced.minResourceFee);

const txnBuilder = TransactionBuilder.cloneFrom(raw, {
// automatically update the tx fee that will be set on the resulting tx to
Expand All @@ -65,7 +69,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: simulation.transactionData,
sorobanData: coalesced.transactionData.build(),
});

switch (raw.operations[0].type) {
Expand All @@ -85,18 +89,13 @@ export function assembleTransaction(
//
// the intuition is "if auth exists, this tx has probably been
// simulated before"
auth:
existingAuth.length > 0
? existingAuth
: simulation.results[0].auth?.map((a) =>
xdr.SorobanAuthorizationEntry.fromXDR(a, "base64")
) ?? [],
auth: existingAuth.length > 0 ? existingAuth : coalesced.result.auth,
})
);
break;
}

return txnBuilder.build();
return txnBuilder;
}

function isSorobanTransaction(tx: Transaction): boolean {
Expand All @@ -108,9 +107,67 @@ function isSorobanTransaction(tx: Transaction): boolean {
case "invokeHostFunction":
case "bumpFootprintExpiration":
case "restoreFootprint":
return true;
return true

default:
return false;
}
}

/**
* Converts a raw response schema into one with parsed XDR fields and a
* simplified interface.
*
* @param raw the raw response schema (parsed ones are allowed, best-effort
* detected, and returned untouched)
*
* @returns the original parameter (if already parsed), parsed otherwise
*/
export function parseRawSimulation(
sim:
| SorobanRpc.SimulateTransactionResponse
| SorobanRpc.RawSimulateTransactionResponse
): SorobanRpc.SimulateTransactionResponse {
const looksRaw = isSimulationRaw(sim);
if (!looksRaw) {
// Gordon Ramsey in shambles
return sim;
}

return {
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
}),
};
}

function isSimulationRaw(
sim:
| SorobanRpc.SimulateTransactionResponse
| SorobanRpc.RawSimulateTransactionResponse
): sim is SorobanRpc.RawSimulateTransactionResponse {
// lazy check to determine parameter type
return (
(sim as SorobanRpc.SimulateTransactionResponse).result === undefined ||
(typeof sim.transactionData === "string" ||
((sim as SorobanRpc.RawSimulateTransactionResponse).results ?? [])
.length > 0)
);
}
Loading

0 comments on commit 9816725

Please sign in to comment.