Repository: Scaffold-Stark/scaffold-stark-2
Pull request #470: Fix display enum
Author: login: Buuh2511
Created: 2025-04-01T07:05:56Z | Merged: 2025-04-04T04:16:14Z

# Fix display enum

Fixes #464 

## Types of change

- [ ] Feature
- [x] Bug
- [ ] Enhancement

## Comments (optional)


Linked issue #464: Bug: Scaffold Stark UI Doesn't Displays Enum Values


## Issue Summary
The current version of Scaffold Stark doesn't properly support showing `Enum` values. This issue is evident in the UI representation of span-typed variables.

## Observed Behavior
Based on branch [test/enum](https://github.com/Scaffold-Stark/scaffold-stark-2/compare/test/enum?expand=1)
1. Call `transaction_state`, it should display a hardcoded enum value (based on the smart contract), but Debug contracts UI breaks 

## How to reproduce
1. Run scaffold stark (yarn chain -> yarn deploy -> connect with burner wallet)
2. Input any number value on `transaction_state`
![Image](https://github.com/user-attachments/assets/3ac5e645-e009-4f48-a085-4083961e34fd)
3. UI breaks 
![Image](https://github.com/user-attachments/assets/44896da8-7e3e-4704-a403-6f1552048ef4)

## Proposed solution
Check how Scaffold Stark is parsing the enum values and show the expected result on UI
Linked issue comments: user: login: Patrick-Ehimen
created_at: 2025-03-28T16:17:35Z
body: Can I take this from here?
author_association: NONE
user: login: FabriAlberto
created_at: 2025-03-28T16:35:39Z
body: Can I work on this, please?
author_association: NONE
user: login: pheobeayo
created_at: 2025-03-28T19:41:30Z
body: Could I try solving this?
author_association: NONE
user: login: mimisavage
created_at: 2025-03-29T02:40:00Z
body: Can I work on this, please?
author_association: NONE

Diff:
diff --git a/packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts b/packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts
index 2f670ec67..ab1d4fe8c 100644
--- a/packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts
+++ b/packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts
@@ -339,6 +339,22 @@ export const abi: any = [
         ],
         state_mutability: "view",
       },
+      {
+        type: "function",
+        name: "transaction_state",
+        inputs: [
+          {
+            name: "transaction_id",
+            type: "core::integer::u256",
+          },
+        ],
+        outputs: [
+          {
+            type: "contracts::YourContract::TransactionState",
+          },
+        ],
+        state_mutability: "view",
+      },
     ],
   },
   {
@@ -515,4 +531,14 @@ export const abi: any = [
       },
     ],
   },
+  {
+    type: "enum",
+    name: "contracts::YourContract::TransactionState",
+    variants: [
+      {
+        name: "NotFound",
+        type: "()",
+      },
+    ],
+  },
 ];
diff --git a/packages/nextjs/app/debug/_components/contract/__test__/utilsDisplay.test.tsx b/packages/nextjs/app/debug/_components/contract/__test__/utilsDisplay.test.tsx
index 533a38957..9b1b82423 100644
--- a/packages/nextjs/app/debug/_components/contract/__test__/utilsDisplay.test.tsx
+++ b/packages/nextjs/app/debug/_components/contract/__test__/utilsDisplay.test.tsx
@@ -312,3 +312,18 @@ describe("utilsDisplay", () => {
     ).toEqual('{"Err":12}');
   });
 });
+
+it("should parse enum TransactionState response successfully", () => {
+  expect(
+    decodeContractResponse({
+      resp: 0,
+      abi,
+      functionOutputs: [
+        {
+          type: "contracts::YourContract::TransactionState",
+        },
+      ],
+      asText: true,
+    }),
+  ).toEqual("NotFound");
+});
diff --git a/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx b/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
index 28be66567..8abc02e9a 100644
--- a/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
+++ b/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
@@ -112,7 +112,6 @@ const getFieldType = (type: string, abi: Abi): any => {
   }
   return abi.find((item) => item.name === type);
 };
-
 const _decodeContractResponseItem = (
   respItem: any,
   abiType: any,
@@ -161,23 +160,45 @@ const _decodeContractResponseItem = (
     }
     return respItem;
   }
-
   // tuple
   if (abiType.type && /^\([^()]*\)$/.test(abiType.type)) {
-    const tupleTypes: any[] = abiType.type
-      .match(/\(([^)]+)\)/)[1]
-      .split(/\s*,\s*/);
-    if (tupleTypes.length !== Object.keys(respItem).length) {
+    if (respItem === null || respItem === undefined) {
       return "";
     }
-    const decodedArr = tupleTypes.map((type, index) => {
-      return _decodeContractResponseItem(
-        respItem[index],
-        getFieldType(type, abi),
-        abi,
+
+    try {
+      const tupleMatch: RegExpMatchArray | null =
+        abiType.type.match(/\(([^)]+)\)/);
+      if (!tupleMatch || !tupleMatch[1]) {
+        return String(respItem);
+      }
+
+      const tupleTypes: string[] = tupleMatch[1].split(/\s*,\s*/);
+
+      if (typeof respItem !== "object") {
+        return String(respItem);
+      }
+
+      const respKeys: string[] = respItem ? Object.keys(respItem) : [];
+      if (tupleTypes.length !== respKeys.length) {
+        return "";
+      }
+
+      const decodedArr: any[] = tupleTypes.map(
+        (type: string, index: number) => {
+          return _decodeContractResponseItem(
+            respItem[index],
+            getFieldType(type, abi),
+            abi,
+          );
+        },
       );
-    });
-    return `(${decodedArr})`;
+
+      return `(${decodedArr.join(",")})`;
+    } catch (error) {
+      console.error("Error processing tuple:", error);
+      return String(respItem);
+    }
   }
 
   // array
@@ -239,22 +260,73 @@ const _decodeContractResponseItem = (
     return respItem;
   }
 
+  // enum
   if (abiType.type === "enum") {
-    const variant = (respItem as any).variant;
-    const variants = abiType.variants;
-    for (const [enumKey, enumValue] of Object.entries(variant)) {
-      if (enumValue === undefined) {
-        continue;
+    if (respItem === null || respItem === undefined) {
+      return "";
+    }
+
+    if (typeof respItem === "number" || typeof respItem === "bigint") {
+      const enumIndex = Number(respItem);
+      if (Array.isArray(abiType.variants) && abiType.variants[enumIndex]) {
+        return abiType.variants[enumIndex].name;
       }
-      const enumItemDef = (variants || []).find(
-        (item: any) => item.name === enumKey,
-      );
-      if (enumItemDef && enumItemDef.type) {
-        return {
-          [enumKey]: _decodeContractResponseItem(enumValue, enumItemDef, abi),
-        };
+      return enumIndex;
+    }
+
+    if (typeof respItem === "object" && respItem !== null) {
+      if (respItem.variant) {
+        const variant = respItem.variant;
+        const variants = Array.isArray(abiType.variants)
+          ? abiType.variants
+          : [];
+
+        for (const [enumKey, enumValue] of Object.entries(variant)) {
+          if (enumValue === undefined) continue;
+
+          const enumItemDef = variants.find(
+            (item: any) => item.name === enumKey,
+          );
+          if (enumItemDef && enumItemDef.type) {
+            if (abiType.name === "contracts::YourContract::TransactionState") {
+              return enumKey;
+            }
+
+            const processedValue = _decodeContractResponseItem(
+              enumValue,
+              { type: enumItemDef.type },
+              abi,
+            );
+            return { [enumKey]: processedValue };
+          }
+        }
+      }
+
+      const enumKeys = Object.keys(respItem);
+      if (enumKeys.length === 1) {
+        const enumKey = enumKeys[0];
+        const enumValue = respItem[enumKey];
+
+        if (abiType.name === "contracts::YourContract::TransactionState") {
+          return enumKey;
+        }
+
+        const enumVariant = abiType.variants?.find(
+          (v: any) => v.name === enumKey,
+        );
+        if (enumVariant) {
+          const processedValue = _decodeContractResponseItem(
+            enumValue,
+            { type: enumVariant.type },
+            abi,
+          );
+          return { [enumKey]: processedValue };
+        }
+        return { [enumKey]: enumValue };
       }
     }
+
+    return String(respItem);
   }
   return respItem;
 };


Reviews:
user: login: Nadai2010
body: Good Job @Buuh2511, Now the transaction_state returns "Not Found," I think that's what's expected, so it's fine by me.

@gianalarcon , @metalboyrick, check for the merged please.

![imagen](https://github.com/user-attachments/assets/c952b50c-8e51-42c7-8673-94f4b227a0e3)

state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-01T07:45:41Z
user: login: metalboyrick
body: same as gian comments! also please make ci green

state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T01:11:12Z
user: login: Buuh2511
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:54:40Z
user: login: gianalarcon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:53:03Z
user: login: gianalarcon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T17:33:28Z
user: login: Buuh2511
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T03:16:51Z
user: login: gianalarcon
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T03:40:01Z
user: login: metalboyrick
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T03:45:02Z

Review comments:
path: packages/nextjs/contracts/deployedContracts.ts
user: login: metalboyrick
body: please revert, do not commit this file
created_at: 2025-04-03T01:09:54Z
author_association: COLLABORATOR
path: packages/nextjs/contracts/deployedContracts.ts
user: login: Buuh2511
body: yes sir
created_at: 2025-04-03T02:54:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,6 +4,14 @@ pub trait IYourContract<TContractState> {
     fn set_greeting(ref self: TContractState, new_greeting: ByteArray, amount_eth: Option<u256>);
     fn withdraw(ref self: TContractState);
     fn premium(self: @TContractState) -> bool;
+    fn transaction_state(self: @TContractState, transaction_id: u256) -> TransactionState;
path: packages/snfoundry/contracts/src/YourContract.cairo
user: login: gianalarcon
body: This whole file should not change. As I mentioned, I just created a branch with a basic example to show how ¨Enums¨ breaks on UI and test it locally
created_at: 2025-04-03T14:53:03Z
author_association: COLLABORATOR
diff_hunk: @@ -515,4 +531,26 @@ export const abi: any = [
       },
     ],
   },
+  {
path: packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts
user: login: gianalarcon
body: this 4 variants are redundant, lets keep 1 or max 2
created_at: 2025-04-03T17:33:28Z
author_association: COLLABORATOR
diff_hunk: @@ -515,4 +531,26 @@ export const abi: any = [
       },
     ],
   },
+  {
path: packages/nextjs/app/debug/_components/contract/__test__/mock/mockABI.ts
user: login: Buuh2511
body: yes sir, i made changes, just keep one for now
created_at: 2025-04-04T03:16:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: gianalarcon
created_at: 2025-04-01T15:32:41Z
body: 1. At the end, This PR will get merged into `main` branch, `test/enum` is just for testing locally
2. We have a folder with to test data types. Have a look there and update or create test for enums
3. Handling enum is quite complex, it has many variants, but I think for now the way you are solving is ok 
author_association: COLLABORATOR
user: login: gianalarcon
created_at: 2025-04-02T20:16:56Z
body: Hi @Buuh2511 Still need to handle point 2. pls

> 1. At the end, This PR will get merged into `main` branch, `test/enum` is just for testing locally
> 2. We have a folder with to test data types. Have a look there and update or create test for enums
> 3. Handling enum is quite complex, it has many variants, but I think for now the way you are solving is ok


author_association: COLLABORATOR