Skip to content

Commit

Permalink
[dapp-kit] Add ability to configure networks to SuiClientProvider (#1…
Browse files Browse the repository at this point in the history
…3342)

## Description 

This is getting into react stuff where I am not very confident I know
what I am doing. The goal here is to provide a Context provider that
supports switching between networks, and allows providing either
pre-configured SuiClients for each network, or a config object (with
customizable options) that can be used to create a client.

## Test Plan 

How did you test the new or updated feature?

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
hayes-mysten authored Aug 14, 2023
1 parent f786180 commit 65ecb84
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 37 deletions.
18 changes: 15 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions sdk/dapp-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@
"@mysten/build-scripts": "workspace:*",
"@size-limit/preset-small-lib": "^8.2.6",
"@tanstack/react-query": "^4.29.25",
"@testing-library/dom": "^9.3.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.2.15",
"@types/testing-library__jest-dom": "^5.14.9",
"happy-dom": "^10.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
74 changes: 57 additions & 17 deletions sdk/dapp-kit/src/components/SuiClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { SuiClient, getFullnodeUrl } from '@mysten/sui.js/client';
import { SuiClientContext } from '../hooks/useSuiClient.js';
import { useMemo } from 'react';
import { SuiClient, getFullnodeUrl, isSuiClient } from '@mysten/sui.js/client';
import type { SuiClientOptions } from '@mysten/sui.js/client';
import { createContext, useMemo, useState } from 'react';

export interface SuiClientProviderProps {
type NetworkConfig = SuiClient | SuiClientOptions;
type NetworkConfigs<T extends NetworkConfig = NetworkConfig> = Record<string, T>;

export interface SuiClientProviderContext {
client: SuiClient;
networks: NetworkConfigs;
selectedNetwork: string;
selectNetwork: (network: string) => void;
}

export const SuiClientContext = createContext<SuiClientProviderContext | null>(null);

export interface SuiClientProviderProps<T extends NetworkConfigs> {
networks?: T;
createClient?: (name: keyof T, config: T[keyof T]) => SuiClient;
defaultNetwork?: keyof T & string;
children: React.ReactNode;
client?: SuiClient;
url?: string;
queryKeyPrefix: string;
}

export const SuiClientProvider = (props: SuiClientProviderProps) => {
const ctx = useMemo(() => {
const client =
props.client ??
new SuiClient({
url: props.url ?? getFullnodeUrl('devnet'),
});
const DEFAULT_NETWORKS = {
localnet: { url: getFullnodeUrl('localnet') },
};

const DEFAULT_CREATE_CLIENT = function createClient(
_name: string,
config: NetworkConfig | SuiClient,
) {
if (isSuiClient(config)) {
return config;
}

return new SuiClient(config);
};

export function SuiClientProvider<T extends NetworkConfigs>(props: SuiClientProviderProps<T>) {
const networks = (props.networks ?? DEFAULT_NETWORKS) as T;
const createClient =
(props.createClient as typeof DEFAULT_CREATE_CLIENT) ?? DEFAULT_CREATE_CLIENT;

const [selectedNetwork, setSelectedNetwork] = useState<keyof T & string>(
props.defaultNetwork ?? (Object.keys(networks)[0] as keyof T & string),
);

const [client, setClient] = useState<SuiClient>(() => {
return createClient(selectedNetwork, networks[selectedNetwork]);
});

const ctx = useMemo((): SuiClientProviderContext => {
return {
client,
queryKey: (key: unknown[]) => [props.queryKeyPrefix, ...key],
networks,
selectedNetwork,
selectNetwork: (network) => {
if (network !== selectedNetwork) {
setSelectedNetwork(network);
setClient(createClient(network, networks[network]));
}
},
};
}, [props.client, props.url, props.queryKeyPrefix]);
}, [client, setClient, createClient, selectedNetwork, networks]);

return <SuiClientContext.Provider value={ctx}>{props.children}</SuiClientContext.Provider>;
};
}
12 changes: 2 additions & 10 deletions sdk/dapp-kit/src/hooks/useSuiClient.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { SuiClient } from '@mysten/sui.js/client';
import { createContext, useContext } from 'react';

export const SuiClientContext = createContext<
| {
client: SuiClient;
queryKey: (key: unknown[]) => unknown[];
}
| undefined
>(undefined);
import { useContext } from 'react';
import { SuiClientContext } from '../components/SuiClientProvider.js';

export function useSuiClientContext() {
const suiClient = useContext(SuiClientContext);
Expand Down
3 changes: 1 addition & 2 deletions sdk/dapp-kit/src/hooks/useSuiClientQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ export function useSuiClientQuery<T extends keyof Methods>(

return useQuery({
...options,
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: suiContext.queryKey(queryKey ?? [method, params]),
queryKey: [suiContext.selectedNetwork, method, params],
enabled,
queryFn: async () => {
return await suiContext.client[method](params as never);
Expand Down
104 changes: 104 additions & 0 deletions sdk/dapp-kit/test/components/SuiClientProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';
import { SuiClientProvider } from '../../src/components/SuiClientProvider.js';
import { useSuiClient, useSuiClientContext } from 'dapp-kit/src/index.js';
import { SuiClient } from '@mysten/sui.js/client';
import { useState } from 'react';

describe('SuiClientProvider', () => {
it('renders without crashing', () => {
render(
<SuiClientProvider>
<div>Test</div>
</SuiClientProvider>,
);
expect(screen.getByText('Test')).toBeInTheDocument();
});

it('provides a SuiClient instance to its children', () => {
const ChildComponent = () => {
const client = useSuiClient();
expect(client).toBeInstanceOf(SuiClient);
return <div>Test</div>;
};

render(
<SuiClientProvider>
<ChildComponent />
</SuiClientProvider>,
);
});

it('can accept pre-configured SuiClients', () => {
const suiClient = new SuiClient({ url: 'http://localhost:8080' });
const ChildComponent = () => {
const client = useSuiClient();
expect(client).toBeInstanceOf(SuiClient);
expect(client).toBe(suiClient);
return <div>Test</div>;
};

render(
<SuiClientProvider networks={{ localnet: suiClient }}>
<ChildComponent />
</SuiClientProvider>,
);

expect(screen.getByText('Test')).toBeInTheDocument();
});

test('can create sui clients with custom options', async () => {
function NetworkSelector() {
const ctx = useSuiClientContext();

return (
<div>
{Object.keys(ctx.networks).map((network) => (
<button key={network} onClick={() => ctx.selectNetwork(network)}>
{`select ${network}`}
</button>
))}
</div>
);
}
function CustomConfigProvider() {
const [selectedNetwork, setSelectedNetwork] = useState<string>();

return (
<SuiClientProvider
networks={{
a: {
url: 'http://localhost:8080',
custom: setSelectedNetwork,
},
b: {
url: 'http://localhost:8080',
custom: setSelectedNetwork,
},
}}
createClient={(name, { custom, ...config }) => {
custom(name);
return new SuiClient(config);
}}
>
<div>{`selected network: ${selectedNetwork}`}</div>
<NetworkSelector />
</SuiClientProvider>
);
}

const user = userEvent.setup();

render(<CustomConfigProvider />);

expect(screen.getByText('selected network: a')).toBeInTheDocument();

await user.click(screen.getByText('select b'));

expect(screen.getByText('selected network: b')).toBeInTheDocument();
});
});
3 changes: 3 additions & 0 deletions sdk/dapp-kit/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import '@testing-library/jest-dom';
6 changes: 1 addition & 5 deletions sdk/dapp-kit/test/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { SuiClientProvider } from 'dapp-kit/src';

export function createSuiClientContextWrapper(client: SuiClient) {
return function SuiClientContextWrapper({ children }: { children: React.ReactNode }) {
return (
<SuiClientProvider queryKeyPrefix="devnet" client={client}>
{children}
</SuiClientProvider>
);
return <SuiClientProvider networks={{ test: client }}>{children}</SuiClientProvider>;
};
}
1 change: 1 addition & 0 deletions sdk/dapp-kit/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default defineConfig({
environment: 'happy-dom',
restoreMocks: true,
globals: true,
setupFiles: ['./test/setup.ts'],
},
resolve: {
conditions: ['source'],
Expand Down
15 changes: 15 additions & 0 deletions sdk/typescript/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,23 @@ export type NetworkOrTransport =
url?: never;
};

export const SUI_CLIENT_BRAND = Symbol.for('@mysten/SuiClient');

export function isSuiClient(client: unknown): client is SuiClient {
return (
typeof client === 'object' &&
client !== null &&
(client as { [SUI_CLIENT_BRAND]: unknown })[SUI_CLIENT_BRAND] === true
);
}

export class SuiClient {
protected transport: SuiTransport;

get [SUI_CLIENT_BRAND]() {
return true;
}

/**
* Establish a connection to a Sui RPC endpoint
*
Expand Down

6 comments on commit 65ecb84

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

sui-typescript-docs – ./sdk/docs

sui-typescript-docs-mysten-labs.vercel.app
sui-typescript-docs-git-main-mysten-labs.vercel.app
sui-typescript-docs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

sui-kiosk – ./dapps/kiosk

sui-kiosk-git-main-mysten-labs.vercel.app
sui-kiosk.vercel.app
sui-kiosk-mysten-labs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

mysten-ui – ./apps/ui

mysten-ui-git-main-mysten-labs.vercel.app
mysten-ui-mysten-labs.vercel.app
mysten-ui.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 65ecb84 Aug 14, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

sui-wallet-kit – ./sdk/wallet-adapter/site

sui-wallet-kit-mysten-labs.vercel.app
sui-wallet-kit.vercel.app
sui-wallet-kit-git-main-mysten-labs.vercel.app

Please sign in to comment.