-
Notifications
You must be signed in to change notification settings - Fork 619
[SDK] Feature: Adds EIP-1193 Adapter #5354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6e69eee
abstract wallet support
joaquim-verges a12a0cb
feat(sdk/eip1193): emit events
gregfromstl d16fb21
Merge branch 'main' into 11-06-abstract_wallet_support
gregfromstl 8233b77
feat(sdk/eip1193): add switch chain handler
gregfromstl 6730c9c
test: Add test suite for fromProvider in EIP-1193 adapter
gregfromstl 87f49f2
test: Add comprehensive test suite for toProvider function
gregfromstl 418a83d
fix(sdk/eip1193): remove incorrect jsdoc
gregfromstl 6f62d1b
docs: Add comprehensive JSDoc for `toProvider` EIP-1193 adapter
gregfromstl 669071d
docs: Add comprehensive JSDoc for EIP-1193 provider adapter function
gregfromstl 1b5a98f
Merge branch 'main' into 11-06-abstract_wallet_support
gregfromstl ed14777
chore(sdk): adds eip1193 changeset
gregfromstl a7dc471
docs(sdk/eip1193): recommend named imports
gregfromstl 8460f6a
feat: Add comprehensive EIP1193 adapters with conversion and provider…
gregfromstl c044224
fix(sdk/eip1193): failing test
gregfromstl a7c5bb0
Merge branch 'main' into 11-06-abstract_wallet_support
gregfromstl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --- | ||
| "thirdweb": minor | ||
| --- | ||
|
|
||
| Adds EIP1193 adapters that allow conversion between Thirdweb wallets and EIP-1193 providers: | ||
|
|
||
| - `EIP1193.fromProvider()`: Creates a Thirdweb wallet from any EIP-1193 compatible provider (like MetaMask, WalletConnect) | ||
| - `EIP1193.toProvider()`: Converts a Thirdweb wallet into an EIP-1193 provider that can be used with any web3 library | ||
|
|
||
| Key features: | ||
| - Full EIP-1193 compliance for seamless integration | ||
| - Handles account management (connect, disconnect, chain switching) | ||
| - Supports all standard Ethereum JSON-RPC methods | ||
| - Comprehensive event system for state changes | ||
| - Type-safe interfaces with full TypeScript support | ||
|
|
||
| Examples: | ||
|
|
||
| ```ts | ||
| // Convert MetaMask's provider to a Thirdweb wallet | ||
| const wallet = EIP1193.fromProvider({ | ||
| provider: window.ethereum, | ||
| walletId: "io.metamask" | ||
| }); | ||
|
|
||
| // Use like any other Thirdweb wallet | ||
| const account = await wallet.connect({ | ||
| client: createThirdwebClient({ clientId: "..." }) | ||
| }); | ||
|
|
||
| // Convert a Thirdweb wallet to an EIP-1193 provider | ||
| const provider = EIP1193.toProvider({ | ||
| wallet, | ||
| chain: ethereum, | ||
| client: createThirdwebClient({ clientId: "..." }) | ||
| }); | ||
|
|
||
| // Use with any EIP-1193 compatible library | ||
| const accounts = await provider.request({ | ||
| method: "eth_requestAccounts" | ||
| }); | ||
|
|
||
| // Listen for events | ||
| provider.on("accountsChanged", (accounts) => { | ||
| console.log("Active accounts:", accounts); | ||
| }); | ||
| ``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
packages/thirdweb/src/adapters/eip1193/from-eip1193.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { describe, expect, test, vi } from "vitest"; | ||
| import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; | ||
| import { ANVIL_CHAIN } from "../../../test/src/chains.js"; | ||
| import { TEST_CLIENT } from "../../../test/src/test-clients.js"; | ||
| import { trackConnect } from "../../analytics/track/connect.js"; | ||
| import { fromProvider } from "./from-eip1193.js"; | ||
| import type { EIP1193Provider } from "./types.js"; | ||
|
|
||
| vi.mock("../../analytics/track/connect.js"); | ||
|
|
||
| describe("fromProvider", () => { | ||
| const mockProvider: EIP1193Provider = { | ||
| on: vi.fn(), | ||
| removeListener: vi.fn(), | ||
| request: vi.fn(), | ||
| }; | ||
|
|
||
| const mockAccount = TEST_ACCOUNT_A; | ||
|
|
||
| test("should create a wallet with the correct properties", () => { | ||
| const wallet = fromProvider({ | ||
| provider: mockProvider, | ||
| walletId: "io.metamask", | ||
| }); | ||
|
|
||
| expect(wallet.id).toBe("io.metamask"); | ||
| expect(wallet.subscribe).toBeDefined(); | ||
| expect(wallet.connect).toBeDefined(); | ||
| expect(wallet.disconnect).toBeDefined(); | ||
| expect(wallet.getAccount).toBeDefined(); | ||
| expect(wallet.getChain).toBeDefined(); | ||
| expect(wallet.getConfig).toBeDefined(); | ||
| expect(wallet.switchChain).toBeDefined(); | ||
| }); | ||
|
|
||
| test("should use 'adapter' as default walletId", () => { | ||
| const wallet = fromProvider({ | ||
| provider: mockProvider, | ||
| }); | ||
|
|
||
| expect(wallet.id).toBe("adapter"); | ||
| }); | ||
|
|
||
| test("should handle async provider function", async () => { | ||
| const wallet = fromProvider({ | ||
| provider: async () => | ||
| Promise.resolve({ | ||
| ...mockProvider, | ||
| request: () => Promise.resolve([mockAccount.address]), | ||
| }), | ||
| }); | ||
|
|
||
| // Connect to trigger provider initialization | ||
| await wallet.connect({ | ||
| client: TEST_CLIENT, | ||
| }); | ||
|
|
||
| expect(wallet.getAccount()?.address).toBe(mockAccount.address); | ||
| }); | ||
|
|
||
| test("should emit events on connect", async () => { | ||
| const wallet = fromProvider({ | ||
| provider: { | ||
| ...mockProvider, | ||
| request: () => Promise.resolve([mockAccount.address]), | ||
| }, | ||
| }); | ||
|
|
||
| const onConnectSpy = vi.fn(); | ||
| wallet.subscribe("onConnect", onConnectSpy); | ||
|
|
||
| await wallet.connect({ | ||
| client: TEST_CLIENT, | ||
| chain: ANVIL_CHAIN, | ||
| }); | ||
|
|
||
| expect(onConnectSpy).toHaveBeenCalled(); | ||
| expect(trackConnect).toHaveBeenCalledWith({ | ||
| client: TEST_CLIENT, | ||
| walletType: "adapter", | ||
| walletAddress: mockAccount.address, | ||
| }); | ||
| }); | ||
|
|
||
| test("should emit events on disconnect", async () => { | ||
| const wallet = fromProvider({ | ||
| provider: mockProvider, | ||
| }); | ||
|
|
||
| const onDisconnectSpy = vi.fn(); | ||
| wallet.subscribe("disconnect", onDisconnectSpy); | ||
|
|
||
| await wallet.disconnect(); | ||
|
|
||
| expect(onDisconnectSpy).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("should handle chain changes", async () => { | ||
| const wallet = fromProvider({ | ||
| provider: { | ||
| ...mockProvider, | ||
| request: () => Promise.resolve([mockAccount.address]), | ||
| }, | ||
| }); | ||
|
|
||
| const onChainChangedSpy = vi.fn(); | ||
| wallet.subscribe("chainChanged", onChainChangedSpy); | ||
|
|
||
| await wallet.connect({ | ||
| client: TEST_CLIENT, | ||
| chain: ANVIL_CHAIN, | ||
| }); | ||
|
|
||
| const chain = wallet.getChain(); | ||
| expect(chain).toBe(ANVIL_CHAIN); | ||
| }); | ||
|
|
||
| test("should reset state on disconnect", async () => { | ||
| const wallet = fromProvider({ | ||
| provider: { | ||
| ...mockProvider, | ||
| request: () => Promise.resolve([mockAccount.address]), | ||
| }, | ||
| }); | ||
|
|
||
| mockProvider.request = vi.fn().mockResolvedValueOnce([mockAccount.address]); | ||
|
|
||
| await wallet.connect({ | ||
| client: TEST_CLIENT, | ||
| chain: ANVIL_CHAIN, | ||
| }); | ||
|
|
||
| await wallet.disconnect(); | ||
|
|
||
| expect(wallet.getAccount()).toBeUndefined(); | ||
| expect(wallet.getChain()).toBeUndefined(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import * as ox__Hex from "ox/Hex"; | ||
| import { trackConnect } from "../../analytics/track/connect.js"; | ||
| import type { Chain } from "../../chains/types.js"; | ||
| import { getCachedChainIfExists } from "../../chains/utils.js"; | ||
| import { | ||
| autoConnectEip1193Wallet, | ||
| connectEip1193Wallet, | ||
| } from "../../wallets/injected/index.js"; | ||
| import type { Account, Wallet } from "../../wallets/interfaces/wallet.js"; | ||
| import { createWalletEmitter } from "../../wallets/wallet-emitter.js"; | ||
| import type { WalletId } from "../../wallets/wallet-types.js"; | ||
| import type { EIP1193Provider } from "./types.js"; | ||
|
|
||
| /** | ||
| * Options for creating an EIP-1193 provider adapter. | ||
| */ | ||
| export type FromEip1193AdapterOptions = { | ||
| provider: EIP1193Provider | (() => Promise<EIP1193Provider>); | ||
| walletId?: WalletId; | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a Thirdweb wallet from an EIP-1193 compatible provider. | ||
| * | ||
| * This adapter allows you to use any EIP-1193 provider (like MetaMask, WalletConnect, etc.) as a Thirdweb wallet. | ||
| * It handles all the necessary conversions between the EIP-1193 interface and Thirdweb's wallet interface. | ||
| * | ||
| * @param options - Configuration options for creating the wallet adapter | ||
| * @param options.provider - An EIP-1193 compatible provider or a function that returns one | ||
| * @param options.walletId - Optional custom wallet ID to identify this provider (defaults to "adapter") | ||
| * @returns A Thirdweb wallet instance that wraps the EIP-1193 provider | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { EIP1193 } from "thirdweb/wallets"; | ||
| * | ||
| * // Create a Thirdweb wallet from MetaMask's provider | ||
| * const wallet = EIP1193.fromProvider({ | ||
| * provider: window.ethereum, | ||
| * walletId: "io.metamask" | ||
| * }); | ||
| * | ||
| * // Use like any other Thirdweb wallet | ||
| * const account = await wallet.connect({ | ||
| * client: createThirdwebClient({ clientId: "..." }) | ||
| * }); | ||
| * | ||
| * // Sign messages | ||
| * await account.signMessage({ message: "Hello World" }); | ||
| * | ||
| * // Send transactions | ||
| * await account.sendTransaction({ | ||
| * to: "0x...", | ||
| * value: 1000000000000000000n | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function fromProvider(options: FromEip1193AdapterOptions): Wallet { | ||
| const id: WalletId = options.walletId ?? "adapter"; | ||
| const emitter = createWalletEmitter(); | ||
| let account: Account | undefined = undefined; | ||
| let chain: Chain | undefined = undefined; | ||
| let provider: EIP1193Provider | undefined = undefined; | ||
| const getProvider = async () => { | ||
| if (!provider) { | ||
| provider = | ||
| typeof options.provider === "function" | ||
| ? await options.provider() | ||
| : options.provider; | ||
| } | ||
| return provider; | ||
| }; | ||
|
|
||
| const unsubscribeChain = emitter.subscribe("chainChanged", (newChain) => { | ||
| chain = newChain; | ||
| }); | ||
|
|
||
| function reset() { | ||
| account = undefined; | ||
| chain = undefined; | ||
| } | ||
|
|
||
| let handleDisconnect = async () => {}; | ||
|
|
||
| const unsubscribeDisconnect = emitter.subscribe("disconnect", () => { | ||
| reset(); | ||
| unsubscribeChain(); | ||
| unsubscribeDisconnect(); | ||
| }); | ||
|
|
||
| emitter.subscribe("accountChanged", (_account) => { | ||
| account = _account; | ||
| }); | ||
|
|
||
| let handleSwitchChain: (c: Chain) => Promise<void> = async (c) => { | ||
| await provider?.request({ | ||
| method: "wallet_switchEthereumChain", | ||
| params: [{ chainId: ox__Hex.fromNumber(c.id) }], | ||
| }); | ||
| }; | ||
|
|
||
| return { | ||
| id, | ||
| subscribe: emitter.subscribe, | ||
| getConfig: () => undefined, | ||
| getChain() { | ||
| if (!chain) { | ||
| return undefined; | ||
| } | ||
|
|
||
| chain = getCachedChainIfExists(chain.id) || chain; | ||
| return chain; | ||
| }, | ||
| getAccount: () => account, | ||
| connect: async (connectOptions) => { | ||
| const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] = | ||
| await connectEip1193Wallet({ | ||
| id, | ||
| provider: await getProvider(), | ||
| client: connectOptions.client, | ||
| chain: connectOptions.chain, | ||
| emitter, | ||
| }); | ||
| // set the states | ||
| account = connectedAccount; | ||
| chain = connectedChain; | ||
| handleDisconnect = doDisconnect; | ||
| handleSwitchChain = doSwitchChain; | ||
| emitter.emit("onConnect", connectOptions); | ||
| trackConnect({ | ||
| client: connectOptions.client, | ||
| walletType: id, | ||
| walletAddress: account.address, | ||
| }); | ||
| // return account | ||
| return account; | ||
| }, | ||
| autoConnect: async (connectOptions) => { | ||
| const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] = | ||
| await autoConnectEip1193Wallet({ | ||
| id, | ||
| provider: await getProvider(), | ||
| emitter, | ||
| chain: connectOptions.chain, | ||
| client: connectOptions.client, | ||
| }); | ||
| // set the states | ||
| account = connectedAccount; | ||
| chain = connectedChain; | ||
| handleDisconnect = doDisconnect; | ||
| handleSwitchChain = doSwitchChain; | ||
| emitter.emit("onConnect", connectOptions); | ||
| trackConnect({ | ||
| client: connectOptions.client, | ||
| walletType: id, | ||
| walletAddress: account.address, | ||
| }); | ||
| // return account | ||
| return account; | ||
| }, | ||
| disconnect: async () => { | ||
| reset(); | ||
| await handleDisconnect(); | ||
| emitter.emit("disconnect", undefined); | ||
| }, | ||
| switchChain: async (c) => { | ||
| await handleSwitchChain(c); | ||
| emitter.emit("chainChanged", c); | ||
| }, | ||
| }; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.