Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 .github/workflows/bindings-ts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-20.04
services:
rpc:
image: stellar/quickstart:soroban-dev@sha256:a6b03cf6b0433c99f2f799b719f0faadbb79684b1b763e7674ba749fb0f648ee
image: stellar/quickstart:testing@sha256:1c98f895f8b69cc843eeaa5230d67044dbeb390a5529d51dd7762d8ff685c3f8
ports:
- 8000:8000
env:
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ version = "0.9.1"
git = "https://github.com/stellar/rs-soroban-sdk"
rev = "e48a3c435a1c1e1055f03f8e507c63f62a927ef9"

[workspace.dependencies.soroban-token-sdk]
version = "0.9.1"
git = "https://github.com/stellar/rs-soroban-sdk"
rev = "e48a3c435a1c1e1055f03f8e507c63f62a927ef9"

[workspace.dependencies.soroban-ledger-snapshot]
version = "0.9.1"
git = "https://github.com/stellar/rs-soroban-sdk"
Expand Down
113 changes: 67 additions & 46 deletions cmd/crates/soroban-spec-typescript/src/project_template/src/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MethodOptions,
ResponseTypes,
Wallet,
XDR_BASE64,
} from "./method-options.js";

export type Tx = Transaction<Memo<MemoType>, Operation[]>;
Expand All @@ -34,16 +35,12 @@ async function getAccount(
return await server.getAccount(publicKey);
}

export class NotImplementedError extends Error {}
export class NotImplementedError extends Error { }

type Simulation = SorobanClient.SorobanRpc.SimulateTransactionResponse;
type SendTx = SorobanClient.SorobanRpc.SendTransactionResponse;
type GetTx = SorobanClient.SorobanRpc.GetTransactionResponse;

// defined this way so typeahead shows full union, not named alias
let someRpcResponse: Simulation | SendTx | GetTx;
type SomeRpcResponse = typeof someRpcResponse;

type InvokeArgs<R extends ResponseTypes, T = string> = MethodOptions<R> &
ClassOptions & {
method: string;
Expand All @@ -61,13 +58,13 @@ type InvokeArgs<R extends ResponseTypes, T = string> = MethodOptions<R> &
export async function invoke<R extends ResponseTypes = undefined, T = string>(
args: InvokeArgs<R, T>
): Promise<
R extends undefined
? T
: R extends "simulated"
? Simulation
: R extends "full"
? SomeRpcResponse
: T
R extends "simulated"
? { txUnsigned: XDR_BASE64, simulation: Simulation }
: R extends "full"
? { txUnsigned: XDR_BASE64, txSigned?: XDR_BASE64, simulation: Simulation, sendTransactionResponse?: SendTx, getTransactionResponse?: GetTx, getTransactionResponseAll?: GetTx[] }
: R extends undefined
? { txUnsigned: XDR_BASE64, txSigned?: XDR_BASE64, simulation: Simulation, result: T, sendTransactionResponse?: SendTx, getTransactionResponse?: GetTx, getTransactionResponseAll?: GetTx[] }
: { txUnsigned: XDR_BASE64, txSigned?: XDR_BASE64, simulation: Simulation, result: T, sendTransactionResponse?: SendTx, getTransactionResponse?: GetTx, getTransactionResponseAll?: GetTx[] }
>;
export async function invoke<R extends ResponseTypes, T = string>({
method,
Expand All @@ -80,7 +77,7 @@ export async function invoke<R extends ResponseTypes, T = string>({
networkPassphrase,
contractId,
wallet,
}: InvokeArgs<R, T>): Promise<T | string | SomeRpcResponse> {
}: InvokeArgs<R, T>): Promise<{ txUnsigned: XDR_BASE64, txSigned?: XDR_BASE64, simulation: Simulation, result?: T, sendTransactionResponse?: SendTx, getTransactionResponse?: GetTx, getTransactionResponseAll?: GetTx[] }> {
wallet = wallet ?? (await import("@stellar/freighter-api")).default;
let parse = parseResultXdr;
const server = new SorobanClient.Server(rpcUrl, {
Expand All @@ -98,22 +95,22 @@ export async function invoke<R extends ResponseTypes, T = string>({

const contract = new SorobanClient.Contract(contractId);

let tx = new SorobanClient.TransactionBuilder(account, {
const txUnsigned = new SorobanClient.TransactionBuilder(account, {
fee: fee.toString(10),
networkPassphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(SorobanClient.TimeoutInfinite)
.build();
const simulated = await server.simulateTransaction(tx);
const simulation = await server.simulateTransaction(txUnsigned);

if (simulated.error) throw simulated.error;
if (responseType === "simulated") return simulated;
if (simulation.error) throw simulation.error;
if (responseType === "simulated") return { txUnsigned: txUnsigned.toXDR(), simulation };

// is it possible for `auths` to be present but empty? Probably not, but let's be safe.
let authsCount = simulated.result!.auth?.length ?? 0;
let authsCount = simulation.result!.auth?.length ?? 0;

const writeLength = simulated.transactionData
const writeLength = simulation.transactionData
.build()
.resources()
.footprint()
Expand All @@ -122,18 +119,18 @@ export async function invoke<R extends ResponseTypes, T = string>({
const isViewCall = authsCount === 0 && writeLength === 0;

if (isViewCall) {
if (responseType === "full") return simulated;
if (responseType === "full") return { txUnsigned: txUnsigned.toXDR(), simulation };

const retval = simulated.result?.retval;
const retval = simulation.result?.retval;
if (!retval) {
if (simulated.error) {
throw new Error(simulated.error as unknown as string);
if (simulation.error) {
throw new Error(simulation.error as unknown as string);
}
throw new Error(
`Invalid response from simulateTransaction:\n{simulated}`
`Invalid response from simulateTransaction:\n${simulation}`
);
}
return parseResultXdr(retval);
return { txUnsigned: txUnsigned.toXDR(), simulation, result: parseResultXdr(retval) };
}

if (authsCount > 1) {
Expand All @@ -154,26 +151,43 @@ export async function invoke<R extends ResponseTypes, T = string>({
throw new Error("Not connected to Freighter");
}

tx = await signTx(
const txSigned = await signTx(
wallet,
SorobanClient.assembleTransaction(tx, networkPassphrase, simulated).build(),
SorobanClient.assembleTransaction(txUnsigned, networkPassphrase, simulation).build(),
networkPassphrase
);

const raw = await sendTx(tx, secondsToWait, server);
const data = {
simulation,
txUnsigned: txUnsigned.toXDR(),
txSigned: txSigned.toXDR(),
...await sendTx(txSigned, secondsToWait, server)
};

if (responseType === "full") return raw;
if (responseType === "full") return data;

// if `sendTx` awaited the inclusion of the tx in the ledger, it used
// `getTransaction`, which has a `returnValue` field
if ("returnValue" in raw) return parse(raw.returnValue!);
// if `sendTx` awaited the inclusion of the tx in the ledger, it used `getTransaction`
if (
"getTransactionResponse" in data &&
data.getTransactionResponse
) {
// getTransactionResponse has a `returnValue` field unless it failed
if ("returnValue" in data.getTransactionResponse) return {
...data,
result: parse(data.getTransactionResponse.returnValue!)
};

// if "returnValue" not present, the transaction failed; return without parsing the result
console.error("Transaction failed! Cannot parse result.");
return data;
}

// otherwise, it returned the result of `sendTransaction`
if ("errorResultXdr" in raw) return parse(raw.errorResultXdr!);

// if neither of these are present, something went wrong
console.error("Don't know how to parse result! Returning full RPC response.");
return raw;
// if it didn't await, it returned the result of `sendTransaction`
return {
...data,
result: parse(data.sendTransactionResponse.errorResultXdr!),
};
}

/**
Expand Down Expand Up @@ -203,6 +217,8 @@ export async function signTx(
* Send a transaction to the Soroban network.
*
* Wait `secondsToWait` seconds for the transaction to complete (default: 10).
* If you pass `0`, it will automatically return the `sendTransaction` results,
* rather than using `getTransaction`.
*
* If you need to construct or sign a transaction yourself rather than using
* `invoke` or one of the exported contract methods, you may want to use this
Expand All @@ -213,16 +229,17 @@ export async function sendTx(
tx: Tx,
secondsToWait: number,
server: SorobanClient.Server
): Promise<SendTx | GetTx> {
): Promise<{ sendTransactionResponse: SendTx, getTransactionResponse?: GetTx, getTransactionResponseAll?: GetTx[] }> {
const sendTransactionResponse = await server.sendTransaction(tx);

if (sendTransactionResponse.status !== "PENDING" || secondsToWait === 0) {
return sendTransactionResponse;
return { sendTransactionResponse };
}

let getTransactionResponse = await server.getTransaction(
const getTransactionResponseAll: GetTx[] = [];
getTransactionResponseAll.push(await server.getTransaction(
sendTransactionResponse.hash
);
));

const waitUntil = new Date(Date.now() + secondsToWait * 1000).valueOf();

Expand All @@ -231,19 +248,19 @@ export async function sendTx(

while (
Date.now() < waitUntil &&
getTransactionResponse.status === "NOT_FOUND"
getTransactionResponseAll[getTransactionResponseAll.length - 1].status === "NOT_FOUND"
) {
// Wait a beat
await new Promise((resolve) => setTimeout(resolve, waitTime));
/// Exponential backoff
waitTime = waitTime * exponentialFactor;
// See if the transaction is complete
getTransactionResponse = await server.getTransaction(
getTransactionResponseAll.push(await server.getTransaction(
sendTransactionResponse.hash
);
));
}

if (getTransactionResponse.status === "NOT_FOUND") {
if (getTransactionResponseAll[getTransactionResponseAll.length - 1].status === "NOT_FOUND") {
console.error(
`Waited ${secondsToWait} seconds for transaction to complete, but it did not. Returning anyway. Check the transaction status manually. Info: ${JSON.stringify(
sendTransactionResponse,
Expand All @@ -253,5 +270,9 @@ export async function sendTx(
);
}

return getTransactionResponse;
return {
sendTransactionResponse,
getTransactionResponseAll,
getTransactionResponse: getTransactionResponseAll[getTransactionResponseAll.length - 1]
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fd0443086c8813aa0ed2b95105c4c8985c3c056f0213f16e7effdcdf2b9d2b78
Loading