Skip to content
Merged
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
47 changes: 47 additions & 0 deletions .changeset/eleven-chicken-smile.md
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.
28 changes: 28 additions & 0 deletions packages/thirdweb/scripts/wallets/extra-wallets.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,33 @@
"native": null,
"universal": null
}
},
{
"id": "abstract",
"name": "Abstract Global Wallet",
"homepage": "https://abs.xyz/",
"image_id": "abstract.png",
"app": {
"browser": null,
"ios": null,
"android": null,
"mac": null,
"windows": null,
"linux": null,
"chrome": null,
"firefox": null,
"safari": null,
"edge": null,
"opera": null
},
"rdns": "xyz.abs",
"mobile": {
"native": null,
"universal": null
},
"desktop": {
"native": null,
"universal": null
}
}
]
138 changes: 138 additions & 0 deletions packages/thirdweb/src/adapters/eip1193/from-eip1193.test.ts
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();
});
});
171 changes: 171 additions & 0 deletions packages/thirdweb/src/adapters/eip1193/from-eip1193.ts
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;

Check warning on line 75 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L75

Added line #L75 was not covered by tests
});

function reset() {
account = undefined;
chain = undefined;
}

let handleDisconnect = async () => {};

const unsubscribeDisconnect = emitter.subscribe("disconnect", () => {
reset();
unsubscribeChain();
unsubscribeDisconnect();
});

emitter.subscribe("accountChanged", (_account) => {
account = _account;

Check warning on line 92 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L92

Added line #L92 was not covered by tests
});

let handleSwitchChain: (c: Chain) => Promise<void> = async (c) => {
await provider?.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: ox__Hex.fromNumber(c.id) }],
});
};

Check warning on line 100 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L96-L100

Added lines #L96 - L100 were not covered by tests

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,
});

Check warning on line 146 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L139-L146

Added lines #L139 - L146 were not covered by tests
// set the states
account = connectedAccount;
chain = connectedChain;
handleDisconnect = doDisconnect;
handleSwitchChain = doSwitchChain;
emitter.emit("onConnect", connectOptions);
trackConnect({
client: connectOptions.client,
walletType: id,
walletAddress: account.address,
});

Check warning on line 157 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L148-L157

Added lines #L148 - L157 were not covered by tests
// return account
return account;
},

Check warning on line 160 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L159-L160

Added lines #L159 - L160 were not covered by tests
disconnect: async () => {
reset();
await handleDisconnect();
emitter.emit("disconnect", undefined);
},
switchChain: async (c) => {
await handleSwitchChain(c);
emitter.emit("chainChanged", c);
},

Check warning on line 169 in packages/thirdweb/src/adapters/eip1193/from-eip1193.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/adapters/eip1193/from-eip1193.ts#L167-L169

Added lines #L167 - L169 were not covered by tests
};
}
Loading