Skip to content

Commit 217042d

Browse files
committed
[SDK] Add support for base-org/account SDK
- Add @base-org/account 2.5.0 dependency - Implement Base Account SDK wallet connector - Add base-account-web.ts with EIP-1193 provider implementation - Add base-account-wallet.ts with core wallet logic - Export types and helper functions
1 parent 93bcdc5 commit 217042d

File tree

10 files changed

+1061
-466
lines changed

10 files changed

+1061
-466
lines changed

.changeset/add-base-account-sdk.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add Base Account SDK integration with `@base-org/account`
6+
7+
- Add @base-org/account 2.5.0 dependency
8+
- Introduce Base Account SDK wallet connector
9+
- Add base-account-web.ts with EIP-1193 provider implementation
10+
- Add base-account-wallet.ts with core wallet logic
11+
- Add types and helper function exports

packages/thirdweb/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/thirdweb-dev/js/issues"
1212
},
1313
"dependencies": {
14+
"@base-org/account": "2.5.0",
1415
"@coinbase/wallet-sdk": "4.3.0",
1516
"@emotion/react": "11.14.0",
1617
"@emotion/styled": "11.14.1",

packages/thirdweb/src/exports/wallets.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export type {
1111
InjectedSupportedWalletIds,
1212
WCSupportedWalletIds,
1313
} from "../wallets/__generated__/wallet-ids.js";
14+
export type {
15+
BaseAccountSDKWalletConnectionOptions,
16+
BaseAccountWalletCreationOptions,
17+
} from "../wallets/base-account/base-account-web.js";
18+
export { isBaseAccountSDKWallet } from "../wallets/base-account/base-account-web.js";
1419
export type {
1520
CoinbaseSDKWalletConnectionOptions,
1621
CoinbaseWalletCreationOptions,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* internal helper functions for Base Account SDK wallet
3+
*/
4+
5+
import type { ProviderInterface } from "@base-org/account";
6+
import { trackConnect } from "../../analytics/track/connect.js";
7+
import type { Chain } from "../../chains/types.js";
8+
import { getCachedChainIfExists } from "../../chains/utils.js";
9+
import { BASE_ACCOUNT } from "../constants.js";
10+
import type { Account, Wallet } from "../interfaces/wallet.js";
11+
import { createWalletEmitter } from "../wallet-emitter.js";
12+
import type { CreateWalletArgs } from "../wallet-types.js";
13+
14+
/**
15+
* @internal
16+
*/
17+
export function baseAccountWalletSDK(args: {
18+
createOptions?: CreateWalletArgs<typeof BASE_ACCOUNT>[1];
19+
providerFactory: () => Promise<ProviderInterface>;
20+
}): Wallet<typeof BASE_ACCOUNT> {
21+
const { createOptions } = args;
22+
const emitter = createWalletEmitter<typeof BASE_ACCOUNT>();
23+
let account: Account | undefined;
24+
let chain: Chain | undefined;
25+
26+
function reset() {
27+
account = undefined;
28+
chain = undefined;
29+
}
30+
31+
let handleDisconnect = async () => {};
32+
33+
let handleSwitchChain = async (newChain: Chain) => {
34+
chain = newChain;
35+
};
36+
37+
const unsubscribeChainChanged = emitter.subscribe(
38+
"chainChanged",
39+
(newChain) => {
40+
chain = newChain;
41+
},
42+
);
43+
44+
const unsubscribeDisconnect = emitter.subscribe("disconnect", () => {
45+
reset();
46+
unsubscribeChainChanged();
47+
unsubscribeDisconnect();
48+
});
49+
50+
emitter.subscribe("accountChanged", (_account) => {
51+
account = _account;
52+
});
53+
54+
return {
55+
autoConnect: async (options) => {
56+
const { autoConnectBaseAccountSDK } = await import(
57+
"./base-account-web.js"
58+
);
59+
const provider = await args.providerFactory();
60+
const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] =
61+
await autoConnectBaseAccountSDK(options, emitter, provider);
62+
// set the states
63+
account = connectedAccount;
64+
chain = connectedChain;
65+
handleDisconnect = doDisconnect;
66+
handleSwitchChain = doSwitchChain;
67+
trackConnect({
68+
chainId: chain.id,
69+
client: options.client,
70+
walletAddress: account.address,
71+
walletType: BASE_ACCOUNT,
72+
});
73+
// return account
74+
return account;
75+
},
76+
connect: async (options) => {
77+
const { connectBaseAccountSDK } = await import("./base-account-web.js");
78+
const provider = await args.providerFactory();
79+
const [connectedAccount, connectedChain, doDisconnect, doSwitchChain] =
80+
await connectBaseAccountSDK(options, emitter, provider);
81+
82+
// set the states
83+
account = connectedAccount;
84+
chain = connectedChain;
85+
handleDisconnect = doDisconnect;
86+
handleSwitchChain = doSwitchChain;
87+
trackConnect({
88+
chainId: chain.id,
89+
client: options.client,
90+
walletAddress: account.address,
91+
walletType: BASE_ACCOUNT,
92+
});
93+
// return account
94+
return account;
95+
},
96+
disconnect: async () => {
97+
reset();
98+
await handleDisconnect();
99+
},
100+
getAccount: () => account,
101+
getChain() {
102+
if (!chain) {
103+
return undefined;
104+
}
105+
106+
chain = getCachedChainIfExists(chain.id) || chain;
107+
return chain;
108+
},
109+
getConfig: () => createOptions,
110+
id: BASE_ACCOUNT,
111+
subscribe: emitter.subscribe,
112+
switchChain: async (newChain) => {
113+
await handleSwitchChain(newChain);
114+
},
115+
};
116+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type { ProviderInterface } from "@base-org/account";
2+
import * as ox__Hex from "ox/Hex";
3+
import * as ox__TypedData from "ox/TypedData";
4+
import { beforeEach, describe, expect, test, vi } from "vitest";
5+
import { TEST_WALLET_A } from "~test/addresses.js";
6+
import { BASE_ACCOUNT } from "../constants.js";
7+
import type { Wallet } from "../interfaces/wallet.js";
8+
import {
9+
autoConnectBaseAccountSDK,
10+
connectBaseAccountSDK,
11+
isBaseAccountSDKWallet,
12+
} from "./base-account-web.js";
13+
14+
// Mock dependencies
15+
vi.mock("@base-org/account", () => ({
16+
createBaseAccountSDK: vi.fn(() => ({
17+
getProvider: () => ({
18+
disconnect: vi.fn(),
19+
emit: vi.fn(),
20+
off: vi.fn(),
21+
on: vi.fn(),
22+
request: vi.fn(),
23+
}),
24+
})),
25+
}));
26+
27+
vi.mock("../../utils/address.js", () => ({
28+
getAddress: vi.fn((address) => address),
29+
}));
30+
31+
vi.mock("../../chains/utils.js", () => ({
32+
getCachedChain: vi.fn((chainId) => ({ id: chainId })),
33+
getChainMetadata: vi.fn(async (_chain) => ({
34+
explorers: [{ url: "https://explorer.test" }],
35+
name: "Test Chain",
36+
nativeCurrency: { decimals: 18, name: "Test Coin", symbol: "TC" },
37+
})),
38+
}));
39+
40+
vi.mock("../utils/normalizeChainId.js", () => ({
41+
normalizeChainId: vi.fn((chainId) => Number(chainId)),
42+
}));
43+
44+
vi.mock("ox/Hex", async () => {
45+
const actualModule = await vi.importActual("ox/Hex");
46+
return {
47+
...actualModule,
48+
toNumber: vi.fn((hex) => Number.parseInt(hex, 16)),
49+
validate: vi.fn(() => true),
50+
};
51+
});
52+
53+
vi.mock("ox/TypedData", () => ({
54+
extractEip712DomainTypes: vi.fn(() => []),
55+
serialize: vi.fn(() => "serializedData"),
56+
validate: vi.fn(),
57+
}));
58+
59+
describe("Base Account Web", () => {
60+
let provider: ProviderInterface;
61+
62+
beforeEach(async () => {
63+
// Reset module to clear cached provider
64+
vi.resetModules();
65+
const module = await import("./base-account-web.js");
66+
provider = await module.getBaseAccountWebProvider();
67+
});
68+
69+
test("getBaseAccountWebProvider initializes provider", async () => {
70+
expect(provider).toBeDefined();
71+
expect(provider.request).toBeInstanceOf(Function);
72+
});
73+
74+
test("isBaseAccountSDKWallet returns true for Base Account wallet", () => {
75+
const wallet: Wallet = { id: BASE_ACCOUNT } as Wallet;
76+
expect(isBaseAccountSDKWallet(wallet)).toBe(true);
77+
});
78+
79+
test("isBaseAccountSDKWallet returns false for non-Base Account wallet", () => {
80+
const wallet: Wallet = { id: "other" } as unknown as Wallet;
81+
expect(isBaseAccountSDKWallet(wallet)).toBe(false);
82+
});
83+
84+
test("connectBaseAccountSDK connects to the wallet", async () => {
85+
provider.request = vi
86+
.fn()
87+
.mockResolvedValueOnce([TEST_WALLET_A])
88+
.mockResolvedValueOnce("0x1");
89+
const emitter = { emit: vi.fn() };
90+
const options = { client: {} };
91+
92+
const [account, chain] = await connectBaseAccountSDK(
93+
// biome-ignore lint/suspicious/noExplicitAny: Inside tests
94+
options as any,
95+
// biome-ignore lint/suspicious/noExplicitAny: Inside tests
96+
emitter as any,
97+
provider,
98+
);
99+
100+
expect(account.address).toBe(TEST_WALLET_A);
101+
expect(chain.id).toBe(1);
102+
});
103+
104+
test("autoConnectBaseAccountSDK auto-connects to the wallet", async () => {
105+
provider.request = vi
106+
.fn()
107+
.mockResolvedValueOnce([TEST_WALLET_A])
108+
.mockResolvedValueOnce("0x1");
109+
const emitter = { emit: vi.fn() };
110+
const options = { client: {} };
111+
112+
const [account, chain] = await autoConnectBaseAccountSDK(
113+
// biome-ignore lint/suspicious/noExplicitAny: Inside tests
114+
options as any,
115+
// biome-ignore lint/suspicious/noExplicitAny: Inside tests
116+
emitter as any,
117+
provider,
118+
);
119+
120+
expect(account.address).toBe(TEST_WALLET_A);
121+
expect(chain.id).toBe(1);
122+
});
123+
124+
test("signMessage uses ox__Hex for validation", async () => {
125+
const account = {
126+
address: TEST_WALLET_A,
127+
signMessage: async ({ message }: { message: string }) => {
128+
const messageToSign = `0x${ox__Hex.fromString(message)}`;
129+
const res = await provider.request({
130+
method: "personal_sign",
131+
params: [messageToSign, account.address],
132+
});
133+
expect(ox__Hex.validate(res)).toBe(true);
134+
return res;
135+
},
136+
};
137+
138+
provider.request = vi.fn().mockResolvedValue("0xsignature");
139+
const signature = await account.signMessage({ message: "hello" });
140+
expect(signature).toBe("0xsignature");
141+
});
142+
143+
test("signTypedData uses ox__TypedData for serialization", async () => {
144+
const account = {
145+
address: TEST_WALLET_A,
146+
// biome-ignore lint/suspicious/noExplicitAny: Inside tests
147+
signTypedData: async (typedData: any) => {
148+
const { domain, message, primaryType } = typedData;
149+
const types = {
150+
EIP712Domain: ox__TypedData.extractEip712DomainTypes(domain),
151+
...typedData.types,
152+
};
153+
ox__TypedData.validate({ domain, message, primaryType, types });
154+
const stringifiedData = ox__TypedData.serialize({
155+
domain: domain ?? {},
156+
message,
157+
primaryType,
158+
types,
159+
});
160+
const res = await provider.request({
161+
method: "eth_signTypedData_v4",
162+
params: [account.address, stringifiedData],
163+
});
164+
expect(ox__Hex.validate(res)).toBe(true);
165+
return res;
166+
},
167+
};
168+
169+
provider.request = vi.fn().mockResolvedValue("0xsignature");
170+
const signature = await account.signTypedData({
171+
domain: {},
172+
message: {},
173+
primaryType: "EIP712Domain",
174+
types: {},
175+
});
176+
expect(signature).toBe("0xsignature");
177+
});
178+
});

0 commit comments

Comments
 (0)