Skip to content

Commit

Permalink
feat: ContractV2 class gives AssembledTransactions
Browse files Browse the repository at this point in the history
This new AssembledTransaction class provides a way to split up the
complex logic of simulating, signing, and sending Soroban transactions,
while still being low-fuss for view calls.
  • Loading branch information
chadoh committed Oct 31, 2023
1 parent f2453ca commit ad3d6b6
Show file tree
Hide file tree
Showing 6 changed files with 494 additions and 4 deletions.
167 changes: 167 additions & 0 deletions cmd/crates/soroban-spec-typescript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn generate_from_wasm(wasm: &[u8]) -> Result<String, FromWasmError> {

fn generate_class(fns: &[Entry], spec: &[ScSpecEntry]) -> String {
let methods = fns.iter().map(entry_to_ts).join("\n\n ");
let methods_v2 = fns.iter().map(entry_to_ts_v2).join("\n\n ");
let spec = spec
.iter()
.map(|s| format!("\"{}\"", s.to_xdr_base64().unwrap()))
Expand All @@ -78,6 +79,16 @@ fn generate_class(fns: &[Entry], spec: &[ScSpecEntry]) -> String {
return submitTx({{ txXdr, signHere, allSigned, secondsToWait, ...this.options }});
}}
{methods}
}}
export class ContractV2 {{
spec: ContractSpec;
constructor(public readonly options: ClassOptions) {{
this.spec = new ContractSpec([
{spec}
]);
}}
{methods_v2}
}}"#,
)
}
Expand Down Expand Up @@ -140,6 +151,17 @@ fn method_options(return_type: &String) -> String {
)
}

const METHOD_OPTIONS: &str = r#"{
/**
* The fee to pay for the transaction. Default: 100.
*/
fee?: number,
/**
* If the simulation shows that this invocation requires auth/signing, `invoke` will wait `secondsToWait` seconds for the transaction to complete before giving up and returning the incomplete {{@link SorobanClient.SorobanRpc.GetTransactionResponse}} results (or attempting to parse their probably-missing XDR with `parseResultXdr`, depending on `responseType`). Set this to `0` to skip waiting altogether, which will return you {{@link SorobanClient.SorobanRpc.SendTransactionResponse}} more quickly, before the transaction has time to be included in the ledger. Default: 10.
*/
secondsToWait?: number,
}"#;

fn jsify_name(name: &String) -> String {
name.to_lower_camel_case()
}
Expand Down Expand Up @@ -289,6 +311,151 @@ pub fn entry_to_ts(entry: &Entry) -> String {
}
}

#[allow(clippy::too_many_lines)]
pub fn entry_to_ts_v2(entry: &Entry) -> String {
match entry {
Entry::Function {
doc,
name,
inputs,
outputs,
} => {
let input_vals = inputs.iter().map(func_input_to_arg_name).join(", ");
let input = (!inputs.is_empty())
.then(|| {
format!(
"{{{input_vals}}}: {{{}}}, ",
inputs.iter().map(func_input_to_ts).join(", ")
)
})
.unwrap_or_default();
let mut is_result = false;
let mut return_type: String;
if outputs.is_empty() {
return_type = "void".to_owned();
} else if outputs.len() == 1 {
return_type = type_to_ts(&outputs[0]);
is_result = return_type.starts_with("Result<");
} else {
return_type = format!("readonly [{}]", outputs.iter().map(type_to_ts).join(", "));
};
let ts_doc = doc_to_ts_doc(doc);

if is_result {
return_type = return_type
.strip_prefix("Result<")
.unwrap()
.strip_suffix('>')
.unwrap()
.to_owned();
return_type = format!("Ok<{return_type}> | Err<Error_> | undefined");
}

let mut output = outputs
.get(0)
.map(|_| format!("this.spec.funcResToNative(\"{name}\", xdr)"))
.unwrap_or_default();
if is_result {
output = format!("new Ok({output})");
}
if return_type != "void" {
output = format!(r#"return {output};"#);
};
let parse_result_xdr = if return_type == "void" {
r#"parseResultXdr: () => {}"#.to_owned()
} else {
format!(
r#"parseResultXdr: (xdr): {return_type} => {{
{output}
}}"#
)
};
let js_name = jsify_name(name);
let options = METHOD_OPTIONS;
let parsed_scvals = inputs.iter().map(parse_arg_to_scval).join(", ");
let args =
format!("args: this.spec.funcArgsToScVals(\"{name}\", {{{parsed_scvals}}}),");
let mut body = format!(
r#"return await AssembledTransaction.fromSimulation({{
method: '{name}',
{args}
...options,
...this.options,
{parse_result_xdr},
}});"#
);
if is_result {
body = format!(
r#"try {{
{body}
}} catch (e) {{
let err = parseError(e.toString());
if (err) return err;
throw e;
}}"#
);
}
format!(
r#"{ts_doc}{js_name} = async ({input}options: {options} = {{}}) => {{
{body}
}}
"#
)
}
Entry::Struct { doc, name, fields } => {
let docs = doc_to_ts_doc(doc);
let fields = fields.iter().map(field_to_ts).join("\n ");
format!(
r#"{docs}export interface {name} {{
{fields}
}}
"#
)
}

Entry::TupleStruct { doc, name, fields } => {
let docs = doc_to_ts_doc(doc);
let fields = fields.iter().map(type_to_ts).join(", ");
format!("{docs}export type {name} = readonly [{fields}];")
}

Entry::Union { name, doc, cases } => {
let doc = doc_to_ts_doc(doc);
let cases = cases.iter().map(case_to_ts).join(" | ");

format!(
r#"{doc}export type {name} = {cases};
"#
)
}
Entry::Enum { doc, name, cases } => {
let doc = doc_to_ts_doc(doc);
let cases = cases.iter().map(enum_case_to_ts).join("\n ");
let name = (name == "Error")
.then(|| format!("{name}s"))
.unwrap_or(name.to_string());
format!(
r#"{doc}export enum {name} {{
{cases}
}}
"#,
)
}
Entry::ErrorEnum { doc, cases, .. } => {
let doc = doc_to_ts_doc(doc);
let cases = cases
.iter()
.map(|c| format!("{}: {{message:\"{}\"}}", c.value, c.doc))
.join(",\n ");
format!(
r#"{doc}const Errors = {{
{cases}
}}"#
)
}
}
}

fn enum_case_to_ts(case: &types::EnumCase) -> String {
let types::EnumCase { name, value, .. } = case;
format!("{name} = {value},")
Expand Down
Loading

0 comments on commit ad3d6b6

Please sign in to comment.