Skip to content
Open
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
13 changes: 11 additions & 2 deletions examples/next-wagmi-app-router/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Metadata } from 'next'
import { headers } from 'next/headers'

import { wagmiAdapter } from '@/config'
import ContextProvider from '@/context'

import './globals.css'
Expand Down Expand Up @@ -33,11 +35,18 @@ export const metadata: Metadata = {
}
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookies = (await headers()).get('cookie')

// Reset SSR state when no cookies to prevent cross-request state leakage
if (!cookies) {
wagmiAdapter.resetSSRState()
}

return (
<html lang="en">
<body>
<ContextProvider cookies={null}>{children}</ContextProvider>
<ContextProvider cookies={cookies}>{children}</ContextProvider>
</body>
</html>
)
Expand Down
3 changes: 2 additions & 1 deletion examples/next-wagmi-app-router/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const networks = [mainnet, polygon, arbitrum, optimism] as [
// Setup wagmi adapter
export const wagmiAdapter = new WagmiAdapter({
networks,
projectId
projectId,
ssr: true
})

// Create modal
Expand Down
40 changes: 40 additions & 0 deletions packages/adapters/wagmi/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,46 @@ export class WagmiAdapter extends AdapterBlueprint {
this.setupWatchers()
}

/**
* Resets the wagmi config state for SSR (Server-Side Rendering).
*
* In Next.js App Router with SSR, the WagmiAdapter is typically created as a module-level
* singleton. This can cause cross-request state leakage where one user's wallet connection
* state bleeds into another user's server-rendered HTML.
*
* Call this method before each SSR request when `cookieToInitialState` returns undefined
* (i.e., when the user has no wallet cookies) to reset the config state and prevent
* showing another user's wallet address.
*
* @example
* ```tsx
* // In your layout.tsx (server component)
* const cookies = (await headers()).get('cookie')
* const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig, cookies)
*
* // Reset state if no cookies to prevent state leakage
* if (!initialState) {
* wagmiAdapter.resetSSRState()
* }
* ```
*/
public resetSSRState(): void {
if (!this.wagmiConfig) {
return
}

// Reset the wagmi config state to disconnected
this.wagmiConfig.setState(state => ({
...state,
connections: new Map(),
current: null,
status: 'disconnected'
}))

// Clear the connections map
this.wagmiConfig.state.connections.clear()
}

override async getAccounts(
params: AdapterBlueprint.GetAccountsParams
): Promise<AdapterBlueprint.GetAccountsResult> {
Expand Down
102 changes: 102 additions & 0 deletions packages/adapters/wagmi/src/tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1734,3 +1734,105 @@ describe('WagmiAdapter - BaseAccount lazy initialization', () => {
expect(result.provider).toBe(providedProvider as any)
})
})

describe('WagmiAdapter - resetSSRState', () => {
it('should reset wagmi config state to disconnected', () => {
const mockConnections = new Map([
['connector1', { connector: { id: 'connector1' }, accounts: ['0x123'] }]
])

const setStateSpy = vi.fn((fn: (state: any) => any) => {
const newState = fn({
connections: mockConnections,
current: 'connector1',
status: 'connected'
})
expect(newState.connections).toBeInstanceOf(Map)
expect(newState.connections.size).toBe(0)
expect(newState.current).toBeNull()
expect(newState.status).toBe('disconnected')
})

const mockConfig = {
chains: mockCaipNetworks,
connectors: [],
state: {
connections: mockConnections
},
setState: setStateSpy,
_internal: {
connectors: {
setup: vi.fn(connector => connector),
setState: vi.fn()
}
}
} as unknown as Config

vi.spyOn(wagmiCore, 'createConfig').mockReturnValue(mockConfig)

const adapter = new WagmiAdapter({
networks: mockNetworks,
projectId: mockProjectId,
ssr: true
})

adapter.wagmiConfig = mockConfig

adapter.resetSSRState()

expect(setStateSpy).toHaveBeenCalled()
expect(mockConnections.size).toBe(0)
})

it('should return early when wagmiConfig is not set', () => {
const adapter = new WagmiAdapter({
networks: mockNetworks,
projectId: mockProjectId,
ssr: true
})

// Set wagmiConfig to undefined to test early return
adapter.wagmiConfig = undefined as unknown as Config

// This should not throw
expect(() => adapter.resetSSRState()).not.toThrow()
})

it('should clear connections map after setState', () => {
const mockConnections = new Map([
['connector1', { connector: { id: 'connector1' }, accounts: ['0x123'] }],
['connector2', { connector: { id: 'connector2' }, accounts: ['0x456'] }]
])

const mockConfig = {
chains: mockCaipNetworks,
connectors: [],
state: {
connections: mockConnections
},
setState: vi.fn(),
_internal: {
connectors: {
setup: vi.fn(connector => connector),
setState: vi.fn()
}
}
} as unknown as Config

vi.spyOn(wagmiCore, 'createConfig').mockReturnValue(mockConfig)

const adapter = new WagmiAdapter({
networks: mockNetworks,
projectId: mockProjectId,
ssr: true
})

adapter.wagmiConfig = mockConfig

expect(mockConnections.size).toBe(2)

adapter.resetSSRState()

expect(mockConnections.size).toBe(0)
})
})
Loading