Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/" }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like a centralized service, doesn't it make sense to choose from selectable environments rather than a connection string? Like SprinterEnvironment.Test or SprinterEnvironment.Main

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It defaults to mainnet and with that approach, we eliminate the need for updates for client's custom deployments or proxies

<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