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

Automatically decode XDR for getLedgerEntries and friends #154

Merged
merged 14 commits into from
Oct 6, 2023
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ A breaking change should be clearly marked in this log.

## Unreleased

### Breaking Changes
* The fields with XDR structures are now automatically decoded for the `Server.getLedgerEntries` response ([#154](https://github.com/stellar/js-soroban-client/pull/154)). Namely,
- `entries` is now guaranteed to exist, but it may be empty
- `entries[i].key` is an instance of `xdr.LedgerKey`
- the `entries[i].xdr` field is now `val`, instead
- `entries[i].val` is an instance of `xdr.LedgerEntryData`
* If you want to continue to use the raw RPC response, you can use `Server._getLedgerEntries` method, instead.


## v1.0.0-beta.2

Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ export * from "./soroban_rpc";
export { ContractSpec } from "./contract_spec";

// stellar-sdk classes to expose
export { Server } from "./server";
export { Server, Durability } from "./server";
export { AxiosClient, version } from "./axios";
export * from "./transaction";

// only needed for testing
Copy link
Contributor

Choose a reason for hiding this comment

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

once it's available in the wild, nothing prevents clients from using in their production side code, should comment just be removed or not exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't avoid exporting because it would mean it isn't available for browser testing, unfortunately :( you're right that nothing is stopping people from using it if they want. I added a comment to that effect in 47e445e.

export { parseRawSimulation } from "./parsers";

// expose classes and functions from stellar-base
export * from "stellar-base";

Expand Down
110 changes: 110 additions & 0 deletions src/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { xdr, SorobanDataBuilder } from 'stellar-base';
import { SorobanRpc } from './soroban_rpc';

export function parseLedgerEntries(
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
raw: SorobanRpc.RawGetLedgerEntriesResponse
): SorobanRpc.GetLedgerEntriesResponse {
return {
latestLedger: raw.latestLedger,
entries: (raw.entries ?? []).map(rawEntry => {
if (!rawEntry.key || !rawEntry.xdr) {
throw new TypeError(`invalid ledger entry: ${rawEntry}`);
}

return {
lastModifiedLedgerSeq: rawEntry.lastModifiedLedgerSeq,
key: xdr.LedgerKey.fromXDR(rawEntry.key, 'base64'),
val: xdr.LedgerEntryData.fromXDR(rawEntry.xdr, 'base64'),
};
})
};
}

/**
* 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 = SorobanRpc.isSimulationRaw(sim);
if (!looksRaw) {
// Gordon Ramsey in shambles
return sim;
}

// shared across all responses
let base: SorobanRpc.BaseSimulateTransactionResponse = {
_parsed: true,
id: sim.id,
latestLedger: sim.latestLedger,
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 || sim.restorePreamble.transactionData === '') {
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
),
}
};
}
43 changes: 21 additions & 22 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Address,
Contract,
FeeBumpTransaction,
StrKey,
Keypair,
Transaction,
xdr,
} from "stellar-base";
Expand All @@ -15,7 +15,8 @@ import AxiosClient from "./axios";
import { Friendbot } from "./friendbot";
import * as jsonrpc from "./jsonrpc";
import { SorobanRpc } from "./soroban_rpc";
import { assembleTransaction, parseRawSimulation } from "./transaction";
import { assembleTransaction } from "./transaction";
import { parseRawSimulation, parseLedgerEntries } from "./parsers";

export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000;

Expand Down Expand Up @@ -102,27 +103,19 @@ export class Server {
public async getAccount(address: string): Promise<Account> {
const ledgerKey = xdr.LedgerKey.account(
new xdr.LedgerKeyAccount({
accountId: xdr.PublicKey.publicKeyTypeEd25519(
StrKey.decodeEd25519PublicKey(address),
),
accountId: Keypair.fromPublicKey(address).xdrPublicKey(),
}),
);
const resp = await this.getLedgerEntries(ledgerKey);

const entries = resp.entries ?? [];
if (entries.length === 0) {
const resp = await this.getLedgerEntries(ledgerKey);
if (resp.entries.length === 0) {
return Promise.reject({
code: 404,
message: `Account not found: ${address}`,
});
}

const ledgerEntryData = entries[0].xdr;
const accountEntry = xdr.LedgerEntryData.fromXDR(
ledgerEntryData,
"base64",
).account();

const accountEntry = resp.entries[0].val.account();
return new Account(address, accountEntry.seqNum().toString());
}

Expand Down Expand Up @@ -170,8 +163,8 @@ export class Server {
* @example
* const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5";
* const key = xdr.ScVal.scvSymbol("counter");
* server.getContractData(contractId, key, 'temporary').then(data => {
* console.log("value:", data.xdr);
* server.getContractData(contractId, key, Durability.Temporary).then(data => {
* console.log("value:", data.val);
* console.log("lastModified:", data.lastModifiedLedgerSeq);
* console.log("latestLedger:", data.latestLedger);
* });
Expand Down Expand Up @@ -217,9 +210,8 @@ export class Server {

return this
.getLedgerEntries(contractKey)
.then((response) => {
const entries = response.entries ?? [];
if (entries.length === 0) {
.then((r: SorobanRpc.GetLedgerEntriesResponse) => {
if (r.entries.length === 0) {
return Promise.reject({
code: 404,
message: `Contract data not found. Contract: ${Address.fromScAddress(
Expand All @@ -230,7 +222,7 @@ export class Server {
});
}

return entries[0];
return r.entries[0];
});
}

Expand All @@ -249,6 +241,7 @@ export class Server {
* @returns {Promise<SorobanRpc.GetLedgerEntriesResponse>} the current
* on-chain values for the given ledger keys
*
* @see Server._getLedgerEntries
* @see https://soroban.stellar.org/api/methods/getLedgerEntries
* @example
* const contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM";
Expand All @@ -260,15 +253,21 @@ export class Server {
* server.getLedgerEntries([key]).then(response => {
* const ledgerData = response.entries[0];
* console.log("key:", ledgerData.key);
* console.log("value:", ledgerData.xdr);
* console.log("value:", ledgerData.val);
* console.log("lastModified:", ledgerData.lastModifiedLedgerSeq);
* console.log("latestLedger:", response.latestLedger);
* });
*/
public async getLedgerEntries(
...keys: xdr.LedgerKey[]
): Promise<SorobanRpc.GetLedgerEntriesResponse> {
return await jsonrpc.post(
return this._getLedgerEntries(...keys).then(r => parseLedgerEntries(r));
}

public async _getLedgerEntries(
...keys: xdr.LedgerKey[]
): Promise<SorobanRpc.RawGetLedgerEntriesResponse> {
return jsonrpc.post<SorobanRpc.RawGetLedgerEntriesResponse>(
this.serverURL.toString(),
"getLedgerEntries",
keys.map((k) => k.toXDR("base64")),
Expand Down
31 changes: 28 additions & 3 deletions src/soroban_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export namespace SorobanRpc {
}

export interface LedgerEntryResult {
lastModifiedLedgerSeq?: number;
key: xdr.LedgerKey;
val: xdr.LedgerEntryData;
}

export interface RawLedgerEntryResult {
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
lastModifiedLedgerSeq?: number;
/** a base-64 encoded {@link xdr.LedgerKey} instance */
key: string;
Expand All @@ -35,7 +41,12 @@ export namespace SorobanRpc {
/* Response for jsonrpc method `getLedgerEntries`
*/
export interface GetLedgerEntriesResponse {
entries: LedgerEntryResult[] | null;
entries: LedgerEntryResult[];
latestLedger: number;
}

export interface RawGetLedgerEntriesResponse {
entries?: RawLedgerEntryResult[];
latestLedger: number;
}

Expand Down Expand Up @@ -152,11 +163,17 @@ export namespace SorobanRpc {

export interface SendTransactionResponse {
status: SendTransactionStatus;
// errorResultXdr is only set when status is ERROR
errorResultXdr?: string;
hash: string;
latestLedger: number;
latestLedgerCloseTime: number;

/**
* This is a base64-encoded instance of {@link xdr.TransactionResult}, set
* only when `status` is `"ERROR"`.
*
* It contains details on why the network rejected the transaction.
*/
errorResultXdr?: string;
}

export interface SimulateHostFunctionResult {
Expand Down Expand Up @@ -252,6 +269,14 @@ export namespace SorobanRpc {
!!sim.restorePreamble.transactionData;
}

export function isSimulationRaw(
sim:
| SorobanRpc.SimulateTransactionResponse
| SorobanRpc.RawSimulateTransactionResponse
): sim is SorobanRpc.RawSimulateTransactionResponse {
return !(sim as SorobanRpc.SimulateTransactionResponse)._parsed;
}

interface RawSimulateHostFunctionResult {
// each string is SorobanAuthorizationEntry XDR in base64
auth?: string[];
Expand Down
Loading