Skip to content

Commit b693b78

Browse files
committed
[SDK] Fix: Switch underlying admin wallet with ecosystem smart account (#6119)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the functionality of smart wallets in the `thirdweb` ecosystem, ensuring they correctly trigger chain switches and improving related wallet checks and configurations. ### Detailed summary - Fixed smart wallets to trigger chain switch on admin wallets. - Updated `smartWalletGetCapabilities` to accept a generic `Wallet` type. - Implemented `isSmartWallet` function to check for smart wallet status. - Modified `getSmartWallet` to throw an error for non-smart wallets. - Refactored wallet checks in `createConnectionManager`. - Added tests for `isSmartWallet` and `getSmartWallet`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent daceeed commit b693b78

File tree

11 files changed

+348
-101
lines changed

11 files changed

+348
-101
lines changed

.changeset/metal-icons-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fix: Ecosystem smart wallets now properly trigger switch chain on their admin wallets

packages/thirdweb/src/wallets/eip5792/get-capabilities.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { isCoinbaseSDKWallet } from "../coinbase/coinbase-web.js";
44
import { isInAppWallet } from "../in-app/core/wallet/index.js";
55
import { getInjectedProvider } from "../injected/index.js";
66
import type { Wallet } from "../interfaces/wallet.js";
7-
import { isSmartWallet } from "../smart/index.js";
87
import { isWalletConnect } from "../wallet-connect/controller.js";
98
import type { WalletId } from "../wallet-types.js";
109
import type { WalletCapabilities, WalletCapabilitiesRecord } from "./types.js";
@@ -47,7 +46,7 @@ export async function getCapabilities<const ID extends WalletId = WalletId>({
4746
};
4847
}
4948

50-
if (isSmartWallet(wallet)) {
49+
if (wallet.id === "smart") {
5150
const { smartWalletGetCapabilities } = await import(
5251
"../smart/lib/smart-wallet-capabilities.js"
5352
);

packages/thirdweb/src/wallets/manager/connection-manager.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
33
import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js";
4+
import { baseSepolia } from "../../chains/chain-definitions/base-sepolia.js";
45
import { sepolia } from "../../chains/chain-definitions/sepolia.js";
56
import type { ThirdwebClient } from "../../client/client.js";
67
import type { AsyncStorage } from "../../utils/storage/AsyncStorage.js";
8+
import { inAppWallet } from "../in-app/web/in-app.js";
79
import type { Account, Wallet } from "../interfaces/wallet.js";
10+
import { smartWallet } from "../smart/smart-wallet.js";
811
import type { SmartWalletOptions } from "../smart/types.js";
912
import {
1013
createConnectionManager,
@@ -157,6 +160,46 @@ describe.runIf(process.env.TW_SECRET_KEY)("Connection Manager", () => {
157160
expect(wallet.switchChain).toHaveBeenCalledWith(newChain);
158161
});
159162

163+
it("should switch admin wallet for smart wallet if available", async () => {
164+
const manager = createConnectionManager(storage);
165+
const adminAccount = TEST_ACCOUNT_A;
166+
const adminWallet = inAppWallet();
167+
adminWallet.getAccount = () => adminAccount;
168+
169+
const _wallet = smartWallet({
170+
chain: baseSepolia,
171+
sponsorGas: true,
172+
});
173+
await _wallet.connect({
174+
client,
175+
personalAccount: adminAccount,
176+
});
177+
178+
await manager.handleConnection(adminWallet, { client });
179+
await manager.handleConnection(_wallet, { client });
180+
181+
const newChain = {
182+
id: 2,
183+
name: "New Chain",
184+
rpc: "https://rpc.example.com",
185+
};
186+
187+
// Mock storage and wallet setup
188+
storage.getItem = vi.fn().mockResolvedValue("inApp");
189+
adminWallet.id = "inApp";
190+
_wallet.switchChain = vi.fn();
191+
adminWallet.switchChain = vi.fn();
192+
193+
// Add wallets to connected wallets store
194+
manager.addConnectedWallet(adminWallet);
195+
manager.addConnectedWallet(_wallet);
196+
197+
await manager.switchActiveWalletChain(newChain);
198+
199+
expect(_wallet.switchChain).toHaveBeenCalledWith(newChain);
200+
expect(adminWallet.switchChain).toHaveBeenCalledWith(newChain);
201+
});
202+
160203
it("should define chains", async () => {
161204
const manager = createConnectionManager(storage);
162205
await manager.handleConnection(wallet, { client });

packages/thirdweb/src/wallets/manager/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { stringify } from "../../utils/json.js";
99
import type { AsyncStorage } from "../../utils/storage/AsyncStorage.js";
1010
import { deleteConnectParamsFromStorage } from "../../utils/storage/walletStorage.js";
1111
import type { Account, Wallet } from "../interfaces/wallet.js";
12+
import { isSmartWallet } from "../smart/index.js";
1213
import { smartWallet } from "../smart/smart-wallet.js";
1314
import type { SmartWalletOptions } from "../smart/types.js";
1415
import type { WalletId } from "../wallet-types.js";
@@ -257,7 +258,7 @@ export function createConnectionManager(storage: AsyncStorage) {
257258
throw new Error("Wallet does not support switching chains");
258259
}
259260

260-
if (wallet.id === "smart") {
261+
if (isSmartWallet(wallet)) {
261262
// also switch personal wallet
262263
const personalWalletId = await getStoredActiveWalletId(storage);
263264
if (personalWalletId) {
@@ -266,10 +267,14 @@ export function createConnectionManager(storage: AsyncStorage) {
266267
.find((w) => w.id === personalWalletId);
267268
if (personalWallet) {
268269
await personalWallet.switchChain(chain);
270+
await wallet.switchChain(chain);
271+
// reset the active wallet as switch chain recreates a new smart account
272+
handleSetActiveWallet(wallet);
273+
return;
269274
}
270275
}
276+
// If we couldn't find the personal wallet, just switch the smart wallet
271277
await wallet.switchChain(chain);
272-
// reset the active wallet as switch chain recreates a new smart account
273278
handleSetActiveWallet(wallet);
274279
} else {
275280
await wallet.switchChain(chain);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
import { optimism } from "../../chains/chain-definitions/optimism.js";
3+
import type { Wallet } from "../interfaces/wallet.js";
4+
import { getSmartWallet } from "./get-smart-wallet-config.js";
5+
import type { SmartWalletOptions } from "./types.js";
6+
7+
describe("getSmartWallet", () => {
8+
const mockSmartWalletConfig: SmartWalletOptions = {
9+
chain: optimism,
10+
sponsorGas: false,
11+
};
12+
13+
it("should return config for smart wallet ID", () => {
14+
const wallet = {
15+
id: "smart",
16+
getConfig: () => mockSmartWalletConfig,
17+
} as Wallet<"smart">;
18+
19+
expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig);
20+
});
21+
22+
it("should return smartAccount config for wallet with smartAccount", () => {
23+
const wallet = {
24+
id: "inApp",
25+
getConfig: () => ({
26+
smartAccount: mockSmartWalletConfig,
27+
}),
28+
} as Wallet;
29+
30+
expect(getSmartWallet(wallet)).toBe(mockSmartWalletConfig);
31+
});
32+
33+
it("should throw error for non-smart wallet", () => {
34+
const wallet = {
35+
id: "inApp",
36+
getConfig: () => ({}),
37+
} as Wallet;
38+
39+
expect(() => getSmartWallet(wallet)).toThrow(
40+
"Wallet is not a smart wallet",
41+
);
42+
});
43+
44+
it("should throw error when getConfig returns null", () => {
45+
const wallet = {
46+
id: "inApp",
47+
getConfig: () => null,
48+
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
49+
} as any as Wallet;
50+
51+
expect(() => getSmartWallet(wallet)).toThrow(
52+
"Wallet is not a smart wallet",
53+
);
54+
});
55+
56+
it("should throw error when smartAccount is null", () => {
57+
const wallet = {
58+
id: "inApp",
59+
getConfig: () => ({ smartAccount: null }),
60+
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
61+
} as any as Wallet;
62+
63+
expect(() => getSmartWallet(wallet)).toThrow(
64+
"Wallet is not a smart wallet",
65+
);
66+
});
67+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Wallet } from "../interfaces/wallet.js";
2+
import type { SmartWalletOptions } from "./types.js";
3+
4+
/**
5+
* Gets the smart wallet configuration for a given wallet.
6+
*
7+
* @param {Wallet} wallet - The wallet to check.
8+
* @returns {SmartWalletOptions} The smart wallet configuration.
9+
*
10+
* @throws {Error} If the wallet is not a smart wallet.
11+
* @internal
12+
*/
13+
export function getSmartWallet(wallet: Wallet): SmartWalletOptions {
14+
if (wallet.id === "smart") {
15+
return (wallet as Wallet<"smart">).getConfig();
16+
}
17+
18+
const config = wallet.getConfig();
19+
if (!!config && "smartAccount" in config && !!config?.smartAccount) {
20+
return config.smartAccount;
21+
}
22+
23+
throw new Error("Wallet is not a smart wallet");
24+
}

packages/thirdweb/src/wallets/smart/index.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ import type { Hex } from "../../utils/encoding/hex.js";
2020
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
2121
import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js";
2222
import { type SignableMessage, maxUint96 } from "../../utils/types.js";
23-
import type {
24-
Account,
25-
SendTransactionOption,
26-
Wallet,
27-
} from "../interfaces/wallet.js";
28-
import type { WalletId } from "../wallet-types.js";
23+
import type { Account, SendTransactionOption } from "../interfaces/wallet.js";
2924
import {
3025
broadcastZkTransaction,
3126
bundleUserOp,
@@ -58,17 +53,7 @@ import type {
5853
UserOperationV06,
5954
UserOperationV07,
6055
} from "./types.js";
61-
/**
62-
* Checks if the provided wallet is a smart wallet.
63-
*
64-
* @param wallet - The wallet to check.
65-
* @returns True if the wallet is a smart wallet, false otherwise.
66-
*/
67-
export function isSmartWallet(
68-
wallet: Wallet<WalletId>,
69-
): wallet is Wallet<"smart"> {
70-
return wallet.id === "smart";
71-
}
56+
export { isSmartWallet } from "./is-smart-wallet.js";
7257

7358
/**
7459
* For in-app wallets, the smart wallet creation is implicit so we track these to be able to retrieve the personal account for a smart account on the wallet API.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { Wallet } from "../interfaces/wallet.js";
3+
import { isSmartWallet } from "./is-smart-wallet.js";
4+
5+
describe("isSmartWallet", () => {
6+
it("should return true for smart wallet ID", () => {
7+
const wallet = {
8+
id: "smart",
9+
} as Wallet;
10+
expect(isSmartWallet(wallet)).toBe(true);
11+
});
12+
13+
it("should return true for wallet with smartAccount config", () => {
14+
const wallet = {
15+
id: "inApp",
16+
getConfig: () => ({
17+
smartAccount: {
18+
chain: { id: 1, name: "test", rpc: "test" },
19+
},
20+
}),
21+
} as Wallet;
22+
expect(isSmartWallet(wallet)).toBe(true);
23+
});
24+
25+
it("should return false for non-smart wallet", () => {
26+
const wallet = {
27+
id: "inApp",
28+
getConfig: () => ({}),
29+
} as Wallet;
30+
expect(isSmartWallet(wallet)).toBe(false);
31+
});
32+
33+
it("should return false when getConfig returns null", () => {
34+
const wallet = {
35+
id: "inApp",
36+
getConfig: () => null,
37+
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
38+
} as any as Wallet;
39+
expect(isSmartWallet(wallet)).toBe(false);
40+
});
41+
42+
it("should return false when getConfig returns undefined", () => {
43+
const wallet = {
44+
id: "inApp",
45+
getConfig: () => undefined,
46+
} as Wallet;
47+
expect(isSmartWallet(wallet)).toBe(false);
48+
});
49+
50+
it("should return false when smartAccount is null", () => {
51+
const wallet = {
52+
id: "inApp",
53+
// biome-ignore lint/suspicious/noExplicitAny: Testing invalid config
54+
getConfig: () => ({ smartAccount: null }) as any,
55+
} as Wallet;
56+
expect(isSmartWallet(wallet)).toBe(false);
57+
});
58+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Wallet } from "../interfaces/wallet.js";
2+
3+
/**
4+
* Checks if the given wallet is a smart wallet.
5+
*
6+
* @param {Wallet} wallet - The wallet to check.
7+
* @returns {boolean} True if the wallet is a smart wallet, false otherwise.
8+
* @internal
9+
*/
10+
export function isSmartWallet(wallet: Wallet): boolean {
11+
if (wallet.id === "smart") {
12+
return true;
13+
}
14+
15+
const config = wallet.getConfig();
16+
if (!!config && "smartAccount" in config && !!config.smartAccount) {
17+
return true;
18+
}
19+
20+
return false;
21+
}

packages/thirdweb/src/wallets/smart/lib/smart-wallet-capabilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Wallet } from "../../interfaces/wallet.js";
44
* @internal
55
*/
66
export function smartWalletGetCapabilities(args: {
7-
wallet: Wallet<"smart">;
7+
wallet: Wallet;
88
}) {
99
const { wallet } = args;
1010

0 commit comments

Comments
 (0)