Skip to content

Commit

Permalink
feat: useChainId hook (#47)
Browse files Browse the repository at this point in the history
- Get current chain ID from context
- Optionally assert current chain ID using allowlist and/or denylist
  • Loading branch information
tien authored Jul 24, 2024
1 parent c8dfafa commit 435791b
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 19 deletions.
8 changes: 8 additions & 0 deletions .changeset/friendly-fireants-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@reactive-dot/react": minor
---

Add `useChainId` hook.

- Get current chain ID from context
- Optionally assert current chain ID using allowlist and/or denylist
70 changes: 70 additions & 0 deletions apps/docs/docs/getting-started/multichain.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,73 @@ function Component() {
const westendBlock = useBlock({ chainId: "westend" });
}
```

## Chain narrowing

By default, Reactive DOT provides type definitions based on the merged definitions of all chains in the config. For example, if your DApp is set up to be used with Polkadot, Kusama, and Westend, the following code will not work because the Bounties pallet only exists on Polkadot and Kusama, not on Westend:

```tsx
function Component() {
// Since `Bounties` pallet doesn't exist on Westend, this will:
// 1. Raise a TypeScript error
// 2. Throw an error during runtime if Westend is selected
const bountyCount = useLazyLoadQuery((builder) =>
builder.readStorage("Bounties", "BountyCount", []),
);

// ...
}
```

You have the option of either explicitly specifying the chain to query, which will override the chain ID provided via context:

```tsx
function Component() {
const bountyCount = useLazyLoadQuery(
(builder) => builder.readStorage("Bounties", "BountyCount", []),
{ chainId: "polkadot" },
);

// ...
}
```

Or, to continue using the chain ID provided via context, you can use the [`useChainId`](/api/react/function/useChainId) hook along with its allowlist/denylist functionality:

```tsx
function BountiesPalletRequiredComponent() {
const bountyCount = useLazyLoadQuery(
(builder) => builder.readStorage("Bounties", "BountyCount", []),
{
// `useChainId` with the allow/deny list will:
// 1. Throw an error if the context's chain ID conflicts with the list(s)
// 2. Restrict descriptors used by `useLazyLoadQuery` to provide correct intellisense
chainId: useChainId({
allowlist: ["polkadot", "kusama"],
// Or
denylist: ["westend"],
}),
},
);

// ...
}

function App() {
// ...

// Only use compatible chain IDs, else an error will be thrown
const bountiesEnabledChainIds = ["polkadot", "kusama"] satisfies ChainId[];

return (
<div>
{bountiesEnabledChainIds.map((chainId) => (
<ReDotChainProvider key={chainId} chainId={chainId}>
<BountiesPalletRequiredComponent />
</ReDotChainProvider>
))}
{/* ... */}
</div>
);
}
```
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-accounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { accountsAtom } from "../stores/accounts.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import { useAtomValue } from "jotai";

/**
Expand All @@ -10,5 +10,5 @@ import { useAtomValue } from "jotai";
* @returns The currently connected accounts
*/
export function useAccounts(options?: ChainHookOptions) {
return useAtomValue(accountsAtom(useChainId(options)));
return useAtomValue(accountsAtom(useChainId_INTERNAL(options)));
}
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
finalizedBlockAtomFamily,
} from "../stores/block.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import { useAtomValue } from "jotai";

/**
Expand All @@ -17,7 +17,7 @@ export function useBlock(
tag: "best" | "finalized" = "finalized",
options?: ChainHookOptions,
) {
const chainId = useChainId(options);
const chainId = useChainId_INTERNAL(options);

return useAtomValue(
tag === "finalized"
Expand Down
36 changes: 33 additions & 3 deletions packages/react/src/hooks/use-chain-id.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
import { ChainIdContext } from "../contexts/index.js";
import type { ChainHookOptions } from "./types.js";
import { ReDotError } from "@reactive-dot/core";
import { type ChainId, ReDotError } from "@reactive-dot/core";
import { useContext } from "react";

export function useChainId(options?: ChainHookOptions) {
/**
* Hook for getting the current chain ID.
*
* @param options - Additional options
* @returns
*/
export function useChainId<
const TAllowList extends ChainId[],
const TDenylist extends ChainId[] = [],
>(options?: { allowlist?: TAllowList; denylist?: TDenylist }) {
const chainId = useContext(ChainIdContext);

if (chainId === undefined) {
throw new ReDotError("No chain ID provided");
}

if (options?.allowlist?.includes(chainId) === false) {
throw new ReDotError("Chain ID not allowed", { cause: chainId });
}

if (options?.denylist?.includes(chainId)) {
throw new ReDotError("Chain ID denied", { cause: chainId });
}

return chainId as Exclude<
Extract<ChainId, TAllowList[number]>,
TDenylist[number]
>;
}

export function useChainId_INTERNAL(options?: ChainHookOptions) {
const contextChainId = useContext(ChainIdContext);
const chainId = options?.chainId ?? contextChainId;

if (chainId === undefined) {
throw new ReDotError("No chain Id provided");
throw new ReDotError("No chain ID provided");
}

return chainId;
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-chain-spec-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { chainSpecDataAtomFamily } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import { useAtomValue } from "jotai";

/**
Expand All @@ -10,5 +10,5 @@ import { useAtomValue } from "jotai";
* @returns The [JSON-RPC spec](https://paritytech.github.io/json-rpc-interface-spec/api/chainSpec.html)
*/
export function useChainSpecData(options?: ChainHookOptions) {
return useAtomValue(chainSpecDataAtomFamily(useChainId(options)));
return useAtomValue(chainSpecDataAtomFamily(useChainId_INTERNAL(options)));
}
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clientAtomFamily } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import { useAtomValue } from "jotai";

/**
Expand All @@ -10,5 +10,5 @@ import { useAtomValue } from "jotai";
* @returns Polkadot-API client
*/
export function useClient(options?: ChainHookOptions) {
return useAtomValue(clientAtomFamily(useChainId(options)));
return useAtomValue(clientAtomFamily(useChainId_INTERNAL(options)));
}
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { typedApiAtomFamily } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useAsyncState } from "./use-async-state.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import type { ChainId, Chains } from "@reactive-dot/core";
import { MutationError, PENDING } from "@reactive-dot/core";
import { useAtomCallback } from "jotai/utils";
Expand Down Expand Up @@ -54,7 +54,7 @@ export function useMutation<
txOptions?: TxOptions<ReturnType<TAction>>;
},
) {
const chainId = useChainId(options);
const chainId = useChainId_INTERNAL(options);
const mutationEventSubject = useContext(MutationEventSubjectContext);
const contextSigner = useContext(SignerContext);

Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
import type { Falsy, FalsyGuard, FlatHead } from "../types.js";
import { flatHead, stringify } from "../utils/vanilla.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import {
IDLE,
Query,
Expand Down Expand Up @@ -37,7 +37,7 @@ export function useQueryRefresher<
: Chains[TChainId],
TChainId extends ChainId,
>(builder: TQuery, options?: ChainHookOptions<TChainId>) {
const chainId = useChainId(options);
const chainId = useChainId_INTERNAL(options);

const refresh = useAtomCallback(
useCallback(
Expand Down Expand Up @@ -99,7 +99,7 @@ export function useLazyLoadQueryWithRefresh<
>,
refresh: () => void,
] {
const chainId = useChainId(options);
const chainId = useChainId_INTERNAL(options);

const query = useMemo(
() => (!builder ? undefined : builder(new Query([]))),
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-typed-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { typedApiAtomFamily } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useChainId } from "./use-chain-id.js";
import { useChainId_INTERNAL } from "./use-chain-id.js";
import type { ChainId, Chains } from "@reactive-dot/core";
import { useAtomValue } from "jotai";
import type { TypedApi } from "polkadot-api";
Expand All @@ -14,5 +14,5 @@ import type { TypedApi } from "polkadot-api";
export function useTypedApi<TChainId extends ChainId>(
options?: ChainHookOptions<TChainId>,
): TypedApi<Chains[TChainId]> {
return useAtomValue(typedApiAtomFamily(useChainId(options)));
return useAtomValue(typedApiAtomFamily(useChainId_INTERNAL(options)));
}
3 changes: 2 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ export {
export type { ChainHookOptions } from "./hooks/types.js";
export { useAccounts } from "./hooks/use-accounts.js";
export { useBlock } from "./hooks/use-block.js";
export { useChainId } from "./hooks/use-chain-id.js";
export { useChainSpecData } from "./hooks/use-chain-spec-data.js";
export { useClient } from "./hooks/use-client.js";
export { useConnectWallet } from "./hooks/use-connect-wallet.js";
export { useDisconnectWallet } from "./hooks/use-disconnect-wallet.js";
export { useMutation } from "./hooks/use-mutation.js";
export { useMutationEffect } from "./hooks/use-mutation-effect.js";
export { useMutation } from "./hooks/use-mutation.js";
export {
useLazyLoadQuery,
useLazyLoadQueryWithRefresh,
Expand Down

0 comments on commit 435791b

Please sign in to comment.