Repository: stellar/stellar-cli
Pull request #2031: fix(ts-bindings): valid TS when >1 contracterror
Author: login: chadoh
Created: 2025-05-16T17:15:11Z | Merged: 2025-05-22T21:01:47Z

We were generating a separate `const Errors` definition for every `#[contracterror]` in the original contract source. For example, with the new `custom_type` source code, the generated TS would be:

```ts
export const Errors = {
  /**
   * Please provide an odd number
   */
  1: {message:"NumberMustBeOdd"}
}
export const Errors = {
  /**
   * Some contract libraries contain extra #[contracterror] definitions that end up compiled
   * into the main contract types. We need to make sure tooling deals with this properly.
   */
  100: {message:"HowCouldYou"}
}
```

This now names the `const` after the name of the error enum in the original file, following the logic of other enum types. It will also rename `Error` to `Errors`, to avoid conflicts with JS `Error` type. So the updated `custom_type` source code now generates:

```ts
export const Errors = {
  /**
   * Please provide an odd number
   */
  1: {message:"NumberMustBeOdd"}
}
export const Erroneous = {
  /**
   * Some contract libraries contain extra #[contracterror] definitions that end up compiled
   * into the main contract types. We need to make sure tooling deals with this properly.
   */
  100: {message:"HowCouldYou"}
}
```

These constants are NOT ACTUALLY USED by the TS, but are continuing to be exported to avoid considering this a breaking change to the library generation logic.

This is needed in order to generate TS bindings for [OpenZeppelin's Fungible Token][1]. The contract itself defines [`contracterror` number 1][2], the `pausable` libraries defines [100-series `contracterror`s][3], and the `fungible` library creates [200-series `contracterror`s][4].

  [1]: https://github.com/OpenZeppelin/stellar-contracts/blob/main/examples/fungible-token-interface/src/contract.rs
  [2]: https://github.com/OpenZeppelin/stellar-contracts/blob/54eec16f8f8d8373936147464d6eb81eff3c58c2/examples/fungible-token-interface/src/contract.rs#L29C1-L34C2
  [3]: https://github.com/OpenZeppelin/stellar-contracts/blob/54eec16f8f8d8373936147464d6eb81eff3c58c2/packages/contract-utils/pausable/src/pausable.rs#L74-L82
  [4]: https://github.com/OpenZeppelin/stellar-contracts/blob/54eec16f8f8d8373936147464d6eb81eff3c58c2/packages/tokens/fungible/src/fungible.rs#L156-L182



Diff:
diff --git a/cmd/crates/soroban-spec-typescript/src/lib.rs b/cmd/crates/soroban-spec-typescript/src/lib.rs
index 1d8cb96b65..c86fe277a6 100644
--- a/cmd/crates/soroban-spec-typescript/src/lib.rs
+++ b/cmd/crates/soroban-spec-typescript/src/lib.rs
@@ -11,7 +11,7 @@ use itertools::Itertools;
 use sha2::{Digest, Sha256};
 use stellar_xdr::curr::{Limits, ScSpecEntry, WriteXdr};
 
-use types::Entry;
+use types::{Entry, ErrorEnumCase};
 
 use soroban_spec::read::{from_wasm, FromWasmError};
 
@@ -121,14 +121,7 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
     let mut constructor_args: Option<Vec<types::FunctionInput>> = None;
     // Filter out function entries with names that start with "__" and partition the results
     collected.iter().for_each(|entry| match entry {
@@ -143,7 +136,7 @@ pub fn generate(spec: &[ScSpecEntry]) -> String {
         .partition(|entry| matches!(entry, Entry::Function { .. }));
     let top = other.iter().map(entry_to_method_type).join("\n");
     let bottom = generate_class(&fns, constructor_args, spec);
-    format!("{top}\n\n{bottom}")
+    format!("{top}\n{bottom}")
 }
 
 fn doc_to_ts_doc(doc: &str, method: Option<&str>, indent_level: usize) -> String {
@@ -178,10 +171,6 @@ fn doc_to_ts_doc(doc: &str, method: Option<&str>, indent_level: usize) -> String
     )
 }
 
-fn is_error_enum(entry: &ScSpecEntry) -> bool {
-    matches!(entry, ScSpecEntry::UdtErrorEnumV0(_))
-}
-
 const METHOD_OPTIONS: &str = r"{
     /**
      * The fee to pay for the transaction. Default: BASE_FEE
@@ -266,7 +255,7 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
         Entry::TupleStruct { doc, name, fields } => {
             let docs = doc_to_ts_doc(doc, None, 0);
             let fields = fields.iter().map(type_to_ts).join(",  ");
-            format!("{docs}export type {name} = readonly [{fields}];")
+            format!("{docs}export type {name} = readonly [{fields}];\n")
         }
 
         Entry::Union { name, doc, cases } => {
@@ -293,35 +282,29 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
+        Entry::ErrorEnum { doc, cases, name } => {
             let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+            let cases = cases.iter().map(error_case_to_ts).join(",\n");
+            let name = if name == "Error" {
+                format!("{name}s")
+            } else {
+                name.to_string()
+            };
+            format!(
+                r"{doc}export const {name} = {{
+  {cases}
+}}
+",
+            )
         }
     }
 }
 
+fn error_case_to_ts(ErrorEnumCase { doc, value, name }: &types::ErrorEnumCase) -> String {
+    let doc = doc_to_ts_doc(doc, None, 1);
+    format!("{doc}  {value}: {{message:\"{name}\"}}")
+}
+
 fn enum_case_to_ts(case: &types::EnumCase) -> String {
     let types::EnumCase { name, value, .. } = case;
     format!("{name} = {value},")
diff --git a/cmd/crates/soroban-test/tests/fixtures/test-wasms/custom_type/src/lib.rs b/cmd/crates/soroban-test/tests/fixtures/test-wasms/custom_type/src/lib.rs
index 9e4b442b53..f891d6de1c 100644
--- a/cmd/crates/soroban-test/tests/fixtures/test-wasms/custom_type/src/lib.rs
+++ b/cmd/crates/soroban-test/tests/fixtures/test-wasms/custom_type/src/lib.rs
@@ -52,6 +52,16 @@ pub enum Error {
     /// Please provide an odd number
     NumberMustBeOdd = 1,
 }
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+#[repr(u32)]
+pub enum Erroneous {
+    /// Some contract libraries contain extra #[contracterror] definitions that end up compiled
+    /// into the main contract types. We need to make sure tooling deals with this properly.
+    HowCouldYou = 100,
+}
+
 #[contractimpl]
 impl Contract {
     pub fn hello(_env: Env, hello: Symbol) -> Symbol {


Reviews:
user: login: willemneal
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T17:43:57Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:16:11Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:20:22Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:24:34Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:26:02Z
user: login: willemneal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:26:46Z
user: login: willemneal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:27:37Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:28:30Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:28:54Z
user: login: willemneal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:28:57Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:31:00Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:31:46Z
user: login: willemneal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:43:10Z
user: login: willemneal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:43:25Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T19:23:13Z
user: login: leighmcculloch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T21:28:37Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:39:24Z
user: login: leighmcculloch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T22:28:31Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T20:05:06Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T20:05:40Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T20:07:18Z
user: login: leighmcculloch
body: Question, but regardless lgtm.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T21:01:05Z
user: login: chadoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T21:59:03Z
user: login: leighmcculloch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:25:57Z

Review comments:
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: Generally this is a bad way to concatenate strings. Basically you it's reallocating and copying each time. The better way is with a join after collecting all the strings. I doubt it will make much of an issue here given how few errors there are, but a good thing to know in general.
created_at: 2025-05-16T17:34:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: 
A raw string is more readable IMHO
```suggestion
    let errors = format!(
        r#"{error_docs}export const Errors = {{
{}
}}"#,
        error_cases.join(",\n")
    );
```
created_at: 2025-05-16T17:35:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
+            String::new()
         }
     }
 }
 
+fn error_cases_to_ts(cases: &[ErrorEnumCase]) -> Vec<String> {
+    cases
+        .iter()
+        .map(|c| {
+            format!(
+                "{}  {}: {{message:\"{}\"}}",
+                doc_to_ts_doc(&c.doc, None, 1),
+                c.value,
+                c.name
+            )
+        })
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: ```suggestion
        .map(|ErrorEnumCase { value, name, doc, .. } | {
            format!(
                "{value}  {name}: {{message:\"{}\"}}",
                doc_to_ts_doc(&doc, None, 1)
            )
        })
```
created_at: 2025-05-16T17:40:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
+            String::new()
         }
     }
 }
 
+fn error_cases_to_ts(cases: &[ErrorEnumCase]) -> Vec<String> {
+    cases
+        .iter()
+        .map(|c| {
+            format!(
+                "{}  {}: {{message:\"{}\"}}",
+                doc_to_ts_doc(&c.doc, None, 1),
+                c.value,
+                c.name
+            )
+        })
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: Gotta love pattern matching on paramaters.
created_at: 2025-05-16T17:40:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: This lines confuses me.  I don't see where it is filtered.
created_at: 2025-05-16T17:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Disagree, don't like the multiline! 
created_at: 2025-05-16T18:16:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Is this what you're picturing?

```rs
.fold(
    (vec![], vec![]),
    |(acc_docs, acc_cases), (docs, cases)| {
        (
            [acc_docs, [docs]].concat(),
            [acc_cases, cases].concat(),
        )
    }
);
let error_docs = doc_to_ts_doc(&error_docs.join(""), None, 0);
```
created_at: 2025-05-16T18:20:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
+            String::new()
         }
     }
 }
 
+fn error_cases_to_ts(cases: &[ErrorEnumCase]) -> Vec<String> {
+    cases
+        .iter()
+        .map(|c| {
+            format!(
+                "{}  {}: {{message:\"{}\"}}",
+                doc_to_ts_doc(&c.doc, None, 1),
+                c.value,
+                c.name
+            )
+        })
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Thanks! I moved this from another location but yeah, the pattern-matched version is 10x better.
created_at: 2025-05-16T18:24:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: That's the whole `filter_map` thing starting on [line 125](https://github.com/stellar/stellar-cli/pull/2031/files#diff-81cd9dbf2c88f86e0c16d781e82b16a951ea8148d057e530aef67105a7df3ab3R125)
created_at: 2025-05-16T18:26:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: Well then at least make the error cases a variable and inline it `{error_cases}`
created_at: 2025-05-16T18:26:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: I try to always do it. I hate seeing `\n` in a string literal if it's a template.
created_at: 2025-05-16T18:27:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Updated the code; lmk what you think
created_at: 2025-05-16T18:28:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
+            String::new()
         }
     }
 }
 
+fn error_cases_to_ts(cases: &[ErrorEnumCase]) -> Vec<String> {
+    cases
+        .iter()
+        .map(|c| {
+            format!(
+                "{}  {}: {{message:\"{}\"}}",
+                doc_to_ts_doc(&c.doc, None, 1),
+                c.value,
+                c.name
+            )
+        })
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: See the newest change for the correct version of this (you put some params in the wrong spots)
created_at: 2025-05-16T18:28:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: Maybe, it's hard for me mentally understand what the first element in the tuple is. If you need you might need to `.flatten` first.
created_at: 2025-05-16T18:28:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Sure, we'll still have some `\n` in there. It's a nice one-liner, though. The raw string is significantly uglier imho.
created_at: 2025-05-16T18:30:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,14 +121,28 @@ export class Client extends ContractClient {{
 }
 
 pub fn generate(spec: &[ScSpecEntry]) -> String {
-    let mut collected: Vec<_> = spec.iter().map(Entry::from).collect();
-    if !spec.iter().any(is_error_enum) {
-        collected.push(Entry::ErrorEnum {
-            doc: String::new(),
-            name: "Error".to_string(),
-            cases: vec![],
-        });
-    }
+    let collected: Vec<_> = spec.iter().map(Entry::from).collect();
+    let (error_docs, error_cases) = collected
+        .clone()
+        .into_iter()
+        .filter_map(|entry| {
+            if let Entry::ErrorEnum { doc, cases, .. } = entry {
+                Some((doc, error_cases_to_ts(&cases)))
+            } else {
+                None
+            }
+        })
+        .fold(
+            (String::new(), vec![]),
+            |(acc_docs, acc_cases), (docs, cases)| {
+                (format!("{acc_docs}{docs}"), [acc_cases, cases].concat())
+            },
+        );
+    let error_docs = doc_to_ts_doc(&error_docs, None, 0);
+    let errors = format!(
+        "{error_docs}export const Errors = {{\n{}\n}}",
+        error_cases.join(",\n")
+    );
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: New version:

```rs
let error_docs = doc_to_ts_doc(&error_docs.join(""), None, 0);
let error_cases = error_cases.join(",\n");
let errors = format!("{error_docs}export const Errors = {{\n{error_cases}\n}}");
```
created_at: 2025-05-16T18:31:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: I see that, but that is on a clone of the original `collected`. And later it seems that the original is used.

```rust
let (fns, other): (Vec<_>, Vec<_>) = collected
        .into_iter()
        .filter(|entry| !matches!(entry, Entry::Function { name, .. } if name.starts_with("__")))
        .partition(|entry| matches!(entry, Entry::Function { .. }))
```rust
created_at: 2025-05-16T18:43:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: willemneal
body: Why I hate clone!
created_at: 2025-05-16T18:43:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +303,27 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // type filtered out before entry_to_method_type is called
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: The referenced code has been removed and the comment has been updated. `ErrorEnum` logic here is still a no-op.
created_at: 2025-05-16T19:23:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,31 +282,10 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // Generated file does not need to error types; they are parsed at runtime:
+            // https://github.com/stellar/js-stellar-sdk/blob/f1f81f7870affb444c1d2a289c1a272f01410346/src/contract/client.ts#L34-L40
+            String::new()
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: leighmcculloch
body: Hmm, this looks like it's a breaking change because a type that was generated, that theoretically someone might be using has disappeared from the generated code. I assume we want this to target a minor or patch release.

Instead of removing it entirely, we could merge the errors enums together so that the issue of multiple `Errors` being created is resolved by creating one error.

The bug also seems to be that a `Errors` is generated at all with that name. The name should be coming from the spec so in the test wasm where `Errorneous` was added would result in `Errors` and `Errorneous` being generated instead of two `Errors`. Changing to using names would also be a breaking change.

A non-breaking way to fix this might be to simply emit only the first error enum. And then next major version follow up with emitting types using their names.
created_at: 2025-05-19T21:28:37Z
author_association: MEMBER
diff_hunk: @@ -293,31 +282,10 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // Generated file does not need to error types; they are parsed at runtime:
+            // https://github.com/stellar/js-stellar-sdk/blob/f1f81f7870affb444c1d2a289c1a272f01410346/src/contract/client.ts#L34-L40
+            String::new()
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: > we could merge the errors enums together so that the issue of multiple Errors being created is resolved by creating one error.

This was the original approach here, and is preserved in the first commit.

> A non-breaking way to fix this...

We could also go back to my first commit here? 

The current behavior is obviously a bug and results in broken code for any contracts with more than one `contracterror` enum. Whether we fix it by emitting one combined `const Errors` or emitting separate named constants based on the original Rust enum names, I think it would be ok? We _could_ make the first one have the name `Errors` and then 2nd to Nth named after their Rust enum type... 

If you want to keep the behavior of `entry_to_method_type` unchanged, we could do that, since `generate` is no longer calling it for `ErrorEnum` types. I would want to add a comment to make it clear that there's no actual reason for it to do so.
created_at: 2025-05-20T15:39:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,31 +282,10 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // Generated file does not need to error types; they are parsed at runtime:
+            // https://github.com/stellar/js-stellar-sdk/blob/f1f81f7870affb444c1d2a289c1a272f01410346/src/contract/client.ts#L34-L40
+            String::new()
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: leighmcculloch
body: > emitting separate named constants based on the original Rust enum names, I think it would be ok?

I think this would be okay too.
created_at: 2025-05-20T22:28:31Z
author_association: MEMBER
diff_hunk: @@ -293,35 +282,29 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
+        Entry::ErrorEnum { doc, cases, name } => {
             let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+            let cases = cases.iter().map(error_case_to_ts).join(",\n");
+            let name = if name == "Error" {
+                format!("{name}s")
+            } else {
+                name.to_string()
+            };
+            format!(
+                r"{doc}export const {name} = {{
+  {cases}
+}}
+",
+            )
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: This is almost identical to the `Entry::Enum` case directly above, starting on line 270
created_at: 2025-05-22T20:05:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,7 +255,7 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
         Entry::TupleStruct { doc, name, fields } => {
             let docs = doc_to_ts_doc(doc, None, 0);
             let fields = fields.iter().map(type_to_ts).join(",  ");
-            format!("{docs}export type {name} = readonly [{fields}];")
+            format!("{docs}export type {name} = readonly [{fields}];\n")
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: This fixes an inconsequential formatting error in the generated TS, since I noticed it while I was here and it was easy to fix
created_at: 2025-05-22T20:05:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,31 +282,10 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
-            let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+        Entry::ErrorEnum { .. } => {
+            // Generated file does not need to error types; they are parsed at runtime:
+            // https://github.com/stellar/js-stellar-sdk/blob/f1f81f7870affb444c1d2a289c1a272f01410346/src/contract/client.ts#L34-L40
+            String::new()
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: Ok, I updated it as suggested. If there is no `contracterror` enum named `Error` or `Errors`, then there will no longer be an emitted const named `Errors`. I still think we can consider this a bug fix rather than a breaking change.
created_at: 2025-05-22T20:07:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +282,29 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
+        Entry::ErrorEnum { doc, cases, name } => {
             let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+            let cases = cases.iter().map(error_case_to_ts).join(",\n");
+            let name = if name == "Error" {
+                format!("{name}s")
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: leighmcculloch
body: Do you know why the s is added? I see you're just copying the behaviour from above.
created_at: 2025-05-22T21:00:22Z
author_association: MEMBER
diff_hunk: @@ -293,35 +282,29 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
+        Entry::ErrorEnum { doc, cases, name } => {
             let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+            let cases = cases.iter().map(error_case_to_ts).join(",\n");
+            let name = if name == "Error" {
+                format!("{name}s")
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: chadoh
body: I suspect the original guess was that `Error` is a reserved word in JS. It’s not; we can name the export `Error` and compile with no problem. It does make for unexpected name collisions though, with the built in `Error`. 

I figured matching the logic above would make for a more consistent file as well as less of a change from the previous behavior.
created_at: 2025-05-22T21:59:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,35 +282,29 @@ pub fn entry_to_method_type(entry: &Entry) -> String {
 ",
             )
         }
-        Entry::ErrorEnum { doc, cases, .. } => {
+        Entry::ErrorEnum { doc, cases, name } => {
             let doc = doc_to_ts_doc(doc, None, 0);
-            let cases = cases
-                .iter()
-                .enumerate()
-                .map(|(i, c)| {
-                    if c.doc.is_empty() {
-                        format!(
-                            "{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            c.value,
-                            c.name
-                        )
-                    } else {
-                        format!(
-                            "{}{}  {}: {{message:\"{}\"}}",
-                            if i == 0 { "" } else { "\n" },
-                            doc_to_ts_doc(&c.doc, None, 1),
-                            c.value,
-                            c.name
-                        )
-                    }
-                })
-                .join(",\n");
-            format!("{doc}export const Errors = {{\n{cases}\n}}")
+            let cases = cases.iter().map(error_case_to_ts).join(",\n");
+            let name = if name == "Error" {
+                format!("{name}s")
path: cmd/crates/soroban-spec-typescript/src/lib.rs
user: login: leighmcculloch
body: 👍
created_at: 2025-05-22T23:25:57Z
author_association: MEMBER

PR comments:
user: login: willemneal
created_at: 2025-05-16T18:30:00Z
body: I first reviewed blind to the context. Does this mean that when you have multiple error defs they are assigned a value as they are seen. e.g. first error enum with four [0-3], does this second start at value 4?
author_association: CONTRIBUTOR
user: login: chadoh
created_at: 2025-05-16T18:37:14Z
body: @willemneal no, it doesn't reassign error numbers. I just checked; if you have two separate `#[contracterror]` definitions with re-used error numbers, you get the following invalid `const Errors` in the TS:

```ts
export const Errors = {
  100: {message:"Unauthorized"},
  /**
   * The operation failed because the contract is paused.
   */
  100: {message:"EnforcedPause"},
  /**
   * The operation failed because the contract is not paused.
   */
  101: {message:"ExpectedPause"}
}
```

Great catch. Not sure what to about this.
author_association: CONTRIBUTOR
user: login: chadoh
created_at: 2025-05-16T19:21:08Z
body: @willemneal ok, I dug into it, and I believe these `Errors` constants are **never used** in the current code. The const is never referenced in the generated file. Our `parseError` logic in `AssembledTransaction` in the JS SDK relies on an `errorTypes` map that gets passed in, and this `errorTypes` matches the format of `Errors`. But we don't need to pass in `Errors` (anymore?) because the error types are parsed from the contract spec [when you instantiate](https://github.com/AhaLabs/js-stellar-sdk/blob/f1f81f7870affb444c1d2a289c1a272f01410346/src/contract/client.ts#L34-L40) a `contract.Client`. Relevant lines:

```ts
errorTypes: spec.errorCases().reduce(
  (acc, curr) => ({
    ...acc,
    [curr.value()]: { message: curr.doc().toString() },
  }),
  {} as Pick<ClientOptions, "errorTypes">,
),
```

I've added a second commit to remove all the logic to generate `errors`. 
author_association: CONTRIBUTOR