Skip to content

Commit

Permalink
react-sdk: first draft (#249)
Browse files Browse the repository at this point in the history
Missing for now:
- actions / mutations (need spec)
- defaultOptions (can be super useful for actions, its a good place to
add toasts)
- example usage in playground
- tests
  • Loading branch information
miguel-nascimento authored Jun 27, 2024
1 parent 54853c1 commit 26e373a
Show file tree
Hide file tree
Showing 24 changed files with 498 additions and 32 deletions.
5 changes: 5 additions & 0 deletions packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@river-build/react-sdk": "workspace:^",
"@river-build/sdk": "workspace:^",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"ethers": "^5.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.38.0",
"react-router-dom": "^6.4.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^1.18.2",
"wagmi": "^1.4.12",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
21 changes: 18 additions & 3 deletions packages/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { RouterProvider } from 'react-router-dom'
import { WagmiConfig, configureChains, createConfig, mainnet } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'
import { InjectedConnector } from 'wagmi/connectors/injected'
import { RiverSyncProvider } from '@river-build/react-sdk'
import { router } from './routes'

const { publicClient, webSocketPublicClient } = configureChains([mainnet], [publicProvider()])

const config = createConfig({
autoConnect: true,
publicClient,
webSocketPublicClient,
connectors: [new InjectedConnector()],
})

function App() {
return (
<>
<RouterProvider router={router} />
</>
<WagmiConfig config={config}>
<RiverSyncProvider>
<RouterProvider router={router} />
</RiverSyncProvider>
</WagmiConfig>
)
}

Expand Down
11 changes: 10 additions & 1 deletion packages/playground/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { createBrowserRouter } from 'react-router-dom'
import { RootLayout } from './root'
import { RootLayout } from './layout'

export const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
path: '/',
lazy: async () => {
const { ConnectRoute } = await import('./root')
return {
Component: ConnectRoute,
}
},
},
{
path: 'components',
lazy: async () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/playground/src/routes/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Outlet } from 'react-router-dom'

export const RootLayout = () => {
return (
<div className="flex min-h-screen w-full flex-col">
<header className="border-b border-zinc-200 px-4 py-3">
<h1 className="text-2xl font-bold">River Playground</h1>
</header>
<div className="flex h-full flex-1 flex-col bg-zinc-50 px-4 pt-8">
<Outlet />
</div>
</div>
)
}
90 changes: 82 additions & 8 deletions packages/playground/src/routes/root.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,88 @@
import { Outlet } from 'react-router-dom'
import { useAccount, useConnect } from 'wagmi'
import { makeRiverConfig } from '@river-build/sdk'
import { useRiverConnection, useSyncValue } from '@river-build/react-sdk'
import { Button } from '@/components/ui/button'
import { useEthersSigner } from '@/utils/viem-to-ethers'

export const ConnectRoute = () => {
const { isConnected } = useAccount()

return (
<div className="flex flex-col gap-6">
{isConnected ? <ConnectRiver /> : <ChainConnectButton />}
</div>
)
}

const ChainConnectButton = () => {
const { connector: activeConnector } = useAccount()
const { connect, connectors, error, isLoading, pendingConnector } = useConnect()

return (
<div>
{connectors.map((connector) => (
<Button
disabled={!connector.ready}
key={connector.id}
onClick={() => connect({ connector })}
>
{activeConnector?.id === connector.id
? `Connected - ${connector.name}`
: connector.name}
{isLoading && pendingConnector?.id === connector.id && ' (connecting)'}
</Button>
))}
{error && <div>{error.message}</div>}
</div>
)
}

const riverConfig = makeRiverConfig('gamma')

const ConnectRiver = () => {
const signer = useEthersSigner()
const { connect, disconnect, isConnecting, isConnected } = useRiverConnection()

export const RootLayout = () => {
return (
<div className="flex min-h-screen w-full flex-col">
<header className="border-b border-zinc-200 px-4 py-3">
<h1 className="text-2xl font-bold">River Playground</h1>
</header>
<div className="flex h-full flex-1 flex-col bg-zinc-50 px-4 pt-8">
<Outlet />
<>
<div>
<Button
onClick={() => {
if (isConnected) {
disconnect()
} else {
if (signer) {
connect(signer, { riverConfig })
}
}
}}
>
{isConnected ? 'Disconnect' : isConnecting ? 'Connecting...' : 'Connect'}
</Button>
</div>
{isConnected ? (
<>
<h2 className="text-lg font-semibold">Connected to Sync Agent</h2>
<ConnectedContent />
</>
) : (
<h2 className="text-lg font-semibold">Not Connected</h2>
)}
</>
)
}

const ConnectedContent = () => {
const { data: nodeUrls } = useSyncValue((s) => s.riverStreamNodeUrls, {
onUpdate: (data) => console.log('onUpdate', data),
onError: (error) => console.error('onError', error),
onSaved: (data) => console.log('onSaved', data),
})
return (
<div className="max-w-xl rounded-sm border border-zinc-200 bg-zinc-100 p-2">
<pre className="overflow-auto whitespace-pre-wrap">
{JSON.stringify(nodeUrls, null, 2)}
</pre>
</div>
)
}
24 changes: 24 additions & 0 deletions packages/playground/src/utils/viem-to-ethers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react'
import { type WalletClient, useWalletClient } from 'wagmi'
import { providers } from 'ethers'

export function walletClientToSigner(walletClient: WalletClient) {
const { account, chain, transport } = walletClient
const network = {
chainId: chain.id,
name: chain.name,
ensAddress: chain.contracts?.ensRegistry?.address,
}
const provider = new providers.Web3Provider(transport, network)
const signer = provider.getSigner(account.address)
return signer
}

/** Hook to convert a viem Wallet Client to an ethers.js Signer. */
export function useEthersSigner({ chainId }: { chainId?: number } = {}) {
const { data: walletClient } = useWalletClient({ chainId })
return React.useMemo(
() => (walletClient ? walletClientToSigner(walletClient) : undefined),
[walletClient],
)
}
50 changes: 48 additions & 2 deletions packages/react-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,55 @@ yarn add @river-build/react-sdk

# Usage

TODO for now. But that's what we need to solve:
## Connect to River

## Connect to a River stream
`@river-build/react-sdk` suggests you to use Wagmi to connect to River.
Wrap your app with `RiverSyncProvider` and use the `useConnectRiver` hook to connect to River.

> [!note] If you're using Viem
> You'll need to use `useEthersSigner` to get the signer from viem wallet client.
> You can get the hook from [wagmi docs](https://wagmi.sh/react/guides/ethers#usage-1).
```tsx
import {
RiverSyncProvider,
useConnectRiver,
useConnection,
} from "@river-build/react-sdk";
import { makeRiverConfig } from "@river-build/sdk";
import { WagmiProvider } from "wagmi";
import { useEthersSigner } from "./utils/viem-to-ethers";

const riverConfig = makeRiverConfig("gamma");

const App = ({ children }: { children: React.ReactNode }) => {
return (
<WagmiProvider config={wagmiConfig}>
<RiverSyncProvider>{children}</RiverSyncProvider>
</WagmiProvider>
);
};

const ConnectRiver = () => {
const { connect, isConnecting, isConnected } = useConnectRiver();
const signer = useEthersSigner();

return (
<>
<button
onClick={() => {
if (signer) {
connect(signer, { riverConfig });
}
}}
>
{isConnecting ? "Disconnect" : "Connect"}
</button>
{isConnected && <span>Connected!</span>}
</>
);
};
```

## Get information about an account

Expand Down
11 changes: 8 additions & 3 deletions packages/react-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"build": "yarn run clean && yarn run build:esm+types",
"build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"watch": "yarn build -w",
"test:build": "publint --strict && attw --pack --ignore-rules cjs-resolves-to-esm",
"typecheck": "tsc --noEmit"
},
Expand All @@ -23,13 +24,17 @@
],
"sideEffects": false,
"type": "module",
"main": "./dist/esm/exports/index.js",
"types": "./dist/types/exports/index.d.ts",
"typings": "./dist/types/exports/index.d.ts",
"main": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"peerDependencies": {
"react": "^18.2.0",
"typescript": "^5.1.6"
},
"dependencies": {
"@river-build/sdk": "workspace:^",
"ethers": "^5.7.2"
},
"devDependencies": {
"@testing-library/react": "^14.2.1",
"@types/react": "^18.2.11",
Expand Down
30 changes: 30 additions & 0 deletions packages/react-sdk/src/RiverSyncProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'
import type { SyncAgent } from '@river-build/sdk'
import { useEffect, useState } from 'react'
import { RiverSyncContext } from './internals/RiverSyncContext'

type RiverSyncProviderProps = {
syncAgent?: SyncAgent
children?: React.ReactNode
}

export const RiverSyncProvider = (props: RiverSyncProviderProps) => {
const [syncAgent, setSyncAgent] = useState(() => props.syncAgent)

useEffect(() => {
if (syncAgent) {
syncAgent.start()
}
}, [syncAgent])

return (
<RiverSyncContext.Provider
value={{
syncAgent,
setSyncAgent,
}}
>
{props.children}
</RiverSyncContext.Provider>
)
}
13 changes: 13 additions & 0 deletions packages/react-sdk/src/connectRiver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// This file can be used on server side to create a River Client
/// We don't want a 'use client' directive here
import { SyncAgent, type SyncAgentConfig, makeSignerContext } from '@river-build/sdk'
import { ethers } from 'ethers'

export const connectRiver = async (
signer: ethers.Signer,
config: Omit<SyncAgentConfig, 'context'>,
): Promise<SyncAgent> => {
const delegateWallet = ethers.Wallet.createRandom()
const signerContext = await makeSignerContext(signer, delegateWallet)
return new SyncAgent({ context: signerContext, ...config })
}
3 changes: 0 additions & 3 deletions packages/react-sdk/src/context.ts

This file was deleted.

11 changes: 10 additions & 1 deletion packages/react-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export { hello } from './context'
/**************************************************************************
* This file can be auto generated by 🏕️ scripts/generate_sdk_index.sh 🏕️ *
**************************************************************************/
export * from './RiverSyncProvider'
export * from './connectRiver'
export * from './useObservable'
export * from './useRiverConnection'
export * from './useSyncAgent'
export * from './useSyncValue'
export * from './utils'
9 changes: 9 additions & 0 deletions packages/react-sdk/src/internals/RiverSyncContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use client'
import { SyncAgent } from '@river-build/sdk'
import { createContext } from 'react'

type RiverSyncContextType = {
syncAgent: SyncAgent | undefined
setSyncAgent: (syncAgent: SyncAgent | undefined) => void
}
export const RiverSyncContext = createContext<RiverSyncContextType | undefined>(undefined)
4 changes: 4 additions & 0 deletions packages/react-sdk/src/internals/useRiverSync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use client'
import { useContext } from 'react'
import { RiverSyncContext } from './RiverSyncContext'
export const useRiverSync = () => useContext(RiverSyncContext)
Loading

0 comments on commit 26e373a

Please sign in to comment.