Skip to content

Feat/superfluid streaming sdk#30

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

Feat/superfluid streaming sdk#30
HushLuxe wants to merge 5 commits intoGoodDollar:mainfrom
HushLuxe:feat/superfluid-streaming-sdk

Conversation

@HushLuxe
Copy link
Contributor

@HushLuxe HushLuxe commented Feb 15, 2026

This PR implements the Superfluid Streaming SDK along with its associated React hooks and an interactive test page for the GoodDollar ecosystem. The implementation provides a unified interface for managing money streams and GDA distribution pools on Celo and Base.

Key Technical Highlights:

  • Unified SDK Architecture: Consolidated streaming and distribution logic into high-cohesion modules.
  • CI/CD Stabilization: Resolved Vercel deployment and linting issues by standardizing ESLint Flat Config and adding explicit dependency builds to the workflow.
  • Documentation: Added STREAMING.md with clear instructions for testing and local development.
  • Secure Configuration: Implemented header-based API key management for subgraph queries and added .env support.
  • Official Compliance: Protocol addresses are retrieved directly from official Superfluid address maps.

How Has This Been Tested?

  • Unit Tests: 37 tests passing (100% coverage for core utilities and SDK logic).
  • External Consumer Test: Verified packaging and imports (CJS/ESM) in an isolated project.
  • Manual Verification: End-to-end testing of the [StreamingTestPage] on Celo/Base (instructions in [STREAMING.md]
  • Workspace Health: Passed global turbo build and turbo lint checks.

Checklist:

  • Code follows style guidelines and has been thoroughly self-reviewed
  • All requirements and instructions from task are fulfilled
  • Added comprehensive unit tests (37/37 passing)
  • Included a detailed description and user testing guide (STREAMING.md)
  • Verified Vercel and CI/CD compatibility

fixed issue #23

Summary by Sourcery

Introduce a Superfluid-based streaming SDK with React hooks and an interactive streaming test page, and wire it into the GoodDollar demo app and CI.

New Features:

  • Add @goodsdks/streaming-sdk package providing Superfluid streaming and GDA pool management on supported chains.
  • Expose new streaming-related React hooks in @goodsdks/react-hooks for managing streams, pools, SUP reserves, and associated mutations.
  • Add a StreamingTestPage route and UI in the demo identity app for creating, updating, deleting streams and interacting with GDA pools and SUP reserves.

Enhancements:

  • Extend the demo identity app routing and navigation with tabs for Identity and Streaming views while keeping existing identity and UBI flows intact.
  • Expand supported wallet networks in the demo identity app to include Base mainnet and Celo Alfajores.
  • Tighten error handling and typing in the ClaimButton and VerifyButton flows.
  • Document streaming SDK usage and testing via new STREAMING.md and TESTING.md guides, and update existing READMEs with streaming-related instructions.
  • Configure ESLint Flat Config for the demo app and adjust lint script for faster, scoped linting.
  • Add build configuration, TypeScript setup, and bundling for the new streaming SDK package, including Vitest setup for unit tests.

Build:

  • Add @goodsdks/streaming-sdk and React Query dependencies and configure tsup, TypeScript, and Vitest for building and testing the new package.
  • Mark the demo identity app as an ES module and update its lint script to use the new flat ESLint config with caching.

CI:

  • Update the Vercel preview workflow to include streaming-sdk and react-hooks packages and to build dependencies via Turbo before deploying.

Documentation:

  • Add STREAMING.md to the demo identity app describing how to use the streaming test page and required environment configuration.
  • Add comprehensive README and TESTING guides for @goodsdks/streaming-sdk and extend react-hooks and demo app READMEs with streaming hook usage and examples.

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 1 security issue, 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)

General comments:

  • In StreamingTestPage, OperationSection is being used with a disabled prop but the component’s props interface doesn’t define it or pass it through to the underlying button, which will cause type errors and makes the disabled state ineffective—add disabled?: boolean to the props and wire it to the Button’s disabled prop.
  • The change in VerifyButton to call onVerificationSuccess() immediately after setting window.location.href will mark verification as successful before the user actually completes the external flow; consider moving this callback to the point where the user returns and the verification result has been confirmed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `StreamingTestPage`, `OperationSection` is being used with a `disabled` prop but the component’s props interface doesn’t define it or pass it through to the underlying button, which will cause type errors and makes the disabled state ineffective—add `disabled?: boolean` to the props and wire it to the `Button`’s `disabled` prop.
- The change in `VerifyButton` to call `onVerificationSuccess()` immediately after setting `window.location.href` will mark verification as successful before the user actually completes the external flow; consider moving this callback to the point where the user returns and the verification result has been confirmed.

## Individual Comments

### Comment 1
<location> `apps/demo-identity-app/src/components/StreamingTestPage.tsx:62-70` </location>
<code_context>
+    </YStack>
+)
+
+const OperationSection: React.FC<{
+    title: string
+    buttonText: string
+    buttonColor: string
+    isLoading: boolean
+    onAction: (receiver: string, amount: string) => void
+    showAmount?: boolean
+    timeUnit?: string
+    setTimeUnit?: (unit: "hour" | "day" | "month") => void
+}> = ({
+    title,
</code_context>

<issue_to_address>
**issue (bug_risk):** OperationSection props don’t define a disabled flag, but the component is used with a disabled prop, which will cause a type error and the disabled state to be ignored.

To resolve this, add `disabled?: boolean` to `OperationSection`’s props and pass it through to the Button, e.g. `disabled={isLoading || disabled}`. That will satisfy TypeScript and ensure the Button is actually non-interactive when `!G$_TOKEN`.
</issue_to_address>

### Comment 2
<location> `apps/demo-identity-app/src/components/StreamingTestPage.tsx:273-282` </location>
<code_context>
+
+    const G$_TOKEN = chainId ? getG$Token(chainId, environment) : undefined
+
+    const handleAction = (
+        receiver: string,
+        amount: string,
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        mutation: (args: UseCreateStreamParams | UseUpdateStreamParams | UseDeleteStreamParams, options?: any) => void,
+        msg: string
+    ) => {
+        if (!receiver || (!amount && msg !== "Stream deleted!")) return alert("Please fill in all fields")
+        if (!G$_TOKEN) return alert('G$ token not configured for this chain/environment')
+        const flowRate = amount ? calculateFlowRate(parseEther(amount), timeUnit) : undefined
+
+        const args: UseCreateStreamParams & UseUpdateStreamParams & UseDeleteStreamParams = {
+            receiver: receiver as Address,
+            token: G$_TOKEN as Address,
</code_context>

<issue_to_address>
**issue (bug_risk):** handleAction uses SDK hook parameter types without importing them and combines them in a way that doesn’t match the actual shapes.

`UseCreateStreamParams`, `UseUpdateStreamParams`, and `UseDeleteStreamParams` aren’t imported, so this won’t compile. Also, typing `args` as `UseCreateStreamParams & UseUpdateStreamParams & UseDeleteStreamParams` is inaccurate, since those fields aren’t all present at once and the discriminator is `msg`. Either import the SDK types and use the appropriate one per action, or define a minimal local `args` type matching what the mutations actually need (receiver, token, environment, and optional flowRate/newFlowRate) instead of intersecting all three.
</issue_to_address>

### Comment 3
<location> `packages/react-hooks/src/streaming/index.ts:49-26` </location>
<code_context>
+    enabled?: boolean
+}
+
+export interface UseGDAPoolsParams {
+    enabled?: boolean
+}
+
+export interface UsePoolMembershipsParams {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** useGDAPools ignores environment selection, but callers pass an environment expecting behavior to change per env.

In `useGDAPools`, the params only accept `enabled`, and the hook constructs `GdaSDK` with `{ chainId: publicClient.chain?.id }` without using an environment. On `StreamingTestPage` you call `useGDAPools({ environment, enabled: !!address })`, so `environment` is effectively ignored and env buttons don’t change the data source. If the SDK’s subgraph or behavior is env-specific, this hook should accept an `environment?: Environment`, pass it into `GdaSDK`/`SubgraphClient`, and include it in the query key so caching and data align with the selected env.

Suggested implementation:

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

```

To fully implement the suggestion, also update the `useGDAPools` hook implementation (in this or the corresponding hooks file) to:
1. Accept `environment` in its params type (which will now be `UseGDAPoolsParams`).
2. Pass `environment` into the `GdaSDK` / `SubgraphClient` constructor instead of or in addition to `{ chainId: publicClient.chain?.id }`, according to how the SDK expects env configuration (e.g. `new GdaSDK({ chainId: publicClient.chain?.id, environment })`).
3. Include `environment` in the React Query key (e.g. `['gdaPools', environment, publicClient.chain?.id, ...]`) so results are cached per environment.
4. Ensure all existing callers that pass `environment` (like `StreamingTestPage`) now get env-specific behavior without type errors.
</issue_to_address>

### Comment 4
<location> `packages/streaming-sdk/src/streaming-sdk.ts:74-82` </location>
<code_context>
+        this.walletClient = walletClient
+    }
+
+    async createStream(params: CreateStreamParams): Promise<Hash> {
+        const { receiver, token, flowRate, userData = "0x", onHash } = params
+
+        if (flowRate <= BigInt(0)) {
+            throw new Error("Flow rate must be greater than zero")
+        }
+
+        return this.submitAndWait(
+            {
+                address: this.cfaForwarder,
</code_context>

<issue_to_address>
**suggestion:** The createStream API accepts userData but doesn’t pass it to the on-chain call, which is inconsistent with the type and the GdaSDK API.

`CreateStreamParams` exposes `userData`, but `setFlowrate` only sends `[token, receiver, flowRate]`. If the target CFA forwarder method supports `userData`, please forward it (to match `updateStream`/`deleteStream` and GdaSDK pool operations). If it doesn’t, consider removing `userData` from `CreateStreamParams` or clearly marking it as reserved so the API doesn’t imply unused functionality.

```suggestion
        return this.submitAndWait(
            {
                address: this.cfaForwarder,
                abi: cfaForwarderAbi,
                functionName: "setFlowrate",
                args: [token, receiver, flowRate, userData],
            },
            onHash,
        )
```
</issue_to_address>

### Comment 5
<location> `packages/react-hooks/src/streaming/index.ts:82` </location>
<code_context>
+    const { data: walletClient } = useWalletClient()
+    const queryClient = useQueryClient()
+
+    const sdks = useMemo(() => {
+        if (!publicClient) return new Map<string, StreamingSDK>()
+        const envs = ["production", "staging", "development"] as const
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the streaming and GDA hooks to use small shared factory/helpers instead of memoized maps and duplicated mutation logic to simplify and unify SDK usage.

You can simplify and de‑duplicate the hooks without changing behavior by:

1. Removing the per‑environment `Map` and constructing the SDK lazily in the mutation/query.
2. Extracting a small shared factory for streaming mutations.
3. Using a consistent pattern for GDA mutations.

### 1. Replace `sdks` map with a small SDK factory

Instead of memoizing a `Map<string, StreamingSDK>`, you can create a helper that constructs the SDK on demand:

```ts
function getStreamingSdk(
  publicClient: ReturnType<typeof usePublicClient>,
  walletClient: ReturnType<typeof useWalletClient>["data"] | undefined,
  environment: Environment
) {
  if (!publicClient) throw new Error("Public client not available");
  if (!walletClient) throw new Error("Wallet client not available");

  return new StreamingSDK(publicClient, walletClient, { environment });
}
```

Then your mutations become simpler and don’t need `useMemo` or a `Map`:

```ts
export function useCreateStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      flowRate,
      userData = "0x",
      environment = "production",
    }: UseCreateStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.createStream({ receiver, token, flowRate, userData });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}
```

You can apply the same pattern to `useUpdateStream` and `useDeleteStream`:

```ts
export function useUpdateStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      newFlowRate,
      userData = "0x",
      environment = "production",
    }: UseUpdateStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.updateStream({ receiver, token, newFlowRate, userData });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}

export function useDeleteStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      environment = "production",
    }: UseDeleteStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.deleteStream({ receiver, token });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}
```

This removes the repeated `useMemo` + `Map` logic and eagerly created SDKs, while preserving all functionality and error semantics (and keeps the `environment` arg).

### 2. Optional: DRY up streaming mutations further

If you want to centralize the common streaming mutation pattern:

```ts
function createStreamingMutation<TArgs>(
  mutate: (sdk: StreamingSDK, args: TArgs) => Promise<Hash>,
  invalidateKey: unknown[]
) {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (args: TArgs & { environment?: Environment }) => {
      const {
        environment = "production",
        ...rest
      } = args as TArgs & { environment?: Environment };
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return mutate(sdk, rest as TArgs);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: invalidateKey });
    },
  });
}
```

Usage:

```ts
export function useCreateStream() {
  return createStreamingMutation<UseCreateStreamParams>(
    (sdk, { receiver, token, flowRate, userData = "0x" }) =>
      sdk.createStream({ receiver, token, flowRate, userData }),
    ["streams"]
  );
}
```

Same pattern for update/delete with their respective payload shapes.

### 3. Align GDA mutations on a similar pattern

`useConnectToPool` and `useDisconnectFromPool` are very similar. You can use a small helper to reduce duplication and make the instantiation pattern consistent:

```ts
function createGdaMutation(
  mutate: (sdk: GdaSDK, args: UseConnectToPoolParams | UseDisconnectFromPoolParams) => Promise<Hash>
) {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (args): Promise<Hash> => {
      if (!publicClient) throw new Error("Public client not available");
      if (!walletClient) throw new Error("Wallet client not available");
      const sdk = new GdaSDK(publicClient, walletClient as any);
      return mutate(sdk, args);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["gda-pools"] });
      queryClient.invalidateQueries({ queryKey: ["gda-memberships"] });
    },
  });
}

export function useConnectToPool() {
  return createGdaMutation((sdk, { poolAddress, userData = "0x" }) =>
    sdk.connectToPool({ poolAddress, userData })
  );
}

export function useDisconnectFromPool() {
  return createGdaMutation((sdk, { poolAddress, userData = "0x" }) =>
    sdk.disconnectFromPool({ poolAddress, userData })
  );
}
```

These changes should address the reviewer’s concerns by:

- Eliminating repeated `useMemo` + map logic.
- Making SDK instantiation consistent across hooks.
- Centralizing error handling and invalidation behavior while preserving the existing API.
</issue_to_address>

### Comment 6
<location> `packages/streaming-sdk/src/sdk.test.ts:30` </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 +62 to +70
const OperationSection: React.FC<{
title: string
buttonText: string
buttonColor: string
isLoading: boolean
onAction: (receiver: string, amount: string) => void
showAmount?: boolean
timeUnit?: string
setTimeUnit?: (unit: "hour" | "day" | "month") => void
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): OperationSection props don’t define a disabled flag, but the component is used with a disabled prop, which will cause a type error and the disabled state to be ignored.

To resolve this, add disabled?: boolean to OperationSection’s props and pass it through to the Button, e.g. disabled={isLoading || disabled}. That will satisfy TypeScript and ensure the Button is actually non-interactive when !G$_TOKEN.

Comment on lines +273 to +282
const handleAction = (
receiver: string,
amount: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutation: (args: UseCreateStreamParams | UseUpdateStreamParams | UseDeleteStreamParams, options?: any) => void,
msg: string
) => {
if (!receiver || (!amount && msg !== "Stream deleted!")) return alert("Please fill in all fields")
if (!G$_TOKEN) return alert('G$ token not configured for this chain/environment')
const flowRate = amount ? calculateFlowRate(parseEther(amount), timeUnit) : undefined
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): handleAction uses SDK hook parameter types without importing them and combines them in a way that doesn’t match the actual shapes.

UseCreateStreamParams, UseUpdateStreamParams, and UseDeleteStreamParams aren’t imported, so this won’t compile. Also, typing args as UseCreateStreamParams & UseUpdateStreamParams & UseDeleteStreamParams is inaccurate, since those fields aren’t all present at once and the discriminator is msg. Either import the SDK types and use the appropriate one per action, or define a minimal local args type matching what the mutations actually need (receiver, token, environment, and optional flowRate/newFlowRate) instead of intersecting all three.

flowRate: bigint
userData?: `0x${string}`
environment?: Environment
}
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): useGDAPools ignores environment selection, but callers pass an environment expecting behavior to change per env.

In useGDAPools, the params only accept enabled, and the hook constructs GdaSDK with { chainId: publicClient.chain?.id } without using an environment. On StreamingTestPage you call useGDAPools({ environment, enabled: !!address }), so environment is effectively ignored and env buttons don’t change the data source. If the SDK’s subgraph or behavior is env-specific, this hook should accept an environment?: Environment, pass it into GdaSDK/SubgraphClient, and include it in the query key so caching and data align with the selected env.

Suggested implementation:

export interface UseGDAPoolsParams {
    environment?: Environment
    enabled?: boolean
}

To fully implement the suggestion, also update the useGDAPools hook implementation (in this or the corresponding hooks file) to:

  1. Accept environment in its params type (which will now be UseGDAPoolsParams).
  2. Pass environment into the GdaSDK / SubgraphClient constructor instead of or in addition to { chainId: publicClient.chain?.id }, according to how the SDK expects env configuration (e.g. new GdaSDK({ chainId: publicClient.chain?.id, environment })).
  3. Include environment in the React Query key (e.g. ['gdaPools', environment, publicClient.chain?.id, ...]) so results are cached per environment.
  4. Ensure all existing callers that pass environment (like StreamingTestPage) now get env-specific behavior without type errors.

Comment on lines +74 to +82
return this.submitAndWait(
{
address: this.cfaForwarder,
abi: cfaForwarderAbi,
functionName: "setFlowrate",
args: [token, receiver, flowRate],
},
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: The createStream API accepts userData but doesn’t pass it to the on-chain call, which is inconsistent with the type and the GdaSDK API.

CreateStreamParams exposes userData, but setFlowrate only sends [token, receiver, flowRate]. If the target CFA forwarder method supports userData, please forward it (to match updateStream/deleteStream and GdaSDK pool operations). If it doesn’t, consider removing userData from CreateStreamParams or clearly marking it as reserved so the API doesn’t imply unused functionality.

Suggested change
return this.submitAndWait(
{
address: this.cfaForwarder,
abi: cfaForwarderAbi,
functionName: "setFlowrate",
args: [token, receiver, flowRate],
},
onHash,
)
return this.submitAndWait(
{
address: this.cfaForwarder,
abi: cfaForwarderAbi,
functionName: "setFlowrate",
args: [token, receiver, flowRate, userData],
},
onHash,
)

const { data: walletClient } = useWalletClient()
const queryClient = useQueryClient()

const sdks = useMemo(() => {
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 refactoring the streaming and GDA hooks to use small shared factory/helpers instead of memoized maps and duplicated mutation logic to simplify and unify SDK usage.

You can simplify and de‑duplicate the hooks without changing behavior by:

  1. Removing the per‑environment Map and constructing the SDK lazily in the mutation/query.
  2. Extracting a small shared factory for streaming mutations.
  3. Using a consistent pattern for GDA mutations.

1. Replace sdks map with a small SDK factory

Instead of memoizing a Map<string, StreamingSDK>, you can create a helper that constructs the SDK on demand:

function getStreamingSdk(
  publicClient: ReturnType<typeof usePublicClient>,
  walletClient: ReturnType<typeof useWalletClient>["data"] | undefined,
  environment: Environment
) {
  if (!publicClient) throw new Error("Public client not available");
  if (!walletClient) throw new Error("Wallet client not available");

  return new StreamingSDK(publicClient, walletClient, { environment });
}

Then your mutations become simpler and don’t need useMemo or a Map:

export function useCreateStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      flowRate,
      userData = "0x",
      environment = "production",
    }: UseCreateStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.createStream({ receiver, token, flowRate, userData });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}

You can apply the same pattern to useUpdateStream and useDeleteStream:

export function useUpdateStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      newFlowRate,
      userData = "0x",
      environment = "production",
    }: UseUpdateStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.updateStream({ receiver, token, newFlowRate, userData });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}

export function useDeleteStream() {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      environment = "production",
    }: UseDeleteStreamParams): Promise<Hash> => {
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return sdk.deleteStream({ receiver, token });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] });
    },
  });
}

This removes the repeated useMemo + Map logic and eagerly created SDKs, while preserving all functionality and error semantics (and keeps the environment arg).

2. Optional: DRY up streaming mutations further

If you want to centralize the common streaming mutation pattern:

function createStreamingMutation<TArgs>(
  mutate: (sdk: StreamingSDK, args: TArgs) => Promise<Hash>,
  invalidateKey: unknown[]
) {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (args: TArgs & { environment?: Environment }) => {
      const {
        environment = "production",
        ...rest
      } = args as TArgs & { environment?: Environment };
      const sdk = getStreamingSdk(publicClient, walletClient, environment);
      return mutate(sdk, rest as TArgs);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: invalidateKey });
    },
  });
}

Usage:

export function useCreateStream() {
  return createStreamingMutation<UseCreateStreamParams>(
    (sdk, { receiver, token, flowRate, userData = "0x" }) =>
      sdk.createStream({ receiver, token, flowRate, userData }),
    ["streams"]
  );
}

Same pattern for update/delete with their respective payload shapes.

3. Align GDA mutations on a similar pattern

useConnectToPool and useDisconnectFromPool are very similar. You can use a small helper to reduce duplication and make the instantiation pattern consistent:

function createGdaMutation(
  mutate: (sdk: GdaSDK, args: UseConnectToPoolParams | UseDisconnectFromPoolParams) => Promise<Hash>
) {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (args): Promise<Hash> => {
      if (!publicClient) throw new Error("Public client not available");
      if (!walletClient) throw new Error("Wallet client not available");
      const sdk = new GdaSDK(publicClient, walletClient as any);
      return mutate(sdk, args);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["gda-pools"] });
      queryClient.invalidateQueries({ queryKey: ["gda-memberships"] });
    },
  });
}

export function useConnectToPool() {
  return createGdaMutation((sdk, { poolAddress, userData = "0x" }) =>
    sdk.connectToPool({ poolAddress, userData })
  );
}

export function useDisconnectFromPool() {
  return createGdaMutation((sdk, { poolAddress, userData = "0x" }) =>
    sdk.disconnectFromPool({ poolAddress, userData })
  );
}

These changes should address the reviewer’s concerns by:

  • Eliminating repeated useMemo + map logic.
  • Making SDK instantiation consistent across hooks.
  • Centralizing error handling and invalidation behavior while preserving the existing API.

writeContract: vi.fn().mockResolvedValue("0xhash"),
})

const TEST_SUPERTOKEN = "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

@HushLuxe
Copy link
Contributor Author

Closing this PR to remove the frontend test page.The test page made the files bulky and caused Vercel preview failures. I’ll reopen a new clean PR with only the core SDK + hooks 25

@HushLuxe HushLuxe closed this Feb 16, 2026
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