Skip to content

Commit a1fc436

Browse files
[SDK] Feature: Adds EIP-1193 Adapter (#5354)
Co-authored-by: gregfromstl <gregfromstl@gmail.com>
1 parent b30177c commit a1fc436

File tree

40 files changed

+1239
-170
lines changed

40 files changed

+1239
-170
lines changed

.changeset/eleven-chicken-smile.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Adds EIP1193 adapters that allow conversion between Thirdweb wallets and EIP-1193 providers:
6+
7+
- `EIP1193.fromProvider()`: Creates a Thirdweb wallet from any EIP-1193 compatible provider (like MetaMask, WalletConnect)
8+
- `EIP1193.toProvider()`: Converts a Thirdweb wallet into an EIP-1193 provider that can be used with any web3 library
9+
10+
Key features:
11+
- Full EIP-1193 compliance for seamless integration
12+
- Handles account management (connect, disconnect, chain switching)
13+
- Supports all standard Ethereum JSON-RPC methods
14+
- Comprehensive event system for state changes
15+
- Type-safe interfaces with full TypeScript support
16+
17+
Examples:
18+
19+
```ts
20+
// Convert MetaMask's provider to a Thirdweb wallet
21+
const wallet = EIP1193.fromProvider({
22+
provider: window.ethereum,
23+
walletId: "io.metamask"
24+
});
25+
26+
// Use like any other Thirdweb wallet
27+
const account = await wallet.connect({
28+
client: createThirdwebClient({ clientId: "..." })
29+
});
30+
31+
// Convert a Thirdweb wallet to an EIP-1193 provider
32+
const provider = EIP1193.toProvider({
33+
wallet,
34+
chain: ethereum,
35+
client: createThirdwebClient({ clientId: "..." })
36+
});
37+
38+
// Use with any EIP-1193 compatible library
39+
const accounts = await provider.request({
40+
method: "eth_requestAccounts"
41+
});
42+
43+
// Listen for events
44+
provider.on("accountsChanged", (accounts) => {
45+
console.log("Active accounts:", accounts);
46+
});
47+
```
Loading

packages/thirdweb/scripts/wallets/extra-wallets.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,33 @@
9191
"native": null,
9292
"universal": null
9393
}
94+
},
95+
{
96+
"id": "abstract",
97+
"name": "Abstract Global Wallet",
98+
"homepage": "https://abs.xyz/",
99+
"image_id": "abstract.png",
100+
"app": {
101+
"browser": null,
102+
"ios": null,
103+
"android": null,
104+
"mac": null,
105+
"windows": null,
106+
"linux": null,
107+
"chrome": null,
108+
"firefox": null,
109+
"safari": null,
110+
"edge": null,
111+
"opera": null
112+
},
113+
"rdns": "xyz.abs",
114+
"mobile": {
115+
"native": null,
116+
"universal": null
117+
},
118+
"desktop": {
119+
"native": null,
120+
"universal": null
121+
}
94122
}
95123
]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, expect, test, vi } from "vitest";
2+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
3+
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
4+
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
5+
import { trackConnect } from "../../analytics/track/connect.js";
6+
import { fromProvider } from "./from-eip1193.js";
7+
import type { EIP1193Provider } from "./types.js";
8+
9+
vi.mock("../../analytics/track/connect.js");
10+
11+
describe("fromProvider", () => {
12+
const mockProvider: EIP1193Provider = {
13+
on: vi.fn(),
14+
removeListener: vi.fn(),
15+
request: vi.fn(),
16+
};
17+
18+
const mockAccount = TEST_ACCOUNT_A;
19+
20+
test("should create a wallet with the correct properties", () => {
21+
const wallet = fromProvider({
22+
provider: mockProvider,
23+
walletId: "io.metamask",
24+
});
25+
26+
expect(wallet.id).toBe("io.metamask");
27+
expect(wallet.subscribe).toBeDefined();
28+
expect(wallet.connect).toBeDefined();
29+
expect(wallet.disconnect).toBeDefined();
30+
expect(wallet.getAccount).toBeDefined();
31+
expect(wallet.getChain).toBeDefined();
32+
expect(wallet.getConfig).toBeDefined();
33+
expect(wallet.switchChain).toBeDefined();
34+
});
35+
36+
test("should use 'adapter' as default walletId", () => {
37+
const wallet = fromProvider({
38+
provider: mockProvider,
39+
});
40+
41+
expect(wallet.id).toBe("adapter");
42+
});
43+
44+
test("should handle async provider function", async () => {
45+
const wallet = fromProvider({
46+
provider: async () =>
47+
Promise.resolve({
48+
...mockProvider,
49+
request: () => Promise.resolve([mockAccount.address]),
50+
}),
51+
});
52+
53+
// Connect to trigger provider initialization
54+
await wallet.connect({
55+
client: TEST_CLIENT,
56+
});
57+
58+
expect(wallet.getAccount()?.address).toBe(mockAccount.address);
59+
});
60+
61+
test("should emit events on connect", async () => {
62+
const wallet = fromProvider({
63+
provider: {
64+
...mockProvider,
65+
request: () => Promise.resolve([mockAccount.address]),
66+
},
67+
});
68+
69+
const onConnectSpy = vi.fn();
70+
wallet.subscribe("onConnect", onConnectSpy);
71+
72+
await wallet.connect({
73+
client: TEST_CLIENT,
74+
chain: ANVIL_CHAIN,
75+
});
76+
77+
expect(onConnectSpy).toHaveBeenCalled();
78+
expect(trackConnect).toHaveBeenCalledWith({
79+
client: TEST_CLIENT,
80+
walletType: "adapter",
81+
walletAddress: mockAccount.address,
82+
});
83+
});
84+
85+
test("should emit events on disconnect", async () => {
86+
const wallet = fromProvider({
87+
provider: mockProvider,
88+
});
89+
90+
const onDisconnectSpy = vi.fn();
91+
wallet.subscribe("disconnect", onDisconnectSpy);
92+
93+
await wallet.disconnect();
94+
95+
expect(onDisconnectSpy).toHaveBeenCalled();
96+
});
97+
98+
test("should handle chain changes", async () => {
99+
const wallet = fromProvider({
100+
provider: {
101+
...mockProvider,
102+
request: () => Promise.resolve([mockAccount.address]),
103+
},
104+
});
105+
106+
const onChainChangedSpy = vi.fn();
107+
wallet.subscribe("chainChanged", onChainChangedSpy);
108+
109+
await wallet.connect({
110+
client: TEST_CLIENT,
111+
chain: ANVIL_CHAIN,
112+
});
113+
114+
const chain = wallet.getChain();
115+
expect(chain).toBe(ANVIL_CHAIN);
116+
});
117+
118+
test("should reset state on disconnect", async () => {
119+
const wallet = fromProvider({
120+
provider: {
121+
...mockProvider,
122+
request: () => Promise.resolve([mockAccount.address]),
123+
},
124+
});
125+
126+
mockProvider.request = vi.fn().mockResolvedValueOnce([mockAccount.address]);
127+
128+
await wallet.connect({
129+
client: TEST_CLIENT,
130+
chain: ANVIL_CHAIN,
131+
});
132+
133+
await wallet.disconnect();
134+
135+
expect(wallet.getAccount()).toBeUndefined();
136+
expect(wallet.getChain()).toBeUndefined();
137+
});
138+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as ox__Hex from "ox/Hex";
2+
import { trackConnect } from "../../analytics/track/connect.js";
3+
import type { Chain } from "../../chains/types.js";
4+
import { getCachedChainIfExists } from "../../chains/utils.js";
5+
import {
6+
autoConnectEip1193Wallet,
7+
connectEip1193Wallet,
8+
} from "../../wallets/injected/index.js";
9+
import type { Account, Wallet } from "../../wallets/interfaces/wallet.js";
10+
import { createWalletEmitter } from "../../wallets/wallet-emitter.js";
11+
import type { WalletId } from "../../wallets/wallet-types.js";
12+
import type { EIP1193Provider } from "./types.js";
13+
14+
/**
15+
* Options for creating an EIP-1193 provider adapter.
16+
*/
17+
export type FromEip1193AdapterOptions = {
18+
provider: EIP1193Provider | (() => Promise<EIP1193Provider>);
19+
walletId?: WalletId;
20+
};
21+
22+
/**
23+
* Creates a Thirdweb wallet from an EIP-1193 compatible provider.
24+
*
25+
* This adapter allows you to use any EIP-1193 provider (like MetaMask, WalletConnect, etc.) as a Thirdweb wallet.
26+
* It handles all the necessary conversions between the EIP-1193 interface and Thirdweb's wallet interface.
27+
*
28+
* @param options - Configuration options for creating the wallet adapter
29+
* @param options.provider - An EIP-1193 compatible provider or a function that returns one
30+
* @param options.walletId - Optional custom wallet ID to identify this provider (defaults to "adapter")
31+
* @returns A Thirdweb wallet instance that wraps the EIP-1193 provider
32+
*
33+
* @example
34+
* ```ts
35+
* import { EIP1193 } from "thirdweb/wallets";
36+
*
37+
* // Create a Thirdweb wallet from MetaMask's provider
38+
* const wallet = EIP1193.fromProvider({
39+
* provider: window.ethereum,
40+
* walletId: "io.metamask"
41+
* });
42+
*
43+
* // Use like any other Thirdweb wallet
44+
* const account = await wallet.connect({
45+
* client: createThirdwebClient({ clientId: "..." })
46+
* });
47+
*
48+
* // Sign messages
49+
* await account.signMessage({ message: "Hello World" });
50+
*
51+
* // Send transactions
52+
* await account.sendTransaction({
53+
* to: "0x...",
54+
* value: 1000000000000000000n
55+
* });
56+
* ```
57+
*/
58+
export function fromProvider(options: FromEip1193AdapterOptions): Wallet {
59+
const id: WalletId = options.walletId ?? "adapter";
60+
const emitter = createWalletEmitter();
61+
let account: Account | undefined = undefined;
62+
let chain: Chain | undefined = undefined;
63+
let provider: EIP1193Provider | undefined = undefined;
64+
const getProvider = async () => {
65+
if (!provider) {
66+
provider =
67+
typeof options.provider === "function"
68+
? await options.provider()
69+
: options.provider;
70+
}
71+
return provider;
72+
};
73+
74+
const unsubscribeChain = emitter.subscribe("chainChanged", (newChain) => {
75+
chain = newChain;
76+
});
77+
78+
function reset() {
79+
account = undefined;
80+
chain = undefined;
81+
}
82+
83+
let handleDisconnect = async () => {};
84+
85+
const unsubscribeDisconnect = emitter.subscribe("disconnect", () => {
86+
reset();
87+
unsubscribeChain();
88+
unsubscribeDisconnect();
89+
});
90+
91+
emitter.subscribe("accountChanged", (_account) => {
92+
account = _account;
93+
});
94+
95+
let handleSwitchChain: (c: Chain) => Promise<void> = async (c) => {
96+
await provider?.request({
97+
method: "wallet_switchEthereumChain",
98+
params: [{ chainId: ox__Hex.fromNumber(c.id) }],
99+
});
100+
};
101+
102+
return {
103+
id,
104+
subscribe: emitter.subscribe,
105+
getConfig: () => undefined,
106+
getChain() {
107+
if (!chain) {
108+
return undefined;
109+
}
110+
111+
chain = getCachedChainIfExists(chain.id) || chain;
112+
return chain;
113+
},
114+
getAccount: () => account,
115+
connect: async (connectOptions) => {
116+
const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] =
117+
await connectEip1193Wallet({
118+
id,
119+
provider: await getProvider(),
120+
client: connectOptions.client,
121+
chain: connectOptions.chain,
122+
emitter,
123+
});
124+
// set the states
125+
account = connectedAccount;
126+
chain = connectedChain;
127+
handleDisconnect = doDisconnect;
128+
handleSwitchChain = doSwitchChain;
129+
emitter.emit("onConnect", connectOptions);
130+
trackConnect({
131+
client: connectOptions.client,
132+
walletType: id,
133+
walletAddress: account.address,
134+
});
135+
// return account
136+
return account;
137+
},
138+
autoConnect: async (connectOptions) => {
139+
const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] =
140+
await autoConnectEip1193Wallet({
141+
id,
142+
provider: await getProvider(),
143+
emitter,
144+
chain: connectOptions.chain,
145+
client: connectOptions.client,
146+
});
147+
// set the states
148+
account = connectedAccount;
149+
chain = connectedChain;
150+
handleDisconnect = doDisconnect;
151+
handleSwitchChain = doSwitchChain;
152+
emitter.emit("onConnect", connectOptions);
153+
trackConnect({
154+
client: connectOptions.client,
155+
walletType: id,
156+
walletAddress: account.address,
157+
});
158+
// return account
159+
return account;
160+
},
161+
disconnect: async () => {
162+
reset();
163+
await handleDisconnect();
164+
emitter.emit("disconnect", undefined);
165+
},
166+
switchChain: async (c) => {
167+
await handleSwitchChain(c);
168+
emitter.emit("chainChanged", c);
169+
},
170+
};
171+
}

0 commit comments

Comments
 (0)