diff --git a/.changeset/poor-ends-peel.md b/.changeset/poor-ends-peel.md new file mode 100644 index 0000000000..c3262eaf6c --- /dev/null +++ b/.changeset/poor-ends-peel.md @@ -0,0 +1,5 @@ +--- +"@rainbow-me/rainbowkit": patch +--- + +Made `getNonce` optional in `AuthenticationAdapter`. When the server generates the nonce alongside the SIWE message, you can now omit `getNonce` entirely—RainbowKit will skip the client-side nonce pre-fetch and call `createMessage` directly with `{ address, chainId }`. Adapters that already provide `getNonce` keep the existing behavior and continue to receive `nonce` in `createMessage` args. diff --git a/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx b/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx index dbc5655d83..3ef3ed8662 100644 --- a/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx +++ b/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx @@ -14,17 +14,33 @@ export type AuthenticationStatus = | 'unauthenticated' | 'authenticated'; -export interface AuthenticationAdapter { - getNonce: () => Promise; - createMessage: (args: { - nonce: string; - address: Address; - chainId: number; - }) => Promise | Message; +interface AuthenticationAdapterBase { verify: (args: { message: Message; signature: string }) => Promise; signOut: () => Promise; } +// When `getNonce` is omitted, RainbowKit skips client-side nonce pre-fetching +// and calls `createMessage` directly with `{ address, chainId }` +export type AuthenticationAdapter = + AuthenticationAdapterBase & + ( + | { + getNonce: () => Promise; + createMessage: (args: { + nonce: string; + address: Address; + chainId: number; + }) => Promise | Message; + } + | { + getNonce?: undefined; + createMessage: (args: { + address: Address; + chainId: number; + }) => Promise | Message; + } + ); + export interface AuthenticationConfig { adapter: AuthenticationAdapter; status: AuthenticationStatus; diff --git a/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx b/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx index 19cd92e609..0a5ba62ca5 100644 --- a/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx +++ b/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx @@ -32,19 +32,34 @@ const address = '0x0000000000000000000000000000000000000001' as Address; function renderSignIn({ createMessage, + getNonce = async () => 'nonce', }: { createMessage: (args: { - nonce: string; + nonce?: string; address: Address; chainId: number; }) => string | Promise; + // Pass `null` to omit `getNonce` entirely (server-side message creation). + getNonce?: (() => Promise) | null; }) { - const adapter = createAuthenticationAdapter({ - createMessage, - getNonce: async () => 'nonce', - signOut: async () => {}, - verify: async () => true, - }); + const adapter = createAuthenticationAdapter( + getNonce === null + ? { + createMessage, + signOut: async () => {}, + verify: async () => true, + } + : { + createMessage: createMessage as (args: { + nonce: string; + address: Address; + chainId: number; + }) => string | Promise, + getNonce, + signOut: async () => {}, + verify: async () => true, + }, + ); return render( { fireEvent.click(button); expect(createMessage).toHaveBeenCalledOnce(); + expect(createMessage).toHaveBeenCalledWith({ + address, + chainId: 1, + nonce: 'nonce', + }); await waitFor(() => expect(wagmiMocks.signMessageAsync).toHaveBeenCalledWith({ message: 'message', @@ -142,4 +162,30 @@ describe('SignIn', () => { expect(createMessage).toHaveBeenCalledTimes(2); }); + + it('enables sign-in immediately when getNonce is omitted', async () => { + const createMessage = vi.fn(() => 'message'); + wagmiMocks.signMessageAsync.mockReturnValue(new Promise(() => {})); + + renderSignIn({ createMessage, getNonce: null }); + + const button = await screen.findByTestId('rk-auth-message-button'); + expect(button).toHaveTextContent('Sign message'); + expect(button).not.toBeDisabled(); + + fireEvent.click(button); + + expect(createMessage).toHaveBeenCalledOnce(); + // No nonce should be passed to createMessage when getNonce is omitted. + expect(createMessage).toHaveBeenCalledWith({ + address, + chainId: 1, + }); + + await waitFor(() => + expect(wagmiMocks.signMessageAsync).toHaveBeenCalledWith({ + message: 'message', + }), + ); + }); }); diff --git a/packages/rainbowkit/src/components/SignIn/SignIn.tsx b/packages/rainbowkit/src/components/SignIn/SignIn.tsx index 993ced248a..ce82eef184 100644 --- a/packages/rainbowkit/src/components/SignIn/SignIn.tsx +++ b/packages/rainbowkit/src/components/SignIn/SignIn.tsx @@ -30,6 +30,8 @@ export function SignIn({ const authAdapter = useAuthenticationAdapter(); const getNonce = useCallback(async () => { + if (!authAdapter.getNonce) return; + try { const nonce = await authAdapter.getNonce(); setState((x) => ({ ...x, nonce })); @@ -62,9 +64,10 @@ export function SignIn({ const chainId = activeChain?.id; const { nonce } = state; - if (!address || !chainId || !nonce) { - return; - } + if (!address || !chainId) return; + + // Block when the adapter expects a pre-fetched nonce that hasn't arrived. + if (authAdapter.getNonce && !nonce) return; setState((x) => ({ ...x, @@ -73,11 +76,18 @@ export function SignIn({ let message: Awaited>; try { - const messageResult = authAdapter.createMessage({ - address, - chainId, - nonce, - }); + let messageResult: ReturnType; + if (authAdapter.getNonce) { + // This isn't reachable in practice, but for type safety. + if (!nonce) return; + messageResult = authAdapter.createMessage({ + address, + chainId, + nonce, + }); + } else { + messageResult = authAdapter.createMessage({ address, chainId }); + } if (messageResult instanceof Promise) { setState((x) => ({ ...x, status: 'creatingMessage' })); @@ -223,13 +233,14 @@ export function SignIn({ > { const response = await fetch('/api/siwe/message', { method: 'POST', @@ -71,8 +74,19 @@ const authenticationAdapter = createAuthenticationAdapter({ return await response.text(); }, + + verify: async ({ message, signature }) => { + /* ... */ + }, + + signOut: async () => { + /* ... */ + }, +}); ``` +If you still want the client to fetch a nonce separately (for example, to keep the existing `/api/nonce` endpoint), keep `getNonce` and continue to receive `nonce` in `createMessage` args. + #### Providing the authentication state Assuming your application is already managing the authentication lifecycle in some way, you can pass the current authentication status along with your custom adapter to `RainbowKitAuthenticationProvider`, wrapping your existing `RainbowKitProvider`.