Skip to content

feat(streaming): implement superfluid sdk and react hooks#31

Open
HushLuxe wants to merge 45 commits intoGoodDollar:mainfrom
HushLuxe:feat/streaming-sdk
Open

feat(streaming): implement superfluid sdk and react hooks#31
HushLuxe wants to merge 45 commits intoGoodDollar:mainfrom
HushLuxe:feat/streaming-sdk

Conversation

@HushLuxe
Copy link
Copy Markdown
Contributor

@HushLuxe HushLuxe commented Feb 16, 2026

I'm adding the @goodsdks/streaming-sdk and @goodsdks/react-hooks (streaming module) to the monorepo. This provides a unified, type-safe way to handle Superfluid operations on Celo and Base, specifically optimized for G$ and GDA pools.

Changes

  • Implemented core StreamingSDK and GdaSDK in packages/streaming-sdk.
  • Added React hooks for stream management and GDA pool connectivity in packages/react-hooks.
  • Centralized protocol addresses and subgraph URLs in a single constants file.
  • Added comprehensive documentation and a testing guide.

Testing

I've verified the implementation with:

  • Unit Tests: All 37 tests for the core SDK and utilities are passing.
  • Build Verification: Both CommonJS and ESM builds are working as expected.
  • UI Testing: Manually tested stream creation/deletion and GDA pool connections in the demo app.
  • CI Prep: Ran turbo build and turbo lint across the workspace to ensure no regressions.

Checklist:

  • PR title follows convention: [(Feature) Superfluid Streaming SDK Implementation]
  • Code follows project style guidelines
  • Unit tests pass locally and cover the new functionality

Summary by Sourcery

Add a new Superfluid-based streaming SDK for Celo/Base and expose React hooks for managing streams and GDA pool interactions.

New Features:

  • Introduce @goodsdks/streaming-sdk providing StreamingSDK, GdaSDK, subgraph client, and utilities for Superfluid streams, GDA pools, and SUP reserve queries across supported chains and environments.
  • Add React streaming hooks in @goodsdks/react-hooks for listing streams, querying GDA pools and memberships, managing SUP reserves, and mutating streams and pool memberships via React Query.

Enhancements:

  • Centralize Superfluid protocol addresses, G$ SuperToken addresses, subgraph URLs, and chain metadata in shared constants for streaming operations.
  • Provide comprehensive README and TESTING documentation for the streaming SDK, including usage examples, configuration, and validation steps.

Tests:

  • Add a Vitest test suite for the streaming SDK covering utilities, chain validation, core StreamingSDK and GdaSDK behaviors, error handling, and edge cases.

Copy link
Copy Markdown
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 issue, and left some high level feedback:

  • In the React streaming hooks you eagerly instantiate StreamingSDK for all environments on every hook usage (e.g. useCreateStream, useUpdateStream, useDeleteStream); consider lazily creating only the environment actually used and/or sharing a memoized SDK factory to avoid repeated construction and to reduce unnecessary try/catch noise.
  • The userData parameter is accepted in the Streaming/GDA SDK methods and hook mutation params but is not consistently forwarded into the underlying contract calls (e.g. createStream uses setFlowrate without userData), which may be confusing to consumers; either wire it through where supported or remove it from the public API where it has no effect.
  • When resolving CFA_FORWARDER_ADDRESSES/GDA_FORWARDER_ADDRESSES by chainId, there is no guard for missing entries, so an unsupported or misconfigured chain could lead to an undefined address being used; consider validating that a forwarder address exists and throwing a clear error if not.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the React streaming hooks you eagerly instantiate `StreamingSDK` for all environments on every hook usage (e.g. `useCreateStream`, `useUpdateStream`, `useDeleteStream`); consider lazily creating only the environment actually used and/or sharing a memoized SDK factory to avoid repeated construction and to reduce unnecessary try/catch noise.
- The `userData` parameter is accepted in the Streaming/GDA SDK methods and hook mutation params but is not consistently forwarded into the underlying contract calls (e.g. `createStream` uses `setFlowrate` without `userData`), which may be confusing to consumers; either wire it through where supported or remove it from the public API where it has no effect.
- When resolving `CFA_FORWARDER_ADDRESSES`/`GDA_FORWARDER_ADDRESSES` by `chainId`, there is no guard for missing entries, so an unsupported or misconfigured chain could lead to an `undefined` address being used; consider validating that a forwarder address exists and throwing a clear error if not.

## Individual Comments

### Comment 1
<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 extracting a shared helper hook to build the StreamingSDK instances by environment and reuse it across the stream hooks (and optionally the list hook) to centralize environment handling and SDK setup.

You can reduce duplication and make the hooks easier to reason about by extracting a shared SDK helper and a single env definition, then using that across the stream hooks (and optionally the list hook).

### 1. Extract a shared env list + SDK map helper

```ts
const STREAMING_ENVIRONMENTS = ["production", "staging", "development"] as const

function useStreamingSdksByEnvironment() {
  const publicClient = usePublicClient()
  const { data: walletClient } = useWalletClient()

  return useMemo(() => {
    if (!publicClient) return new Map<Environment, StreamingSDK>()
    const m = new Map<Environment, StreamingSDK>()
    for (const e of STREAMING_ENVIRONMENTS) {
      // keep current swallow behavior if you need backward compatibility
      try {
        m.set(
          e,
          new StreamingSDK(
            publicClient,
            walletClient ? (walletClient as any) : undefined,
            { environment: e },
          ),
        )
      } catch {
        // ignore
      }
    }
    return m
  }, [publicClient, walletClient])
}
```

### 2. Reuse in `useCreateStream`, `useUpdateStream`, `useDeleteStream`

```ts
export function useCreateStream() {
  const sdks = useStreamingSdksByEnvironment()
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      flowRate,
      userData = "0x",
      environment = "production",
    }: UseCreateStreamParams): Promise<Hash> => {
      const sdk = sdks.get(environment)
      if (!sdk) throw new Error("SDK not available for selected environment")
      return sdk.createStream({ receiver, token, flowRate, userData })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] })
    },
  })
}

export function useUpdateStream() {
  const sdks = useStreamingSdksByEnvironment()
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      newFlowRate,
      userData = "0x",
      environment = "production",
    }: UseUpdateStreamParams): Promise<Hash> => {
      const sdk = sdks.get(environment)
      if (!sdk) throw new Error("SDK not available for selected environment")
      return sdk.updateStream({ receiver, token, newFlowRate, userData })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] })
    },
  })
}

export function useDeleteStream() {
  const sdks = useStreamingSdksByEnvironment()
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({
      receiver,
      token,
      environment = "production",
    }: UseDeleteStreamParams): Promise<Hash> => {
      const sdk = sdks.get(environment)
      if (!sdk) throw new Error("SDK not available for selected environment")
      return sdk.deleteStream({ receiver, token })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["streams"] })
    },
  })
}
```

This:

- Removes three nearly-identical `useMemo` blocks.
- Centralizes environment handling in one place.
- Removes repeated `publicClient`/`walletClient` checks inside each mutation (they’re implicitly handled by the `sdks` map being empty when clients are missing).

### 3. Optionally reuse helper in `useStreamList`

To keep environment handling consistent:

```ts
export function useStreamList({
  account,
  direction = "all",
  environment = "production",
  enabled = true,
}: UseStreamListParams) {
  const sdks = useStreamingSdksByEnvironment()
  const publicClient = usePublicClient()

  return useQuery<StreamInfo[]>({
    queryKey: ["streams", account, direction, environment, publicClient?.chain?.id],
    queryFn: async () => {
      const sdk = sdks.get(environment)
      if (!sdk) throw new Error("SDK not available for selected environment")
      return sdk.getActiveStreams(account, direction)
    },
    enabled: enabled && !!account && !!publicClient,
  })
}
```

This keeps all existing behavior but consolidates the SDK construction and environment logic so future changes only need to be made in one helper.
</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.

@sirpy sirpy linked an issue Feb 16, 2026 that may be closed by this pull request
13 tasks
@sirpy
Copy link
Copy Markdown
Contributor

sirpy commented Feb 16, 2026

@sourcery-ai guide does it fullfill issue #23

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Feb 16, 2026

Reviewer's Guide

Adds a new Superfluid-based streaming SDK package for Celo/Base plus React hooks that wrap it, centralizing protocol constants, subgraph access, and flow-rate utilities, and wires everything into the monorepo with tests, build config, and docs.

File-Level Changes

Change Details Files
Introduce @goodsdks/streaming-sdk with StreamingSDK, GdaSDK, SubgraphClient, utilities, and chain/config constants for Superfluid operations.
  • Implement StreamingSDK for creating, updating, deleting streams, querying balances/history, and SUP reserves using CFA forwarder and subgraph client.
  • Implement GdaSDK for connecting/disconnecting to GDA pools and querying pools/memberships and SUP reserves via GDA forwarder and subgraph client.
  • Add SubgraphClient wrapping graphql-request with typed queries for streams, balances, balance history, GDA pools/memberships, and SUP reserve lockers.
  • Define shared types for streams, pools, memberships, SUP reserves, and SDK options, plus utilities for chain validation, G$ token lookup, and flow-rate calculations/formatting.
  • Centralize chain metadata, Superfluid forwarder addresses, G$ SuperToken addresses per environment, and subgraph URLs in constants.
  • Set up package build (tsup), TypeScript config, vitest config, and package exports for both ESM and CJS consumers.
  • Add README and TESTING docs describing installation, APIs, environments, subgraph usage, security, and validation steps.
packages/streaming-sdk/src/streaming-sdk.ts
packages/streaming-sdk/src/gda-sdk.ts
packages/streaming-sdk/src/subgraph/client.ts
packages/streaming-sdk/src/types.ts
packages/streaming-sdk/src/utils.ts
packages/streaming-sdk/src/constants.ts
packages/streaming-sdk/src/index.ts
packages/streaming-sdk/src/sdk.test.ts
packages/streaming-sdk/tsup.config.ts
packages/streaming-sdk/tsconfig.json
packages/streaming-sdk/vitest.config.ts
packages/streaming-sdk/README.md
packages/streaming-sdk/TESTING.md
packages/streaming-sdk/package.json
Add React hooks wrapping the streaming SDK for stream CRUD, GDA pools, memberships, and SUP reserves, and expose them from the react-hooks package.
  • Implement React Query based hooks for creating, updating, deleting streams and listing streams per account/environment using StreamingSDK instances cached per environment.
  • Implement hooks for listing GDA pools, querying pool memberships, and connecting/disconnecting pools using GdaSDK bound to the current wagmi public/wallet clients.
  • Implement hook for querying SUP reserve lockers on Base via SubgraphClient, optionally authenticated with an API key.
  • Export the new streaming hooks from the react-hooks entry point and document their usage in the package README.
  • Declare @goodsdks/streaming-sdk and @tanstack/react-query as dependencies of the react-hooks package.
packages/react-hooks/src/streaming/index.ts
packages/react-hooks/src/index.ts
packages/react-hooks/README.md
packages/react-hooks/package.json

Assessment against linked issues

Issue Objective Addressed Explanation
#23 Create a new @goodsdks/streaming-sdk package that uses @sfpro/sdk on Celo and Base with chain validation and env-aware G$ addresses, and that provides typed APIs for Superfluid stream lifecycle (create/update/delete), listing running streams, SuperToken balances and history, GDA pools and memberships, SUP reserve queries, and safe, limited subgraph/RPC scans.
#23 Expose a React-hooks compatible streaming layer in @goodsdks/react-hooks for managing streams and GDA pools (CRUD on streams, list streams, list pools, query memberships, connect/disconnect pools, query SUP reserves) wired to the new streaming SDK.
#23 Add proper package scaffolding, exports, and documentation for the streaming SDK and hooks, including tsup build config, public exports from src/index.ts, README/TESTING docs with usage examples and notes on address resolution and subgraph usage, and ensure the workspace builds.

Possibly linked issues

  • #: They match exactly: the PR delivers the requested Superfluid streaming SDK, subgraph features, GDA pools, SUP reserves, and React hooks.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sirpy
Copy link
Copy Markdown
Contributor

sirpy commented Feb 16, 2026

@HushLuxe There's no specific integration of the G$/SUP tokens in the SDKs
The sdks should be initialized with a default token. the developer should not need to specify a token address.
@L03TJ3 how did you want this to be handled?

Copy link
Copy Markdown
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.

New security issues found

@HushLuxe HushLuxe requested a review from sirpy February 18, 2026 00:05
@sirpy
Copy link
Copy Markdown
Contributor

sirpy commented Feb 18, 2026

@HushLuxe do not use force push. I can not review the changes.
please revert and add your last changes as a separate commit

@HushLuxe HushLuxe requested a review from L03TJ3 March 4, 2026 16:28
}
}
`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the fetch logic for reserves is not filtered to the connected or passed down address/account.

It does not really make sense to expose fetching reserves in general

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @L03TJ3 , Thank you for getting back to me. I have scoped the SUP reserves query to the passed account (through the lockerOwner: $account) and made the account param mandatory, so it only returns the connected wallet’s lockers. I also removed the generic querySUPReserves from StreamingSDK so it only lives on SubgraphClient

return pools.find((p) => p.id.toLowerCase() === poolId.toLowerCase()) ?? null
}

async querySUPReserves() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do have a querySUPreserves for both gda-sdk and streaming-sdk?
whats the difference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have addressed too. I removed querySUPReserves from StreamingSDK so it now lives only on SubgraphClient to eliminate duplicates. The react hook now calls SubgraphClient directly

id: p.id as Address,
token: p.token.id as Address,
totalUnits: BigInt(p.totalUnits),
totalAmountClaimed: BigInt(p.totalAmountDistributedUntilUpdatedAt),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Misleading, totalAmountDistributed I believe is the total that the pool distributed, not what someone has claimed.

should this not use totalAmountClaimed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed the totalAmountClaimed semantic gap. It no longer pulls the pool-wide total distributions, and specifically maps the per-member amount directly.

@HushLuxe HushLuxe requested a review from L03TJ3 March 13, 2026 13:57
@HushLuxe HushLuxe force-pushed the feat/streaming-sdk branch from 64d5889 to 5418546 Compare March 13, 2026 14:19
@@ -0,0 +1,166 @@
# @goodsdks/streaming-sdk
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing items in the Readme:

  • getBalanceHistory

  • Sup Reserve handling (its also a very limited demonstration in the demo)

  • getActiveStreams does not include pagination example (And have a look at how pagination is handled as there are inconsistencies. What would happen if I provide {first: 20} and not define skip?)

  • address maps are not documented

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Demo-app does not demonstrate:

  • Sup Balances
  • getBalanceHistory

)
}

async getActiveStreams(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It is more efficient for historical/aggregated overviews to use subgraph.

However as per bounty request. we should also expose to read current live flow-rate. (using getFlowRate)


/**
* Internal helper to manage SDK instances by environment efficiently
*/
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not all methods have corresponding hooks.
and the wrapper react-hook for the sdk is not exported.

  • No useSuperTokenBalance hook
  • No useBalanceHistory hook
  • useStreamList params do not expose first/skip

@L03TJ3
Copy link
Copy Markdown
Collaborator

L03TJ3 commented Mar 30, 2026

Hey, making sure you saw my latest comments @HushLuxe

@HushLuxe
Copy link
Copy Markdown
Contributor Author

Thanks for the review@L03TJ3
Already working on it

@HushLuxe HushLuxe requested a review from a team March 30, 2026 19:06
@HushLuxe
Copy link
Copy Markdown
Contributor Author

Hi @L03TJ3... I have made an adjustment

The SDK README now includes getBalanceHistory, address resolution, SUP reserve handling, and getActiveStreams() pagination. I also fixed the pagination behavior so { first: 20 } works correctly without requiring skip, and added live flow reads via getFlowRate / getFlowInfo.

On the hooks/demo side, I added useStreamingSDK, useSuperTokenBalance, and useBalanceHistory, exposed first / skip on useStreamList, and updated the demo to show token balances and balance history alongside the existing stream and GDA views.

I also re-ran:

  • yarn workspace @goodsdks/streaming-sdk test --run
  • yarn workspace @goodsdks/react-hooks build
  • yarn workspace demo-streaming-app check-types

Happy to fix anything else quickly

@HushLuxe HushLuxe requested a review from L03TJ3 March 30, 2026 19:56
@L03TJ3 L03TJ3 removed the request for review from sirpy April 1, 2026 09:43
data: balanceHistory,
isLoading: balanceHistoryLoading,
refetch: refetchBalanceHistory,
} = useBalanceHistory({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

there is no from/to timestamp supplied, and we don't seem to have support for a default range (maybe last 30 days)

So this demo will never show anyone balance (that is also true for sup balance)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Alright @L03TJ3
Can I close this current pr and open another one?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No, we need to have a pull-request where the history of discussion is clear.
re-opening new PR's makes it very hard to continue reviewing with having to cross-reference what was done/already reviewed commented on etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have updated the demo (App.tsx) to pass an explicit rolling 30-day window to useBalanceHistory()

Also:

  • Updated the balance snapshot label to clearly say it’s showing the last 30 days.
  • Improved the “Active Streams” section labels for clarity. The direction label now shows From: when the connected wallet is receiving, and To: when it’s sending

@HushLuxe HushLuxe requested a review from L03TJ3 April 4, 2026 15:58
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.

Superfluid Streaming SDK (Celo + Base)

3 participants