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
72 changes: 72 additions & 0 deletions src/sdk/account/utils/toWalletClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { http, type Hex, createWalletClient } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { mainnet } from "viem/chains"
import { afterEach, describe, expect, it } from "vitest"
import { toSigner } from "./toSigner"
import { toWalletClient } from "./toWalletClient"

// Regression coverage for https://github.com/bcnmy/abstractjs/issues/212
//
// toWalletClient used to unconditionally build `custom(window?.ethereum)` when
// the unresolved signer's transport key was "custom". When a user connects
// without a wallet extension (e.g. a 3rd-party email / web2 connector),
// `window.ethereum` is undefined, and `custom(undefined)` produced a transport
// that threw "Cannot read properties of undefined (reading 'request')".
describe("utils.toWalletClient", () => {
const account = privateKeyToAccount(
"0x1234567890123456789012345678901234567890123456789012345678901234" as Hex
)

afterEach(() => {
// Clean up any injected provider stub between cases.
if (typeof window !== "undefined") {
// biome-ignore lint/performance/noDelete: test cleanup
delete (window as { ethereum?: unknown }).ethereum
}
})

it("does not throw when the signer is 'custom' but no injected provider exists", async () => {
const resolvedSigner = await toSigner({ signer: account })
// A wallet client whose transport key is "custom" simulates a browser signer.
const unresolvedSigner = createWalletClient({
account,
chain: mainnet,
transport: http()
})
// Force the "custom" transport key that selects the browser branch, while
// leaving window.ethereum undefined (no extension installed).
;(unresolvedSigner as { transport: { key: string } }).transport.key =
"custom"

expect(() =>
toWalletClient({
unresolvedSigner: unresolvedSigner as never,
resolvedSigner,
chain: mainnet,
transport: http()
})
).not.toThrow()
})

it("falls back to the resolved signer account when there is no injected provider", async () => {
const resolvedSigner = await toSigner({ signer: account })
const unresolvedSigner = createWalletClient({
account,
chain: mainnet,
transport: http()
})
;(unresolvedSigner as { transport: { key: string } }).transport.key =
"custom"

const walletClient = toWalletClient({
unresolvedSigner: unresolvedSigner as never,
resolvedSigner,
chain: mainnet,
transport: http()
})

// Without window.ethereum the account is the full local signer, not just an
// address routed through a (missing) injected provider.
expect(walletClient.account.address).toBe(account.address)
})
})
19 changes: 16 additions & 3 deletions src/sdk/account/utils/toWalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,27 @@ export const toWalletClient = ({
chain,
transport
}: ToWalletClientParameters): ToWalletClientReturnType => {
const browserSigner = unresolvedSigner?.transport?.key === "custom"
// Only route through the injected browser provider when the signer was created
// from one (transport key "custom") AND an EIP-1193 provider is actually
// present on `window.ethereum`. When a user connects without a wallet
// extension (e.g. via a 3rd-party email / web2 connector), `window.ethereum`
// is undefined; `custom(undefined)` previously produced a transport that threw
// "Cannot read properties of undefined (reading 'request')" from
// toNexusAccount. In that case fall back to the resolved (local) signer and
// the provided transport. See https://github.com/bcnmy/abstractjs/issues/212
const injectedProvider =
typeof window !== "undefined"
? (window as { ethereum?: unknown }).ethereum
: undefined
const useBrowserProvider =
unresolvedSigner?.transport?.key === "custom" && Boolean(injectedProvider)
return createWalletClient(
browserSigner
useBrowserProvider
? {
account: resolvedSigner.address,
chain,
// @ts-ignore
transport: custom(window?.ethereum)
transport: custom(injectedProvider)
}
: {
account: resolvedSigner,
Expand Down