Skip to content

Commit

Permalink
feat: react sdk context (#48)
Browse files Browse the repository at this point in the history
closes #47
  • Loading branch information
BeroBurny authored Sep 23, 2024
1 parent efd1be3 commit 39dbe7e
Show file tree
Hide file tree
Showing 34 changed files with 2,112 additions and 36 deletions.
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"packages/sdk": "0.1.0"
"packages/sdk": "0.1.0",
"packages/react": "0.0.0"
}
26 changes: 26 additions & 0 deletions packages/react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

*.tsbuildinfo

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
84 changes: 84 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Sprinter React SDK

The Sprinter React SDK is a React wrapper for the [Sprinter SDK](https://github.com/ChainSafe/sprinter-sdk), enabling easy interaction with blockchain networks through React components and hooks. It provides context management and custom hooks for retrieving balances, tokens, supported chains, and solutions for asset transfers.

## Installation

To install the package, use npm or yarn:

```bash
npm install @chainsafe/sprinter-react
# or
yarn add @chainsafe/sprinter-react
```

## Usage

Wrap your application in the `SprinterContext` to gain access to blockchain-related data within your component tree.

### Example

```tsx
import React from 'react';
import { SprinterContext } from '@chainsafe/sprinter-react';

const App = () => (
<SprinterContext>
<YourComponent />
</SprinterContext>
);

export default App;
```

Inside your components, you can use the provided hooks to interact with blockchain data:

```tsx
import React, { useEffect } from 'react';
import { useSprinterBalances, useSprinterTokens } from '@chainsafe/sprinter-react';

const YourComponent = () => {
const ownerAddress = "0xYourAddressHere";
const { balances, getUserBalances } = useSprinterBalances(ownerAddress);

useEffect(() => {
getUserBalances();
}, [getUserBalances]);

return (
<div>
<h1>Balances:</h1>
<pre>{JSON.stringify(balances, null, 2)}</pre>
<h1>Available Tokens:</h1>
<pre>{JSON.stringify(tokens, null, 2)}</pre>
</div>
);
};

export default YourComponent;
```

### Available Hooks

The following hooks are provided by the SDK:

- **`useSprinter()`**: Access everything from the Sprinter context.
- **`useSprinterBalances(account: Address)`**: Retrieve user balances for a given account.
- **`useSprinterTokens()`**: Retrieve available tokens.
- **`useSprinterChains()`**: Retrieve available blockchain chains.
- **`useSprinterSolution()`**: Retrieve solutions for asset transfers.
- **`useSprinterCallSolution()`**: Call solutions for transferring assets.

### Custom Fetch Options

You can pass custom fetch options when initializing the context:

```tsx
<SprinterContext fetchOptions={{ baseUrl: "https://api.test.sprinter.buildwithsygma.com/" }}>
<YourComponent />
</SprinterContext>
```

## Contributing

Contributions are welcome! Feel free to open issues or submit pull requests for new features or bug fixes.
28 changes: 28 additions & 0 deletions packages/react/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
13 changes: 13 additions & 0 deletions packages/react/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
49 changes: 49 additions & 0 deletions packages/react/lib/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {createContext, ReactNode, useEffect, useState} from "react";
import {type FetchOptions, Sprinter} from "@chainsafe/sprinter-sdk";
import {useTokens} from "./internal/useTokens.ts";
import {useChains} from "./internal/useChains.ts";
import {useSolution} from "./internal/useSolution.ts";
import {useCallSolution} from "./internal/useCallSolution.ts";
import {useBalances} from "./internal/useBalances.ts";

type SprinterContext = ReturnType<typeof useBalances> & ReturnType<typeof useTokens> & ReturnType<typeof useChains> & ReturnType<typeof useSolution> & ReturnType<typeof useCallSolution>;

export const Context = createContext<SprinterContext | null>(null);

interface SprinterContextProps {
children?: ReactNode | undefined;
fetchOptions?: Omit<FetchOptions, "signal">;
}

export function SprinterContext({ children, fetchOptions }: SprinterContextProps) {
const [sprinter] = useState(new Sprinter(fetchOptions));

/** Balances */
const { balances, getUserBalances } = useBalances(sprinter);

/** Tokens */
const { tokens, getAvailableTokens } = useTokens(sprinter);

/** Chains */
const { chains, getAvailableChains } = useChains(sprinter);

/** Solutions */
const { solution, getSolution } = useSolution(sprinter);

/** Call Solution */
const { callSolution, getCallSolution } = useCallSolution(sprinter);

/** Initialization */
useEffect(() => {
getAvailableTokens();
getAvailableChains();
}, [sprinter]);

return <Context.Provider value={{
balances, getUserBalances,
tokens, getAvailableTokens,
chains, getAvailableChains,
solution, getSolution,
callSolution, getCallSolution
}}>{children}</Context.Provider>;
}
53 changes: 53 additions & 0 deletions packages/react/lib/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {useCallback, useContext} from "react";
import {Context} from "./context.tsx";
import {Address} from "@chainsafe/sprinter-sdk";
import {BalancesEntry} from "./internal/useBalances.ts";

/** Everything from context */
export function useSprinter() {
const context = useContext(Context);

if (!context) throw new Error('Sprinter Context is not defined');

return context;
}

/** Balances */
const balancesEmptyState = {
data: null,
loading: false,
error: null,
};

export function useSprinterBalances(account: Address) {
const { balances: _balances, getUserBalances: _getUserBalances } = useSprinter();

const balances: BalancesEntry = _balances[account] || balancesEmptyState;
const getUserBalances = useCallback(() => _getUserBalances(account), [account]);

return { balances, getUserBalances };
}

/** Tokens */
export function useSprinterTokens() {
const { tokens, getAvailableTokens } = useSprinter();
return { tokens, getAvailableTokens };
}

/** Chains */
export function useSprinterChains() {
const { chains, getAvailableChains } = useSprinter();
return { chains, getAvailableChains };
}

/** Solutions */
export function useSprinterSolution() {
const { solution, getSolution } = useSprinter();
return { solution, getSolution };
}

/** Call Solution */
export function useSprinterCallSolution() {
const { callSolution, getCallSolution } = useSprinter();
return { callSolution, getCallSolution };
}
65 changes: 65 additions & 0 deletions packages/react/lib/internal/useAsyncRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {useCallback, useReducer} from "react";

const initialState = {
data: null,
loading: false,
error: null,
};

export function useAsyncRequest<T>() {
const [state, dispatch] = useReducer(fetchReducer<T>, initialState as AsyncRequestState<T>);

const makeRequest = useCallback((request: Promise<T>) => {
dispatch({ type: RequestAction.INIT });

request.then(result => {
dispatch({ type: RequestAction.SUCCESS, payload: result });
}).catch((error: Error) => {
dispatch({ type: RequestAction.FAILURE, error: error.message });
});
}, [dispatch]);

return { state, makeRequest };
}

enum RequestAction {
INIT = 'REQUEST_INIT',
SUCCESS = 'REQUEST_SUCCESS',
FAILURE = "REQUEST_FAILURE",
}

export interface AsyncRequestState<T> {
data: T | null;
loading: boolean;
error: string | null;
}

type AsyncRequestActions<T> =
| { type: RequestAction.INIT }
| { type: RequestAction.SUCCESS; payload: T }
| { type: RequestAction.FAILURE; error: string };

const fetchReducer = <T>(state: AsyncRequestState<T>, action: AsyncRequestActions<T>): AsyncRequestState<T> => {
switch (action.type) {
case RequestAction.INIT:
return {
...state,
loading: true,
error: null,
};
case RequestAction.SUCCESS:
return {
...state,
loading: false,
data: action.payload,
};
case RequestAction.FAILURE:
return {
...state,
loading: false,
error: action.error,
};
default:
throw new Error('Unknown action type');
}
};
Loading

0 comments on commit 39dbe7e

Please sign in to comment.