Skip to content

Feat/streaming sdk#29

Closed
HushLuxe wants to merge 6 commits intoGoodDollar:mainfrom
HushLuxe:feat/streaming-sdk
Closed

Feat/streaming sdk#29
HushLuxe wants to merge 6 commits intoGoodDollar:mainfrom
HushLuxe:feat/streaming-sdk

Conversation

@HushLuxe
Copy link
Contributor

@HushLuxe HushLuxe commented Feb 14, 2026

Implemented the Superfluid Streaming SDK and its associated React Hooks, providing a complete library for managing G$ money streams and GDA distribution pools. It also includes an interactive test UI in the demo-identity-app for verification.

Key Features
Core SDK: Implementation of StreamingSDK and GdaSDK for stream CRUD operations and GDA pool management.
React Hooks: Wagmi-style hooks (useCreateStream, useStreamList, usePoolMemberships, etc.) for easy frontend integration.
Subgraph Integration: Optimized queries for historical balances, active flows, and SUP reserve holdings.
Test UI: A new "Streaming" tab in the demo app to interactively test all SDK features.
Documentation: Comprehensive guides for both core SDK usage and frontend testing.

Summary by Sourcery

Introduce a new streaming SDK and React hooks for managing Superfluid-based G$ money streams and GDA pools, and expose them via a demo app streaming test UI.

New Features:

  • Add @goodsdks/streaming-sdk package providing StreamingSDK, GdaSDK, subgraph client, flow rate utilities, and TypeScript types for Superfluid streams and GDA pools.
  • Add React streaming hooks in @goodsdks/react-hooks (stream CRUD, pool connectivity, SUP reserves, and stream/pool queries) built on the new streaming SDK.
  • Add a StreamingTestPage and navigation tab to the demo-identity-app for interactively testing streams, pools, and SUP reserve data.
  • Document streaming SDK usage and local testing via new README and TESTING guides, plus a streaming-specific README for the demo app.

Enhancements:

  • Wire the streaming SDK into existing packages and tooling, including workspace dependency wiring, tsup/vitest configs, and updated lint/tooling configs for the demo app and monorepo dev script.

Build:

  • Introduce a dedicated build and TypeScript configuration for the new streaming SDK package and update demo app linting to the flat ESLint/TypeScript stack.

Documentation:

  • Add comprehensive API and testing documentation for the streaming SDK, and a usage guide for the demo-identity-app streaming test UI.

Tests:

  • Add vitest unit tests for the streaming and GDA SDKs and utility modules to validate chain handling, flow rate logic, and subgraph interactions.

Chores:

  • Tune Turbo dev concurrency and align eslint/lint scripts across the demo app with the new configuration.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 9 security issues, 5 other issues, and left some high level feedback:

Security issues:

  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)
  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)

General comments:

  • The StreamingTestPage component has grown very large (~800 lines) and mixes networking logic, state management, and complex UI in a single file; consider extracting sections (e.g., create/update/delete stream panels, pool section, SUP reserves section) into smaller presentational/components to improve readability and future maintenance.
  • In the React hooks (useCreateStream, useUpdateStream, useDeleteStream, useConnectToPool, etc.) a new SDK instance is constructed for every mutation; if these hooks are used frequently, consider memoizing or centralizing SDK construction (e.g., via a context or a small factory) to avoid unnecessary re-instantiation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `StreamingTestPage` component has grown very large (~800 lines) and mixes networking logic, state management, and complex UI in a single file; consider extracting sections (e.g., create/update/delete stream panels, pool section, SUP reserves section) into smaller presentational/components to improve readability and future maintenance.
- In the React hooks (`useCreateStream`, `useUpdateStream`, `useDeleteStream`, `useConnectToPool`, etc.) a new SDK instance is constructed for every mutation; if these hooks are used frequently, consider memoizing or centralizing SDK construction (e.g., via a context or a small factory) to avoid unnecessary re-instantiation.

## Individual Comments

### Comment 1
<location> `packages/streaming-sdk/src/constants.ts:42-43` </location>
<code_context>
+    [SupportedChains.BASE]: "https://base-mainnet.subgraph.x.superfluid.dev/",
+    [SupportedChains.BASE_SEPOLIA]:
+        "https://base-sepolia.subgraph.x.superfluid.dev/",
+    supReserve:
+        "https://thegraph.com/explorer/subgraphs/6dRuPxMvaJAp32hvcTsYbAya69A4t1KUHh2EnV3YQeXU",
+}
+
</code_context>

<issue_to_address>
**issue (bug_risk):** SUP reserve subgraph URL points to the Graph Explorer page, not the API endpoint

Because this constant is used by `GraphQLClient` in `querySUPReserves`, requests will fail against the HTML Explorer page. Please update `SUBGRAPH_URLS.supReserve` to the underlying GraphQL API endpoint (e.g., `https://api.thegraph.com/subgraphs/...`) so `SubgraphClient` can query SUP reserve data correctly.
</issue_to_address>

### Comment 2
<location> `packages/streaming-sdk/src/streaming-sdk.ts:79-93` </location>
<code_context>
+        )
+    }
+
+    async updateStream(params: UpdateStreamParams): Promise<Hash> {
+        const { receiver, token, newFlowRate, userData = "0x", onHash } = params
+
+        const account = await this.getAccount()
+
+        return this.submitAndWait(
+            {
+                address: CFA_FORWARDER_ADDRESSES[this.chainId],
+                abi: cfaForwarderAbi,
+                functionName: "updateFlow",
+                args: [token, account, receiver, newFlowRate, userData],
+            },
+            onHash,
+        )
+    }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Inconsistent validation of flow rates between createStream and updateStream

`createStream` validates that `flowRate` is positive, but `updateStream` passes `newFlowRate` through unvalidated. If the protocol requires positive flow rates for active streams, this mismatch could cause unexpected reverts or inconsistent behavior. Please either align the validation behavior in `updateStream` or document why zero/negative values are acceptable here.

```suggestion
    async updateStream(params: UpdateStreamParams): Promise<Hash> {
        const { receiver, token, newFlowRate, userData = "0x", onHash } = params

        if (BigInt(newFlowRate) <= 0n) {
            throw new Error("newFlowRate must be a positive non-zero value")
        }

        const account = await this.getAccount()

        return this.submitAndWait(
            {
                address: CFA_FORWARDER_ADDRESSES[this.chainId],
                abi: cfaForwarderAbi,
                functionName: "updateFlow",
                args: [token, account, receiver, newFlowRate, userData],
            },
            onHash,
        )
    }
```
</issue_to_address>

### Comment 3
<location> `packages/react-hooks/src/streaming/useGDAPools.ts:13-16` </location>
<code_context>
+/**
+ * Hook for fetching GDA distribution pools
+ */
+export function useGDAPools({
+    environment = "production",
+    enabled = true,
+}: UseGDAPoolsParams = {}) {
+    const publicClient = usePublicClient()
+
</code_context>

<issue_to_address>
**suggestion:** Environment parameter in GDA hooks is unused by the underlying SDK

`useGDAPools` and `usePoolMemberships` accept an `environment` parameter and include it in the query key, but never pass it into `GdaSDK` (which only receives `publicClient`, `walletClient?`, `chainId?`). This makes `environment` appear to affect the data source while having no real impact. Either propagate `environment` into the SDK so it influences the selected subgraph/contracts, or remove it from the hook signatures and query keys to avoid misleading callers.

Suggested implementation:

```typescript
export interface UseGDAPoolsParams {
    enabled?: boolean
}

/**
 * Hook for fetching GDA distribution pools
 */

```

```typescript
export function useGDAPools({

```

To fully implement the suggestion and avoid misleading callers, you should also:
1. Remove `environment` from the `UseGDAPoolsParams` type wherever it is imported/used (other files in the codebase).
2. Remove `environment` from the React Query key (or any other caching key) used inside `useGDAPools` so it no longer appears to affect the data source.
3. If there are other related hooks like `usePoolMemberships` that expose `environment` but don't pass it into `GdaSDK`, apply the same changes there: drop `environment` from the params interface, from the function signature, and from any query keys.
</issue_to_address>

### Comment 4
<location> `apps/demo-identity-app/src/components/StreamingTestPage.tsx:90-99` </location>
<code_context>
+
+    const chainId = publicClient?.chain?.id
+
+    const getG$Token = () => {
+        if (!chainId) return "0x"
+        try {
+            // Mapping for UI purposes
+            const addresses: any = {
+                production: {
+                    42220: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A", // Celo Mainnet
+                },
+                staging: {
+                    42220: "0x61FA0fB802fd8345C06da558240E0651886fec69", // Celo Staging
+                },
+                development: {
+                    44787: "0xFa51eFDc0910CCdA91732e6806912Fa12e2FD475", // Celo Alfajores
+                    42220: "0xFa51eFDc0910CCdA91732e6806912Fa12e2FD475", // Fallback for user case
+                }
+            }
+            return addresses[environment]?.[chainId] || addresses[environment]?.[42220] || "0x"
+        } catch (e) {
+            return "0x"
</code_context>

<issue_to_address>
**issue (bug_risk):** Fallback to "0x" as a token address can cause runtime failures in SDK calls

`getG$Token` returns the literal `"0x"` when `chainId` is missing or the mapping lookup fails, then casts it to `Address` and passes it into streaming hooks/SDK calls. This is an invalid address and will likely cause opaque transaction/subgraph failures. Prefer returning `null`/`undefined` and disabling the related actions with a clear UI message, or throwing early with a descriptive error when no mapping exists for the current chain/environment.
</issue_to_address>

### Comment 5
<location> `apps/demo-identity-app/src/components/StreamingTestPage.tsx:322` </location>
<code_context>
-          </Text>
-
-          {/* Disclaimer Section */}
-          <YStack
-            padding="$3"
-            backgroundColor="#FFF8E1"
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting a reusable section card component and a shared mutation helper to reduce duplication and make this large page easier to read and maintain.

You can keep all functionality but cut a lot of cognitive load with two small abstractions that are local and lowrisk:

### 1. Extract a reusable `SectionCard` layout

You repeat the samewhite card with padding/border/gappattern for most sections. Creating a tiny wrapper will shrink JSX and make the structure easier to scan:

```tsx
// locally in this file (top-level, above the component) or in a small ui file
const SectionCard: React.FC<React.PropsWithChildren<{ gap?: any; bg?: string }>> = ({
  children,
  gap = "$3",
  bg = "white",
}) => (
  <YStack
    padding="$4"
    backgroundColor={bg}
    borderRadius="$4"
    borderWidth={1}
    borderColor="#E2E8F0"
    gap={gap}
  >
    {children}
  </YStack>
);
```

Then replace blocks like:

```tsx
<YStack
  padding="$4"
  backgroundColor="white"
  borderRadius="$4"
  borderWidth={1}
  borderColor="#E2E8F0"
  gap="$3"
>
  {/* content */}
</YStack>
```

with:

```tsx
<SectionCard>
  {/* content */}
</SectionCard>
```

For the docs card you can override `bg`:

```tsx
<SectionCard bg="#EBF8FF">
  {/* Documentation Section content */}
</SectionCard>
```

This alone significantly shortens the file and makes sections visually obvious.

---

### 2. Factor out a shared mutation helper for repeated handlers

Your mutation handlers (`handleCreateStream`, `handleUpdateStream`, `handleDeleteStream`, pool connect/disconnect) repeat the same `try → mutate → onSuccess/onError → alert/log` structure. You can abstract that without changing behavior:

```tsx
// helper near the top of the file
type MutationFn<TArgs> = (
  args: TArgs,
  opts: {
    onSuccess?: (hash: string) => void;
    onError?: (error: any) => void;
  },
) => void;

function runMutationWithAlerts<TArgs>(
  mutate: MutationFn<TArgs>,
  args: TArgs,
  {
    onSuccess,
    successMessage,
  }: { onSuccess?: (hash: string) => void; successMessage?: string } = {},
) {
  try {
    mutate(args, {
      onSuccess: (hash) => {
        if (successMessage) {
          alert(`${successMessage} Transaction: ${hash}`);
        }
        onSuccess?.(hash);
      },
      onError: (error: any) => {
        console.error(error);
        alert(`Error: ${error.message}`);
      },
    });
  } catch (error: any) {
    console.error(error);
    alert(`Error: ${error.message}`);
  }
}
```

Then your handlers become much smaller and consistent:

```tsx
const handleCreateStream = () => {
  if (!receiver || !amount) {
    alert("Please fill in all fields");
    return;
  }

  const flowRate = calculateFlowRate(parseEther(amount), timeUnit);

  runMutationWithAlerts(
    createStream,
    { receiver: receiver as Address, token: G$_TOKEN as Address, flowRate, environment },
    {
      onSuccess: (hash) => {
        setLastTxHash(hash);
        refetchStreams();
        setReceiver("");
        setAmount("10");
      },
    },
  );
};

const handleUpdateStream = () => {
  if (!updateReceiver || !updateAmount) {
    alert("Please fill in all fields");
    return;
  }

  const newFlowRate = calculateFlowRate(parseEther(updateAmount), timeUnit);

  runMutationWithAlerts(
    updateStream,
    {
      receiver: updateReceiver as Address,
      token: G$_TOKEN as Address,
      newFlowRate,
      environment,
    },
    {
      onSuccess: (hash) => {
        setLastTxHash(hash);
        refetchStreams();
      },
    },
  );
};
```

`disconnectFromPool` can also use this helper:

```tsx
const handleDisconnectFromPool = () => {
  if (!poolAddress) {
    alert("Please enter pool address");
    return;
  }

  runMutationWithAlerts(
    disconnectFromPool,
    { poolAddress: poolAddress as Address },
    { successMessage: "Disconnected!" },
  );
};
```

These two changes keep all behavior, avoid any structural redesign, but reduce duplication and make the component much easier to follow and extend.
</issue_to_address>

### Comment 6
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:112` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 7
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:125` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 8
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:138` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 9
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:150` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 10
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:167` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 11
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:182` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 12
<location> `packages/streaming-sdk/src/streaming-sdk.test.ts:269` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 13
<location> `packages/streaming-sdk/TESTING.md:105` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

### Comment 14
<location> `packages/streaming-sdk/README.md:48` </location>
<code_context>
0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +42 to +43
supReserve:
"https://thegraph.com/explorer/subgraphs/6dRuPxMvaJAp32hvcTsYbAya69A4t1KUHh2EnV3YQeXU",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): SUP reserve subgraph URL points to the Graph Explorer page, not the API endpoint

Because this constant is used by GraphQLClient in querySUPReserves, requests will fail against the HTML Explorer page. Please update SUBGRAPH_URLS.supReserve to the underlying GraphQL API endpoint (e.g., https://api.thegraph.com/subgraphs/...) so SubgraphClient can query SUP reserve data correctly.

Comment on lines +79 to +93
async updateStream(params: UpdateStreamParams): Promise<Hash> {
const { receiver, token, newFlowRate, userData = "0x", onHash } = params

const account = await this.getAccount()

return this.submitAndWait(
{
address: CFA_FORWARDER_ADDRESSES[this.chainId],
abi: cfaForwarderAbi,
functionName: "updateFlow",
args: [token, account, receiver, newFlowRate, userData],
},
onHash,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Inconsistent validation of flow rates between createStream and updateStream

createStream validates that flowRate is positive, but updateStream passes newFlowRate through unvalidated. If the protocol requires positive flow rates for active streams, this mismatch could cause unexpected reverts or inconsistent behavior. Please either align the validation behavior in updateStream or document why zero/negative values are acceptable here.

Suggested change
async updateStream(params: UpdateStreamParams): Promise<Hash> {
const { receiver, token, newFlowRate, userData = "0x", onHash } = params
const account = await this.getAccount()
return this.submitAndWait(
{
address: CFA_FORWARDER_ADDRESSES[this.chainId],
abi: cfaForwarderAbi,
functionName: "updateFlow",
args: [token, account, receiver, newFlowRate, userData],
},
onHash,
)
}
async updateStream(params: UpdateStreamParams): Promise<Hash> {
const { receiver, token, newFlowRate, userData = "0x", onHash } = params
if (BigInt(newFlowRate) <= 0n) {
throw new Error("newFlowRate must be a positive non-zero value")
}
const account = await this.getAccount()
return this.submitAndWait(
{
address: CFA_FORWARDER_ADDRESSES[this.chainId],
abi: cfaForwarderAbi,
functionName: "updateFlow",
args: [token, account, receiver, newFlowRate, userData],
},
onHash,
)
}

Comment on lines +13 to +16
export function useGDAPools({
environment = "production",
enabled = true,
}: UseGDAPoolsParams = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Environment parameter in GDA hooks is unused by the underlying SDK

useGDAPools and usePoolMemberships accept an environment parameter and include it in the query key, but never pass it into GdaSDK (which only receives publicClient, walletClient?, chainId?). This makes environment appear to affect the data source while having no real impact. Either propagate environment into the SDK so it influences the selected subgraph/contracts, or remove it from the hook signatures and query keys to avoid misleading callers.

Suggested implementation:

export interface UseGDAPoolsParams {
    enabled?: boolean
}

/**
 * Hook for fetching GDA distribution pools
 */
export function useGDAPools({

To fully implement the suggestion and avoid misleading callers, you should also:

  1. Remove environment from the UseGDAPoolsParams type wherever it is imported/used (other files in the codebase).
  2. Remove environment from the React Query key (or any other caching key) used inside useGDAPools so it no longer appears to affect the data source.
  3. If there are other related hooks like usePoolMemberships that expose environment but don't pass it into GdaSDK, apply the same changes there: drop environment from the params interface, from the function signature, and from any query keys.

Comment on lines +90 to +99
const getG$Token = () => {
if (!chainId) return "0x"
try {
// Mapping for UI purposes
const addresses: any = {
production: {
42220: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A", // Celo Mainnet
},
staging: {
42220: "0x61FA0fB802fd8345C06da558240E0651886fec69", // Celo Staging
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Fallback to "0x" as a token address can cause runtime failures in SDK calls

getG$Token returns the literal "0x" when chainId is missing or the mapping lookup fails, then casts it to Address and passes it into streaming hooks/SDK calls. This is an invalid address and will likely cause opaque transaction/subgraph failures. Prefer returning null/undefined and disabling the related actions with a clear UI message, or throwing early with a descriptive error when no mapping exists for the current chain/environment.


{/* Transaction Alert */}
{lastTxHash && (
<YStack
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting a reusable section card component and a shared mutation helper to reduce duplication and make this large page easier to read and maintain.

You can keep all functionality but cut a lot of cognitive load with two small abstractions that are local and low‑risk:

1. Extract a reusable SectionCard layout

You repeat the same “white card with padding/border/gap” pattern for most sections. Creating a tiny wrapper will shrink JSX and make the structure easier to scan:

// locally in this file (top-level, above the component) or in a small ui file
const SectionCard: React.FC<React.PropsWithChildren<{ gap?: any; bg?: string }>> = ({
  children,
  gap = "$3",
  bg = "white",
}) => (
  <YStack
    padding="$4"
    backgroundColor={bg}
    borderRadius="$4"
    borderWidth={1}
    borderColor="#E2E8F0"
    gap={gap}
  >
    {children}
  </YStack>
);

Then replace blocks like:

<YStack
  padding="$4"
  backgroundColor="white"
  borderRadius="$4"
  borderWidth={1}
  borderColor="#E2E8F0"
  gap="$3"
>
  {/* content */}
</YStack>

with:

<SectionCard>
  {/* content */}
</SectionCard>

For the docs card you can override bg:

<SectionCard bg="#EBF8FF">
  {/* Documentation Section content */}
</SectionCard>

This alone significantly shortens the file and makes sections visually obvious.


2. Factor out a shared mutation helper for repeated handlers

Your mutation handlers (handleCreateStream, handleUpdateStream, handleDeleteStream, pool connect/disconnect) repeat the same try → mutate → onSuccess/onError → alert/log structure. You can abstract that without changing behavior:

// helper near the top of the file
type MutationFn<TArgs> = (
  args: TArgs,
  opts: {
    onSuccess?: (hash: string) => void;
    onError?: (error: any) => void;
  },
) => void;

function runMutationWithAlerts<TArgs>(
  mutate: MutationFn<TArgs>,
  args: TArgs,
  {
    onSuccess,
    successMessage,
  }: { onSuccess?: (hash: string) => void; successMessage?: string } = {},
) {
  try {
    mutate(args, {
      onSuccess: (hash) => {
        if (successMessage) {
          alert(`${successMessage} Transaction: ${hash}`);
        }
        onSuccess?.(hash);
      },
      onError: (error: any) => {
        console.error(error);
        alert(`Error: ${error.message}`);
      },
    });
  } catch (error: any) {
    console.error(error);
    alert(`Error: ${error.message}`);
  }
}

Then your handlers become much smaller and consistent:

const handleCreateStream = () => {
  if (!receiver || !amount) {
    alert("Please fill in all fields");
    return;
  }

  const flowRate = calculateFlowRate(parseEther(amount), timeUnit);

  runMutationWithAlerts(
    createStream,
    { receiver: receiver as Address, token: G$_TOKEN as Address, flowRate, environment },
    {
      onSuccess: (hash) => {
        setLastTxHash(hash);
        refetchStreams();
        setReceiver("");
        setAmount("10");
      },
    },
  );
};

const handleUpdateStream = () => {
  if (!updateReceiver || !updateAmount) {
    alert("Please fill in all fields");
    return;
  }

  const newFlowRate = calculateFlowRate(parseEther(updateAmount), timeUnit);

  runMutationWithAlerts(
    updateStream,
    {
      receiver: updateReceiver as Address,
      token: G$_TOKEN as Address,
      newFlowRate,
      environment,
    },
    {
      onSuccess: (hash) => {
        setLastTxHash(hash);
        refetchStreams();
      },
    },
  );
};

disconnectFromPool can also use this helper:

const handleDisconnectFromPool = () => {
  if (!poolAddress) {
    alert("Please enter pool address");
    return;
  }

  runMutationWithAlerts(
    disconnectFromPool,
    { poolAddress: poolAddress as Address },
    { successMessage: "Disconnected!" },
  );
};

These two changes keep all behavior, avoid any structural redesign, but reduce duplication and make the component much easier to follow and extend.

sdk.updateStream({
receiver:
"0x1234567890123456789012345678901234567890" as Address,
token: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A" as Address,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks

sdk.deleteStream({
receiver:
"0x1234567890123456789012345678901234567890" as Address,
token: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A" as Address,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks


const mockQueryBalances = vi.fn().mockResolvedValue([
{
token: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks


const hash = await sdk.createStream({
receiver: '0x...',
token: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', // G$ on Celo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks

const flowRate = calculateFlowRate(parseEther('100'), 'month')
const hash = await streamingSDK.createStream({
receiver: '0x...',
token: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', // G$ on Celo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (generic-api-key): Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Source: gitleaks

@HushLuxe HushLuxe closed this Feb 15, 2026
@HushLuxe
Copy link
Contributor Author

Closing this PR with cleaner fixes

@HushLuxe HushLuxe deleted the feat/streaming-sdk branch February 16, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant