Skip to content

Commit

Permalink
feat(bindings-ts): support multi-auth workflows
Browse files Browse the repository at this point in the history
- add `atomic_swap` and `token` contracts from
  https://github.com/stellar/soroban-examples to `test-wasms`
- greatly complicate `ts-tests/package.json` to deploy & generate
  bindings for these new contracts, plus creating an `alice` identity
  and minting separate amounts of two separate tokens to the `root` user
  and `alice`
- add tests for atomic swap functionality inspired by
  https://github.com/stellar/soroban-react-atomic-swap
- let this logic guide needed updates to `bindings typescript`-generated
  libraries:
  - don't return flat values
  - instead, always return an object which will have at least a
    `txUnsigned` and `simulation` key
  - object contains all possibly-relevant fields from the logic it
    performs

Co-authored-by: Aristides Staffieri <aristides.staffieri@stellar.org>
  • Loading branch information
chadoh and aristidesstaffieri committed Oct 10, 2023
1 parent 9ec6baf commit 7919dbf
Show file tree
Hide file tree
Showing 27 changed files with 3,420 additions and 2,527 deletions.
298 changes: 155 additions & 143 deletions Cargo.lock

Large diffs are not rendered by default.

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 = "20.0.0-rc2"
git = "https://github.com/stellar/rs-soroban-sdk"
rev = "0992413f9b05e5bfb1f872bce99e89d9129b2e61"

[workspace.dependencies.soroban-token-sdk]
version = "20.0.0-rc2"
git = "https://github.com/stellar/rs-soroban-sdk"
rev = "0992413f9b05e5bfb1f872bce99e89d9129b2e61"

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

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

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

type Simulation = SorobanRpc.SimulateTransactionResponse;
type SendTx = SorobanRpc.SendTransactionResponse;
type GetTx = 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 @@ -62,13 +59,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 @@ -81,7 +78,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"));
let parse = parseResultXdr;
const server = new SorobanClient.Server(rpcUrl, {
Expand All @@ -99,33 +96,35 @@ 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 (SorobanRpc.isSimulationError(simulated)) {
throw new Error(simulated.error);
if (SorobanRpc.isSimulationError(simulation)) {
throw new Error(simulation.error);
} else if (responseType === "simulated") {
return simulated;
} else if (!simulated.result) {
throw new Error(`invalid simulation: no result in ${simulated}`);
return { txUnsigned: txUnsigned.toXDR(), simulation };
} else if (!simulation.result) {
throw new Error(`invalid simulation: no result in ${simulation}`);
}

let authsCount = simulated.result.auth.length;
const writeLength = simulated.transactionData.getReadWrite().length;
let authsCount = simulation.result.auth.length;
const writeLength = simulation.transactionData.getReadWrite().length;
const isViewCall = (authsCount === 0) && (writeLength === 0);

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

return parseResultXdr(simulated.result.retval);
return {
txUnsigned: txUnsigned.toXDR(),
simulation,
result: parseResultXdr(simulation.result.retval),
};
}

if (authsCount > 1) {
Expand All @@ -146,27 +145,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);
if (responseType === "full") {
return raw;
}
const data = {
simulation,
txUnsigned: txUnsigned.toXDR(),
txSigned: txSigned.toXDR(),
...await sendTx(txSigned, secondsToWait, server)
};

// 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 (responseType === "full") return data;

// otherwise, it returned the result of `sendTransaction`
if ("errorResultXdr" in raw) return parse(raw.errorResultXdr!);
// 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;
}

// 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 @@ -196,6 +211,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 @@ -206,16 +223,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 @@ -224,19 +242,19 @@ export async function sendTx(

while (
Date.now() < waitUntil &&
getTransactionResponse.status === SorobanRpc.GetTransactionStatus.NOT_FOUND
getTransactionResponseAll[getTransactionResponseAll.length - 1].status === SorobanRpc.GetTransactionStatus.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 === SorobanRpc.GetTransactionStatus.NOT_FOUND) {
if (getTransactionResponseAll[getTransactionResponseAll.length - 1].status === SorobanRpc.GetTransactionStatus.NOT_FOUND) {
console.error(
`Waited ${
secondsToWait
Expand All @@ -250,5 +268,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 @@
07ec9b8333159ac477239ff1a54c6fc45c5817e03cf0f45b6c9e51727c0e3dc7
Loading

0 comments on commit 7919dbf

Please sign in to comment.