diff --git a/.changeset/early-queens-drive.md b/.changeset/early-queens-drive.md new file mode 100644 index 00000000000..e62883aeccd --- /dev/null +++ b/.changeset/early-queens-drive.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Improve EIP5792 support diff --git a/apps/playground-web/src/components/account-abstraction/5792-get-capabilities.tsx b/apps/playground-web/src/components/account-abstraction/5792-get-capabilities.tsx index 03a0b22083c..b8c76bdcdf6 100644 --- a/apps/playground-web/src/components/account-abstraction/5792-get-capabilities.tsx +++ b/apps/playground-web/src/components/account-abstraction/5792-get-capabilities.tsx @@ -6,7 +6,7 @@ import { useCapabilities, useWalletInfo, } from "thirdweb/react"; -import { createWallet } from "thirdweb/wallets"; +import { createWallet, inAppWallet } from "thirdweb/wallets"; import { THIRDWEB_CLIENT } from "../../lib/client"; import CodeClient from "../code/code.client"; @@ -29,6 +29,12 @@ export function Eip5792GetCapabilitiesPreview() { label: "Login to view wallet capabilities", }} wallets={[ + inAppWallet({ + executionMode: { + mode: "EIP7702", + sponsorGas: true, + }, + }), createWallet("io.metamask"), createWallet("com.coinbase.wallet"), ]} diff --git a/packages/thirdweb/src/wallets/coinbase/coinbase-web.ts b/packages/thirdweb/src/wallets/coinbase/coinbase-web.ts index c01ccd33d54..bb9c9e15ccb 100644 --- a/packages/thirdweb/src/wallets/coinbase/coinbase-web.ts +++ b/packages/thirdweb/src/wallets/coinbase/coinbase-web.ts @@ -14,8 +14,16 @@ import { stringToHex, uint8ArrayToHex, } from "../../utils/encoding/hex.js"; +import { stringify } from "../../utils/json.js"; import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js"; import { COINBASE } from "../constants.js"; +import { toGetCallsStatusResponse } from "../eip5792/get-calls-status.js"; +import { toGetCapabilitiesResult } from "../eip5792/get-capabilities.js"; +import { toProviderCallParams } from "../eip5792/send-calls.js"; +import type { + GetCallsStatusRawResponse, + WalletCapabilities, +} from "../eip5792/types.js"; import type { Account, SendTransactionOption, @@ -269,6 +277,64 @@ function createAccount({ } return res; }, + sendCalls: async (options) => { + try { + const { callParams, chain } = await toProviderCallParams( + options, + account, + ); + const callId = await provider.request({ + method: "wallet_sendCalls", + params: callParams, + }); + if (callId && typeof callId === "object" && "id" in callId) { + return { chain, client, id: callId.id as string }; + } + return { chain, client, id: callId as string }; + } catch (error) { + if (/unsupport|not support/i.test((error as Error).message)) { + throw new Error( + `${COINBASE} errored calling wallet_sendCalls, with error: ${error instanceof Error ? error.message : stringify(error)}`, + ); + } + throw error; + } + }, + async getCallsStatus(options) { + try { + const rawResponse = (await provider.request({ + method: "wallet_getCallsStatus", + params: [options.id], + })) as GetCallsStatusRawResponse; + return toGetCallsStatusResponse(rawResponse); + } catch (error) { + if (/unsupport|not support/i.test((error as Error).message)) { + throw new Error( + `${COINBASE} does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.`, + ); + } + throw error; + } + }, + async getCapabilities(options) { + const chainId = options.chainId; + try { + const result = (await provider.request({ + method: "wallet_getCapabilities", + params: [getAddress(account.address)], + })) as Record; + return toGetCapabilitiesResult(result, chainId); + } catch (error: unknown) { + if ( + /unsupport|not support|not available/i.test((error as Error).message) + ) { + return { + message: `${COINBASE} does not support wallet_getCapabilities, reach out to them directly to request EIP-5792 support.`, + }; + } + throw error; + } + }, }; return account; diff --git a/packages/thirdweb/src/wallets/eip5792/get-calls-status.test.ts b/packages/thirdweb/src/wallets/eip5792/get-calls-status.test.ts index 16b8f20f55e..7b6da0fdc6f 100644 --- a/packages/thirdweb/src/wallets/eip5792/get-calls-status.test.ts +++ b/packages/thirdweb/src/wallets/eip5792/get-calls-status.test.ts @@ -6,77 +6,12 @@ import { METAMASK } from "../constants.js"; import { createWallet } from "../create-wallet.js"; import type { Wallet } from "../interfaces/wallet.js"; import { getCallsStatus } from "./get-calls-status.js"; -import { type SendCallsOptions, sendCalls } from "./send-calls.js"; - -const RAW_UNSUPPORTED_ERROR = { - code: -32601, - message: "some nonsense the wallet sends us about not supporting", -}; - -const SEND_CALLS_OPTIONS: Omit = { - calls: [ - { - chain: ANVIL_CHAIN, - client: TEST_CLIENT, - data: "0xabcdef", - to: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709", - }, - { - chain: ANVIL_CHAIN, - client: TEST_CLIENT, - to: "0xa922b54716264130634d6ff183747a8ead91a40b", - value: 123n, - }, - ], -}; const mocks = vi.hoisted(() => ({ - getTransactionReceipt: vi.fn(), - injectedRequest: vi.fn(), + getCallsStatus: vi.fn(), })); -vi.mock("../injected/index.js", () => { - return { - getInjectedProvider: vi.fn().mockReturnValue({ - request: mocks.injectedRequest, - }), - }; -}); - -vi.mock("../../rpc/actions/eth_getTransactionReceipt.js", () => { - return { - eth_getTransactionReceipt: mocks.getTransactionReceipt.mockResolvedValue({ - blockHash: - "0xf19bbafd9fd0124ec110b848e8de4ab4f62bf60c189524e54213285e7f540d4a", - blockNumber: 12345n, - gasUsed: 12345n, - logs: [], - status: "success", - transactionHash: - "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", - }), - }; -}); - -vi.mock("../../transaction/actions/send-and-confirm-transaction.js", () => { - return { - sendAndConfirmTransaction: vi.fn().mockResolvedValue({ - transactionHash: - "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", - }), - }; -}); - -vi.mock("../../transaction/actions/send-batch-transaction.js", () => { - return { - sendBatchTransaction: vi.fn().mockResolvedValue({ - transactionHash: - "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", - }), - }; -}); - -describe.sequential("injected wallet", async () => { +describe.sequential("getCallsStatus general", () => { const wallet: Wallet = createWallet(METAMASK); afterEach(() => { @@ -85,6 +20,7 @@ describe.sequential("injected wallet", async () => { test("with no account should fail", async () => { wallet.getAccount = vi.fn().mockReturnValue(undefined); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); const promise = getCallsStatus({ client: TEST_CLIENT, @@ -93,42 +29,31 @@ describe.sequential("injected wallet", async () => { }); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Failed to get call status, no account found for wallet ${wallet.id}]`, + "[Error: Failed to get call status, no account found for wallet io.metamask]", ); }); - test("should successfully make request", async () => { + test("with no chain should fail", async () => { wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); - mocks.injectedRequest.mockResolvedValue({ - receipts: [], - status: "CONFIRMED", - }); + wallet.getChain = vi.fn().mockReturnValue(undefined); - const result = await getCallsStatus({ + const promise = getCallsStatus({ client: TEST_CLIENT, id: "test", wallet: wallet, }); - expect(mocks.injectedRequest).toHaveBeenCalledWith({ - method: "wallet_getCallsStatus", - params: ["test"], - }); - expect(result).toMatchInlineSnapshot(` - { - "atomic": false, - "chainId": undefined, - "receipts": [], - "status": "success", - "statusCode": 200, - "version": "2.0.0", - } - `); + await expect(promise).rejects.toMatchInlineSnapshot( + "[Error: Failed to get call status, no chain found for wallet io.metamask]", + ); }); - test("without support should fail", async () => { - mocks.injectedRequest.mockRejectedValue(RAW_UNSUPPORTED_ERROR); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + test("without getCallsStatus support should fail", async () => { + wallet.getAccount = vi.fn().mockReturnValue({ + ...TEST_ACCOUNT_A, + // no getCallsStatus method + }); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); const promise = getCallsStatus({ client: TEST_CLIENT, @@ -137,103 +62,188 @@ describe.sequential("injected wallet", async () => { }); await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: io.metamask does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.]", + "[Error: Failed to get call status, wallet io.metamask does not support EIP-5792]", ); }); -}); - -describe.sequential("in-app wallet", async () => { - const sendTransaction = vi.fn(); - const sendBatchTransaction = vi.fn(); - let wallet: Wallet = createWallet("inApp"); - afterEach(() => { - vi.clearAllMocks(); - }); + test("should delegate to account.getCallsStatus", async () => { + const mockResponse = { + status: "success" as const, + statusCode: 200, + receipts: [], + }; - test("default", async () => { - wallet.getAccount = vi.fn().mockReturnValue({ + const mockAccount = { ...TEST_ACCOUNT_A, - sendTransaction, - }); + getCallsStatus: mocks.getCallsStatus.mockResolvedValue(mockResponse), + }; + + wallet.getAccount = vi.fn().mockReturnValue(mockAccount); wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const callResult = await sendCalls({ + const result = await getCallsStatus({ + client: TEST_CLIENT, + id: "test-bundle-id", wallet: wallet, - ...SEND_CALLS_OPTIONS, }); - const result = await getCallsStatus(callResult); - - expect(result.status).toEqual("success"); - expect(result.receipts?.length).toEqual(2); + expect(result).toEqual(mockResponse); + expect(mocks.getCallsStatus).toHaveBeenCalledWith({ + id: "test-bundle-id", + chain: ANVIL_CHAIN, + client: TEST_CLIENT, + }); }); +}); - test("with smart account", async () => { - wallet = createWallet("inApp", { - smartAccount: { chain: ANVIL_CHAIN, sponsorGas: true }, - }); - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue({ +describe.sequential("injected wallet account.getCallsStatus", () => { + // These tests verify the behavior of the getCallsStatus method on injected wallet accounts + // The actual implementation would be in packages/thirdweb/src/wallets/injected/index.ts + + test("should handle successful getCallsStatus", async () => { + const mockProvider = { + request: vi.fn().mockResolvedValue({ + receipts: [], + status: "CONFIRMED", + }), + }; + + // Mock what an injected account with getCallsStatus would look like + const injectedAccount = { ...TEST_ACCOUNT_A, - sendBatchTransaction, - }); + getCallsStatus: async (options: any) => { + // This mimics the implementation in injected/index.ts + const response = await mockProvider.request({ + method: "wallet_getCallsStatus", + params: [options.id], + }); + return { + ...response, + status: "success" as const, + statusCode: 200, + }; + }, + }; + + const wallet: Wallet = createWallet(METAMASK); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const callResult = await sendCalls({ + const result = await getCallsStatus({ + client: TEST_CLIENT, + id: "test", wallet: wallet, - ...SEND_CALLS_OPTIONS, }); - const result = await getCallsStatus(callResult); - expect(result.status).toEqual("success"); - expect(result.receipts?.length).toEqual(1); + expect(result.statusCode).toEqual(200); + expect(mockProvider.request).toHaveBeenCalledWith({ + method: "wallet_getCallsStatus", + params: ["test"], + }); }); - test("with pending transaction", async () => { - mocks.getTransactionReceipt.mockRejectedValue(null); + test("should handle provider errors", async () => { + const mockProvider = { + request: vi.fn().mockRejectedValue({ + code: -32601, + message: "some nonsense the wallet sends us about not supporting", + }), + }; - wallet = createWallet("inApp"); - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue({ + const injectedAccount = { ...TEST_ACCOUNT_A, - sendTransaction, - }); + getCallsStatus: async (options: any) => { + try { + return await mockProvider.request({ + method: "wallet_getCallsStatus", + params: [options.id], + }); + } catch (error) { + throw new Error( + `io.metamask does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.`, + ); + } + }, + }; + + const wallet: Wallet = createWallet(METAMASK); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const callResult = await sendCalls({ + const promise = getCallsStatus({ + client: TEST_CLIENT, + id: "test", wallet: wallet, - ...SEND_CALLS_OPTIONS, }); - const result = await getCallsStatus(callResult); + await expect(promise).rejects.toMatchInlineSnapshot( + "[Error: io.metamask does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.]", + ); + }); +}); - expect(result.status).toEqual("pending"); - expect(result.receipts?.length).toEqual(0); +describe.sequential("in-app wallet", () => { + const wallet: Wallet = createWallet("inApp"); + + afterEach(() => { + vi.clearAllMocks(); }); - test("unknown bundle id should fail", async () => { - const promise = getCallsStatus({ + test("should delegate to in-app wallet getCallsStatus implementation", async () => { + const mockResponse = { + status: "success" as const, + statusCode: 200, + receipts: [ + { + blockNumber: 12345n, + gasUsed: 21000n, + status: "success" as const, + transactionHash: + "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", + }, + ], + }; + + const inAppAccount = { + ...TEST_ACCOUNT_A, + getCallsStatus: async (options: any) => { + // This would be the actual in-app wallet implementation + return mockResponse; + }, + }; + + wallet.getAccount = vi.fn().mockReturnValue(inAppAccount); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); + + const result = await getCallsStatus({ client: TEST_CLIENT, - id: "unknown", + id: "test-bundle-id", wallet: wallet, }); - await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: Failed to get calls status, unknown bundle id]", - ); + expect(result).toEqual(mockResponse); }); - test("without chain should fail", async () => { - wallet.getChain = vi.fn().mockReturnValue(undefined); + test("should handle unknown bundle id error", async () => { + const inAppAccount = { + ...TEST_ACCOUNT_A, + getCallsStatus: async (options: any) => { + throw new Error("Failed to get calls status, unknown bundle id"); + }, + }; + + wallet.getAccount = vi.fn().mockReturnValue(inAppAccount); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); const promise = getCallsStatus({ client: TEST_CLIENT, - id: "test", + id: "unknown", wallet: wallet, }); await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: Failed to get calls status, no active chain found]", + "[Error: Failed to get calls status, unknown bundle id]", ); }); }); diff --git a/packages/thirdweb/src/wallets/eip5792/get-calls-status.ts b/packages/thirdweb/src/wallets/eip5792/get-calls-status.ts index df78cf106a6..10d84f14802 100644 --- a/packages/thirdweb/src/wallets/eip5792/get-calls-status.ts +++ b/packages/thirdweb/src/wallets/eip5792/get-calls-status.ts @@ -1,12 +1,6 @@ import type { ThirdwebClient } from "../../client/client.js"; import { hexToBigInt, hexToNumber } from "../../utils/encoding/hex.js"; -import { isCoinbaseSDKWallet } from "../coinbase/coinbase-web.js"; -import { isInAppWallet } from "../in-app/core/wallet/index.js"; -import { getInjectedProvider } from "../injected/index.js"; -import type { Ethereum } from "../interfaces/ethereum.js"; import type { Wallet } from "../interfaces/wallet.js"; -import { isSmartWallet } from "../smart/index.js"; -import { isWalletConnect } from "../wallet-connect/controller.js"; import type { GetCallsStatusRawResponse, GetCallsStatusResponse, @@ -59,81 +53,58 @@ export async function getCallsStatus({ ); } - // These conveniently operate the same - if (isSmartWallet(wallet) || isInAppWallet(wallet)) { - const { inAppWalletGetCallsStatus } = await import( - "../in-app/core/eip5972/in-app-wallet-calls.js" + const chain = wallet.getChain(); + if (!chain) { + throw new Error( + `Failed to get call status, no chain found for wallet ${wallet.id}`, ); - return inAppWalletGetCallsStatus({ client, id, wallet }); - } - - if (isWalletConnect(wallet)) { - throw new Error("getCallsStatus is not yet supported for Wallet Connect"); } - let provider: Ethereum; - if (isCoinbaseSDKWallet(wallet)) { - const { getCoinbaseWebProvider } = await import( - "../coinbase/coinbase-web.js" - ); - const config = wallet.getConfig(); - provider = (await getCoinbaseWebProvider(config)) as Ethereum; - } else { - provider = getInjectedProvider(wallet.id); + if (account.getCallsStatus) { + return account.getCallsStatus({ id, chain, client }); } - try { - const { - atomic = false, - chainId, - receipts, - version = "2.0.0", - ...response - } = (await provider.request({ - method: "wallet_getCallsStatus", - params: [id], - })) as GetCallsStatusRawResponse; - const [status, statusCode] = (() => { - const statusCode = response.status; - if (statusCode >= 100 && statusCode < 200) - return ["pending", statusCode] as const; - if (statusCode >= 200 && statusCode < 300) - return ["success", statusCode] as const; - if (statusCode >= 300 && statusCode < 700) - return ["failure", statusCode] as const; - // @ts-expect-error: for backwards compatibility - if (statusCode === "CONFIRMED") return ["success", 200] as const; - // @ts-expect-error: for backwards compatibility - if (statusCode === "PENDING") return ["pending", 100] as const; - return [undefined, statusCode]; - })(); - return { - ...response, - atomic, - // @ts-expect-error: for backwards compatibility - chainId: chainId ? hexToNumber(chainId) : undefined, - receipts: - receipts?.map((receipt) => ({ - ...receipt, - blockNumber: hexToBigInt(receipt.blockNumber), - gasUsed: hexToBigInt(receipt.gasUsed), - status: receiptStatuses[receipt.status as "0x0" | "0x1"], - })) ?? [], - status, - statusCode, - version, - }; - } catch (error) { - if (/unsupport|not support/i.test((error as Error).message)) { - throw new Error( - `${wallet.id} does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.`, - ); - } - throw error; - } + throw new Error( + `Failed to get call status, wallet ${wallet.id} does not support EIP-5792`, + ); } const receiptStatuses = { "0x0": "reverted", "0x1": "success", } as const; + +export function toGetCallsStatusResponse( + response: GetCallsStatusRawResponse, +): GetCallsStatusResponse { + const [status, statusCode] = (() => { + const statusCode = response.status; + if (statusCode >= 100 && statusCode < 200) + return ["pending", statusCode] as const; + if (statusCode >= 200 && statusCode < 300) + return ["success", statusCode] as const; + if (statusCode >= 300 && statusCode < 700) + return ["failure", statusCode] as const; + // @ts-expect-error: for backwards compatibility + if (statusCode === "CONFIRMED") return ["success", 200] as const; + // @ts-expect-error: for backwards compatibility + if (statusCode === "PENDING") return ["pending", 100] as const; + return [undefined, statusCode]; + })(); + return { + ...response, + atomic: response.atomic, + // @ts-expect-error: for backwards compatibility + chainId: response.chainId ? hexToNumber(response.chainId) : undefined, + receipts: + response.receipts?.map((receipt) => ({ + ...receipt, + blockNumber: hexToBigInt(receipt.blockNumber), + gasUsed: hexToBigInt(receipt.gasUsed), + status: receiptStatuses[receipt.status as "0x0" | "0x1"], + })) ?? [], + status, + statusCode, + version: response.version, + }; +} diff --git a/packages/thirdweb/src/wallets/eip5792/get-capabilities.test.ts b/packages/thirdweb/src/wallets/eip5792/get-capabilities.test.ts index 8d51b0c3c8b..c522f9a2c07 100644 --- a/packages/thirdweb/src/wallets/eip5792/get-capabilities.test.ts +++ b/packages/thirdweb/src/wallets/eip5792/get-capabilities.test.ts @@ -1,261 +1,242 @@ -import { afterEach, beforeAll, describe, expect, it, test, vi } from "vitest"; -import { - ANVIL_CHAIN, - FORKED_ETHEREUM_CHAIN, -} from "../../../test/src/chains.js"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { ANVIL_CHAIN } from "../../../test/src/chains.js"; import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js"; import { METAMASK } from "../constants.js"; import { createWallet } from "../create-wallet.js"; import type { Wallet } from "../interfaces/wallet.js"; -import { getCapabilities } from "./get-capabilities.js"; - -const SUPPORTED_RESPONSE = { - 1: { - paymasterService: { - supported: true, - }, - sessionKeys: { - supported: true, - }, - }, -}; - -const RAW_UNSUPPORTED_ERROR = { - code: -32601, - message: "some nonsense the wallet sends us about not supporting", -}; - -const UNSUPPORTED_RESPONSE_STRING = "does not support wallet_getCapabilities"; +import { + type GetCapabilitiesOptions, + getCapabilities, +} from "./get-capabilities.js"; const mocks = vi.hoisted(() => ({ - injectedRequest: vi.fn(), + getCapabilities: vi.fn(), })); -vi.mock("../injected/index.js", () => { - return { - getInjectedProvider: vi.fn().mockReturnValue({ - request: mocks.injectedRequest, - }), - }; -}); - -describe.sequential("injected wallet", async () => { +describe.sequential("getCapabilities general", () => { const wallet: Wallet = createWallet(METAMASK); - describe.sequential("supported", () => { - beforeAll(() => { - mocks.injectedRequest.mockResolvedValue(SUPPORTED_RESPONSE); - }); - - afterEach(() => { - mocks.injectedRequest.mockClear(); - }); - test("without account should return no capabilities", async () => { - wallet.getAccount = vi.fn().mockReturnValue(undefined); + afterEach(() => { + vi.clearAllMocks(); + }); - const capabilities = await getCapabilities({ - wallet, - }); + test("without account should return message", async () => { + wallet.getAccount = vi.fn().mockReturnValue(undefined); - expect(mocks.injectedRequest).not.toHaveBeenCalled(); - expect(capabilities).toEqual({ - message: - "Can't get capabilities, no account connected for wallet: io.metamask", - }); + const capabilities = await getCapabilities({ + wallet, }); - test("with account should return capabilities", async () => { - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); - - const capabilities = await getCapabilities({ - wallet, - }); - - expect(mocks.injectedRequest).toHaveBeenCalledWith({ - method: "wallet_getCapabilities", - params: [TEST_ACCOUNT_A.address], - }); - expect(capabilities).toEqual(SUPPORTED_RESPONSE); + expect(capabilities).toEqual({ + message: + "Can't get capabilities, no account connected for wallet: io.metamask", }); }); - describe("unsupported", () => { - beforeAll(() => { - mocks.injectedRequest.mockRejectedValue(RAW_UNSUPPORTED_ERROR); + test("without getCapabilities support should fail", async () => { + wallet.getAccount = vi.fn().mockReturnValue({ + ...TEST_ACCOUNT_A, + // no getCapabilities method }); - afterEach(() => { - mocks.injectedRequest.mockClear(); + const promise = getCapabilities({ + wallet, }); - it("should return clean unsupported response", async () => { - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const capabilities = await getCapabilities({ - wallet, - }); - - expect(mocks.injectedRequest).toHaveBeenCalledWith({ - method: "wallet_getCapabilities", - params: [TEST_ACCOUNT_A.address], - }); - if ("message" in capabilities) { - expect(capabilities.message).toContain(UNSUPPORTED_RESPONSE_STRING); - } else { - throw new Error("capabilities does not contain message"); - } - }); + await expect(promise).rejects.toMatchInlineSnapshot( + "[Error: Failed to get capabilities, wallet io.metamask does not support EIP-5792]", + ); }); -}); -describe.sequential("in-app wallet", async () => { - let wallet: Wallet = createWallet("inApp"); + test("should delegate to account.getCapabilities", async () => { + const mockResponse = { + [ANVIL_CHAIN.id]: { + paymasterService: { + supported: true, + }, + sessionKeys: { + supported: true, + }, + }, + }; - test("should return no support", async () => { - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + const mockAccount = { + ...TEST_ACCOUNT_A, + getCapabilities: mocks.getCapabilities.mockResolvedValue(mockResponse), + }; - const capabilities = await getCapabilities({ + wallet.getAccount = vi.fn().mockReturnValue(mockAccount); + + const result = await getCapabilities({ wallet, }); - expect(capabilities).toEqual({ - [ANVIL_CHAIN.id]: { - atomic: { - status: "unsupported", - }, - paymasterService: { - supported: false, - }, - }, + expect(result).toEqual(mockResponse); + expect(mocks.getCapabilities).toHaveBeenCalledWith({ + chainId: undefined, }); }); - test("with no account should return no capabilities", async () => { - wallet.getAccount = vi.fn().mockReturnValue(undefined); + test("should delegate to account.getCapabilities with chainId", async () => { + const mockResponse = { + paymasterService: { + supported: true, + }, + sessionKeys: { + supported: true, + }, + }; - const capabilities = await getCapabilities({ + const mockAccount = { + ...TEST_ACCOUNT_A, + getCapabilities: mocks.getCapabilities.mockResolvedValue(mockResponse), + }; + + wallet.getAccount = vi.fn().mockReturnValue(mockAccount); + + const result = await getCapabilities({ wallet, + chainId: ANVIL_CHAIN.id, }); - expect(capabilities).toEqual({ - message: "Can't get capabilities, no account connected for wallet: inApp", + expect(result).toEqual(mockResponse); + expect(mocks.getCapabilities).toHaveBeenCalledWith({ + chainId: ANVIL_CHAIN.id, }); }); +}); - describe.sequential("with smart account", async () => { - test("with sponsorGas should support paymasterService and atomicBatch", async () => { - wallet = createWallet("inApp", { - smartAccount: { - chain: ANVIL_CHAIN, - sponsorGas: true, - }, - }); - - wallet.getAccount = vi.fn().mockReturnValue({ - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), - }); - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - - const capabilities = await getCapabilities({ - wallet, - }); +describe.sequential("injected wallet account.getCapabilities", () => { + // These tests verify the behavior of the getCapabilities method on injected wallet accounts + // The actual implementation would be in packages/thirdweb/src/wallets/injected/index.ts - expect(capabilities).toEqual({ + test("should handle successful getCapabilities", async () => { + const mockProvider = { + request: vi.fn().mockResolvedValue({ [ANVIL_CHAIN.id]: { - atomic: { - status: "supported", - }, paymasterService: { supported: true, }, + sessionKeys: { + supported: true, + }, }, - }); + }), + }; + + // Mock what an injected account with getCapabilities would look like + const injectedAccount = { + ...TEST_ACCOUNT_A, + getCapabilities: async (_options: any) => { + // This mimics the implementation in injected/index.ts + const response = await mockProvider.request({ + method: "wallet_getCapabilities", + params: [injectedAccount.address], + }); + return response; + }, + }; + + const wallet: Wallet = createWallet(METAMASK); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); + + const result = await getCapabilities({ + wallet, }); - test("without sponsorGas should support atomicBatch", async () => { - wallet = createWallet("inApp", { - smartAccount: { - chain: ANVIL_CHAIN, - sponsorGas: false, + expect(result).toEqual({ + [ANVIL_CHAIN.id]: { + paymasterService: { + supported: true, }, - }); - wallet.getAccount = vi.fn().mockReturnValue({ - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), - }); - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - - const capabilities = await getCapabilities({ - wallet, - }); - - expect(capabilities).toEqual({ - [ANVIL_CHAIN.id]: { - atomic: { - status: "supported", - }, - paymasterService: { - supported: false, - }, + sessionKeys: { + supported: true, }, - }); + }, + }); + expect(mockProvider.request).toHaveBeenCalledWith({ + method: "wallet_getCapabilities", + params: [TEST_ACCOUNT_A.address], }); }); -}); -describe.sequential("smart wallet", async () => { - let wallet: Wallet = createWallet("smart", { - chain: FORKED_ETHEREUM_CHAIN, - sponsorGas: true, - }); - const smartAccount = { - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), - }; + test("should handle provider errors", async () => { + const mockProvider = { + request: vi.fn().mockRejectedValue({ + code: -32601, + message: "some nonsense the wallet sends us about not supporting", + }), + }; + + const injectedAccount = { + ...TEST_ACCOUNT_A, + getCapabilities: async (_options: any) => { + try { + return await mockProvider.request({ + method: "wallet_getCapabilities", + params: [injectedAccount.address], + }); + } catch { + return { + message: `io.metamask does not support wallet_getCapabilities, reach out to them directly to request EIP-5792 support.`, + }; + } + }, + }; - test("with no chain should return no capabilities", async () => { - wallet.getAccount = vi.fn().mockReturnValue(smartAccount); - wallet.getChain = vi.fn().mockReturnValue(undefined); + const wallet: Wallet = createWallet(METAMASK); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); - const capabilities = await getCapabilities({ + const result = await getCapabilities({ wallet, }); - expect(capabilities).toEqual({ + expect(result).toEqual({ message: - "Can't get capabilities, no active chain found for wallet: smart", + "io.metamask does not support wallet_getCapabilities, reach out to them directly to request EIP-5792 support.", }); }); +}); - test("with no account should return no capabilities", async () => { - wallet.getAccount = vi.fn().mockReturnValue(undefined); - wallet.getChain = vi.fn().mockReturnValue(FORKED_ETHEREUM_CHAIN); - - const capabilities = await getCapabilities({ - wallet, - }); +describe.sequential("in-app wallet", () => { + const wallet: Wallet = createWallet("inApp"); - expect(capabilities).toEqual({ - message: "Can't get capabilities, no account connected for wallet: smart", - }); + afterEach(() => { + vi.clearAllMocks(); }); - test("with sponsorGas should support paymasterService and atomicBatch", async () => { - wallet = createWallet("smart", { - chain: ANVIL_CHAIN, - sponsorGas: true, - }); - wallet.getAccount = vi.fn().mockReturnValue(smartAccount); - wallet.getChain = vi.fn().mockReturnValue(FORKED_ETHEREUM_CHAIN); + test("should delegate to in-app wallet getCapabilities implementation", async () => { + const mockResponse = { + [ANVIL_CHAIN.id]: { + atomic: { + status: "unsupported", + }, + paymasterService: { + supported: false, + }, + }, + }; - const capabilities = await getCapabilities({ + const inAppAccount = { + ...TEST_ACCOUNT_A, + getCapabilities: async (_options: GetCapabilitiesOptions) => { + // This would be the actual in-app wallet implementation + return mockResponse; + }, + }; + + wallet.getAccount = vi.fn().mockReturnValue(inAppAccount); + + const result = await getCapabilities({ wallet, }); - expect(capabilities).toEqual({ - [FORKED_ETHEREUM_CHAIN.id]: { + expect(result).toEqual(mockResponse); + }); + + test("should handle smart account capabilities", async () => { + const mockResponse = { + [ANVIL_CHAIN.id]: { atomic: { status: "supported", }, @@ -263,32 +244,62 @@ describe.sequential("smart wallet", async () => { supported: true, }, }, - }); - }); + }; + + const smartAccount = { + ...TEST_ACCOUNT_A, + sendBatchTransaction: vi.fn(), // indicates it's a smart account + getCapabilities: async (_options: GetCapabilitiesOptions) => { + return mockResponse; + }, + }; - test("without sponsorGas should return atomicBatch", async () => { - wallet = createWallet("smart", { - chain: FORKED_ETHEREUM_CHAIN, - sponsorGas: false, - }); - wallet.getChain = vi.fn().mockReturnValue(FORKED_ETHEREUM_CHAIN); wallet.getAccount = vi.fn().mockReturnValue(smartAccount); - const capabilities = await getCapabilities({ + const result = await getCapabilities({ wallet, }); - expect(capabilities).toEqual({ - [FORKED_ETHEREUM_CHAIN.id]: { + expect(result).toEqual(mockResponse); + }); +}); + +describe.sequential("smart wallet", () => { + const wallet: Wallet = createWallet("smart", { + chain: ANVIL_CHAIN, + sponsorGas: true, + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("should delegate to smart wallet getCapabilities implementation", async () => { + const mockResponse = { + [ANVIL_CHAIN.id]: { atomic: { status: "supported", }, paymasterService: { - supported: false, + supported: true, }, }, + }; + + const smartAccount = { + ...TEST_ACCOUNT_A, + sendBatchTransaction: vi.fn(), + getCapabilities: async (_options: GetCapabilitiesOptions) => { + return mockResponse; + }, + }; + + wallet.getAccount = vi.fn().mockReturnValue(smartAccount); + + const result = await getCapabilities({ + wallet, }); + + expect(result).toEqual(mockResponse); }); }); - -// TODO: Coinbase SDK Tests diff --git a/packages/thirdweb/src/wallets/eip5792/get-capabilities.ts b/packages/thirdweb/src/wallets/eip5792/get-capabilities.ts index 915c8ffa285..26b3b85e4dc 100644 --- a/packages/thirdweb/src/wallets/eip5792/get-capabilities.ts +++ b/packages/thirdweb/src/wallets/eip5792/get-capabilities.ts @@ -1,14 +1,5 @@ -import { getAddress } from "../../utils/address.js"; import type { Prettify } from "../../utils/type-utils.js"; -import { - type CoinbaseWalletCreationOptions, - isCoinbaseSDKWallet, -} from "../coinbase/coinbase-web.js"; -import { isInAppWallet } from "../in-app/core/wallet/index.js"; -import { getInjectedProvider } from "../injected/index.js"; -import type { Ethereum } from "../interfaces/ethereum.js"; import type { Wallet } from "../interfaces/wallet.js"; -import { isWalletConnect } from "../wallet-connect/controller.js"; import type { WalletId } from "../wallet-types.js"; import type { WalletCapabilities, WalletCapabilitiesRecord } from "./types.js"; @@ -52,64 +43,32 @@ export async function getCapabilities({ }; } - if (wallet.id === "smart") { - const { smartWalletGetCapabilities } = await import( - "../smart/lib/smart-wallet-capabilities.js" - ); - return smartWalletGetCapabilities({ wallet }); + if (account.getCapabilities) { + return account.getCapabilities({ chainId }); } - if (isInAppWallet(wallet)) { - const { inAppWalletGetCapabilities } = await import( - "../in-app/core/eip5972/in-app-wallet-capabilities.js" - ); - return inAppWalletGetCapabilities({ wallet }); - } - - // TODO: Add Wallet Connect support - if (isWalletConnect(wallet)) { - return { - message: "getCapabilities is not yet supported with Wallet Connect", - }; - } - - let provider: Ethereum; - if (isCoinbaseSDKWallet(wallet)) { - const { getCoinbaseWebProvider } = await import( - "../coinbase/coinbase-web.js" - ); - const config = wallet.getConfig() as CoinbaseWalletCreationOptions; - provider = (await getCoinbaseWebProvider(config)) as Ethereum; - } else { - provider = getInjectedProvider(wallet.id); - } + throw new Error( + `Failed to get capabilities, wallet ${wallet.id} does not support EIP-5792`, + ); +} - try { - const result = await provider.request({ - method: "wallet_getCapabilities", - params: [getAddress(account.address)], - }); - const capabilities = {} as WalletCapabilitiesRecord< - WalletCapabilities, - number - >; - for (const [chainId, capabilities_] of Object.entries(result)) { - capabilities[Number(chainId)] = {}; - const capabilitiesCopy = {} as WalletCapabilities; - for (const [key, value] of Object.entries(capabilities_)) { - capabilitiesCopy[key] = value; - } - capabilities[Number(chainId)] = capabilitiesCopy; - } - return ( - typeof chainId === "number" ? capabilities[chainId] : capabilities - ) as never; - } catch (error: unknown) { - if (/unsupport|not support|not available/i.test((error as Error).message)) { - return { - message: `${wallet.id} does not support wallet_getCapabilities, reach out to them directly to request EIP-5792 support.`, - }; +export function toGetCapabilitiesResult( + result: Record, + chainId?: number, +): GetCapabilitiesResult { + const capabilities = {} as WalletCapabilitiesRecord< + WalletCapabilities, + number + >; + for (const [chainId, capabilities_] of Object.entries(result)) { + capabilities[Number(chainId)] = {}; + const capabilitiesCopy = {} as WalletCapabilities; + for (const [key, value] of Object.entries(capabilities_)) { + capabilitiesCopy[key] = value; } - throw error; + capabilities[Number(chainId)] = capabilitiesCopy; } + return ( + typeof chainId === "number" ? capabilities[chainId] : capabilities + ) as never; } diff --git a/packages/thirdweb/src/wallets/eip5792/send-calls.test.ts b/packages/thirdweb/src/wallets/eip5792/send-calls.test.ts index cb9cf850002..a2b0d782d4d 100644 --- a/packages/thirdweb/src/wallets/eip5792/send-calls.test.ts +++ b/packages/thirdweb/src/wallets/eip5792/send-calls.test.ts @@ -1,20 +1,15 @@ import { afterEach } from "node:test"; -import { beforeAll, describe, expect, test, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { ANVIL_CHAIN, FORKED_ETHEREUM_CHAIN, } from "../../../test/src/chains.js"; import { TEST_CLIENT } from "../../../test/src/test-clients.js"; -import { USDT_CONTRACT } from "../../../test/src/test-contracts.js"; -import { - TEST_ACCOUNT_A, - TEST_ACCOUNT_B, - TEST_ACCOUNT_C, -} from "../../../test/src/test-wallets.js"; +import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js"; import { sepolia } from "../../exports/chains.js"; -import { approve } from "../../exports/extensions/erc20.js"; import { prepareTransaction } from "../../transaction/prepare-transaction.js"; import { numberToHex } from "../../utils/encoding/hex.js"; +import { stringify } from "../../utils/json.js"; import { METAMASK } from "../constants.js"; import { createWallet } from "../create-wallet.js"; import type { Wallet } from "../interfaces/wallet.js"; @@ -35,167 +30,153 @@ const SEND_CALLS_OPTIONS: Omit = { ), }; -const RAW_UNSUPPORTED_ERROR = { - code: -32601, - message: "some nonsense the wallet sends us about not supporting", -}; - const mocks = vi.hoisted(() => ({ - eth_estimateGas: vi.fn(), - injectedRequest: vi.fn(), - sendAndConfirmTransaction: vi.fn(), - sendBatchTransaction: vi.fn(), + sendCalls: vi.fn(), + inAppWalletSendCalls: vi.fn(), })); -vi.mock("../injected/index.js", () => { +// Mock the in-app wallet calls implementation +vi.mock("../in-app/core/eip5792/in-app-wallet-calls.js", () => { return { - getInjectedProvider: vi.fn().mockReturnValue({ - request: mocks.injectedRequest, - }), + inAppWalletSendCalls: mocks.inAppWalletSendCalls, }; }); -vi.mock("../../transaction/actions/send-and-confirm-transaction.js", () => { - return { - sendAndConfirmTransaction: - mocks.sendAndConfirmTransaction.mockResolvedValue({ - transactionHash: - "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", - }), - }; -}); - -vi.mock("../../transaction/actions/send-batch-transaction.js", () => { - return { - sendBatchTransaction: mocks.sendBatchTransaction.mockResolvedValue({ - transactionHash: - "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff", - }), - }; -}); - -describe.sequential("injected wallet", () => { +describe.sequential("sendCalls general", () => { const wallet: Wallet = createWallet(METAMASK); - beforeAll(() => { - mocks.injectedRequest.mockResolvedValue("0x123456"); - }); - afterEach(() => { vi.clearAllMocks(); }); - test("with no chain should fail to send calls", async () => { - wallet.getChain = vi.fn().mockReturnValue(undefined); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + test("with no account should fail to send calls", async () => { + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); + wallet.getAccount = vi.fn().mockReturnValue(undefined); const promise = sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: Cannot send calls, no active chain found for wallet: io.metamask]", + "[Error: Cannot send calls, no account connected for wallet: io.metamask]", ); }); - test("with no account should fail to send calls", async () => { + test("without sendCalls support should fail", async () => { wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue(undefined); + wallet.getAccount = vi.fn().mockReturnValue({ + ...TEST_ACCOUNT_A, + // no sendCalls method + }); const promise = sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: Cannot send calls, no account connected for wallet: io.metamask]", + "[Error: Cannot send calls, wallet io.metamask does not support EIP-5792]", ); }); - test("should send calls", async () => { + test("should delegate to account.sendCalls", async () => { + const mockAccount = { + ...TEST_ACCOUNT_A, + sendCalls: mocks.sendCalls.mockResolvedValue({ + id: "0x123456", + client: TEST_CLIENT, + chain: ANVIL_CHAIN, + }), + }; + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + wallet.getAccount = vi.fn().mockReturnValue(mockAccount); const result = await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); expect(result.id).toEqual("0x123456"); - expect(mocks.injectedRequest).toHaveBeenCalledWith({ - method: "wallet_sendCalls", - params: [ - { - atomicRequired: false, - calls: [ - { - data: "0xabcdef", - to: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709", - value: undefined, - }, - { - data: "0x", - to: "0xa922b54716264130634d6ff183747a8ead91a40b", - value: numberToHex(123n), - }, - ], - capabilities: undefined, - chainId: numberToHex(ANVIL_CHAIN.id), - from: TEST_ACCOUNT_A.address, - version: "2.0.0", - }, - ], + expect(result.wallet).toBe(wallet); + expect(mocks.sendCalls).toHaveBeenCalledWith({ + calls: SEND_CALLS_OPTIONS.calls, }); }); - test("should send calls from prepared contract call", async () => { - wallet.getChain = vi.fn().mockReturnValue(FORKED_ETHEREUM_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); - - const preparedTx = approve({ - amount: 100, - contract: USDT_CONTRACT, - spender: TEST_ACCOUNT_B.address, - }); - const preparedTx2 = approve({ - amount: 100, - contract: USDT_CONTRACT, - spender: TEST_ACCOUNT_C.address, - }); + test("should switch chain if needed", async () => { + const mockAccount = { + ...TEST_ACCOUNT_A, + sendCalls: mocks.sendCalls.mockResolvedValue({ + id: "0x123456", + client: TEST_CLIENT, + chain: sepolia, + }), + }; - const result = await sendCalls({ - calls: [preparedTx, preparedTx2], - chain: ANVIL_CHAIN, - wallet, - }); + const switchChainMock = vi.fn(); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); + wallet.getAccount = vi.fn().mockReturnValue(mockAccount); + wallet.switchChain = switchChainMock; - expect(result.id).toEqual("0x123456"); - expect(mocks.injectedRequest).toHaveBeenCalledWith({ - method: "wallet_sendCalls", - params: [ + // Create calls with sepolia chain to trigger chain switch + const sepoliaCallsOptions = { + calls: [ { - atomicRequired: false, - calls: [ - { - data: "0x095ea7b300000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000000000005f5e100", - to: "0xdAC17F958D2ee523a2206206994597C13D831ec7", - value: undefined, - }, + data: "0xabcdef" as const, + to: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709", + }, + ].map((call) => + prepareTransaction({ ...call, chain: sepolia, client: TEST_CLIENT }), + ), + }; + + await sendCalls({ wallet, ...sepoliaCallsOptions }); + + expect(switchChainMock).toHaveBeenCalledWith(sepolia); + }); +}); + +describe.sequential("injected wallet account.sendCalls", () => { + // These tests verify the behavior of the sendCalls method on injected wallet accounts + // The actual implementation is in packages/thirdweb/src/wallets/injected/index.ts + + test("should handle successful sendCalls", async () => { + const mockProvider = { + request: vi.fn().mockResolvedValue("0x123456"), + }; + + // Mock what an injected account with sendCalls would look like + const injectedAccount = { + ...TEST_ACCOUNT_A, + sendCalls: async (_options: SendCallsOptions) => { + // This mimics the implementation in injected/index.ts + const callId = await mockProvider.request({ + method: "wallet_sendCalls", + params: [ { - data: "0x095ea7b30000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000005f5e100", - to: "0xdAC17F958D2ee523a2206206994597C13D831ec7", - value: undefined, + atomicRequired: false, + calls: [ + { + data: "0xabcdef", + to: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709", + value: undefined, + }, + { + data: "0x", + to: "0xa922b54716264130634d6ff183747a8ead91a40b", + value: numberToHex(123n), + }, + ], + capabilities: undefined, + chainId: numberToHex(ANVIL_CHAIN.id), + from: TEST_ACCOUNT_A.address, + version: "2.0.0", }, ], - capabilities: undefined, - chainId: numberToHex(ANVIL_CHAIN.id), - from: TEST_ACCOUNT_A.address, - version: "2.0.0", - }, - ], - }); - }); + }); + return { id: callId, client: TEST_CLIENT, chain: ANVIL_CHAIN }; + }, + }; - test("should override chainId", async () => { + const wallet: Wallet = createWallet(METAMASK); wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const result = await sendCalls({ - chain: sepolia, - wallet, - ...SEND_CALLS_OPTIONS, - }); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); + + const result = await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); expect(result.id).toEqual("0x123456"); - expect(mocks.injectedRequest).toHaveBeenCalledWith({ + expect(mockProvider.request).toHaveBeenCalledWith({ method: "wallet_sendCalls", params: [ { @@ -213,7 +194,7 @@ describe.sequential("injected wallet", () => { }, ], capabilities: undefined, - chainId: numberToHex(sepolia.id), + chainId: numberToHex(ANVIL_CHAIN.id), from: TEST_ACCOUNT_A.address, version: "2.0.0", }, @@ -221,14 +202,39 @@ describe.sequential("injected wallet", () => { }); }); - test("without support should fail", async () => { - mocks.injectedRequest.mockRejectedValue(RAW_UNSUPPORTED_ERROR); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + test("should handle provider errors", async () => { + const mockProvider = { + request: vi.fn().mockRejectedValue({ + code: -32601, + message: "some nonsense the wallet sends us about not supporting", + }), + }; + + const injectedAccount = { + ...TEST_ACCOUNT_A, + sendCalls: async (_options: SendCallsOptions) => { + try { + const callId = await mockProvider.request({ + method: "wallet_sendCalls", + params: [], + }); + return { id: callId, client: TEST_CLIENT, chain: ANVIL_CHAIN }; + } catch (error) { + if (/unsupport|not support/i.test((error as Error).message)) { + throw new Error( + `io.metamask errored calling wallet_sendCalls, with error: ${stringify(error)}`, + ); + } + throw error; + } + }, + }; + + const wallet: Wallet = createWallet(METAMASK); wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - const promise = sendCalls({ - wallet, - ...SEND_CALLS_OPTIONS, - }); + wallet.getAccount = vi.fn().mockReturnValue(injectedAccount); + + const promise = sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); await expect(promise).rejects.toMatchInlineSnapshot( `[Error: io.metamask errored calling wallet_sendCalls, with error: {"code":-32601,"message":"some nonsense the wallet sends us about not supporting"}]`, @@ -237,27 +243,37 @@ describe.sequential("injected wallet", () => { }); describe.sequential("in-app wallet", () => { - let wallet: Wallet = createWallet("inApp"); + const wallet: Wallet = createWallet("inApp"); afterEach(() => { vi.clearAllMocks(); }); - test("should send individual calls", async () => { - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue(TEST_ACCOUNT_A); + test("should send calls via inAppWalletSendCalls", async () => { + // Configure the mock to return the expected value + mocks.inAppWalletSendCalls.mockResolvedValue("0x789abc"); - await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); + const inAppAccount = { + ...TEST_ACCOUNT_A, + sendCalls: async (options: SendCallsOptions) => { + const id = await mocks.inAppWalletSendCalls({ + account: inAppAccount, + calls: options.calls, + }); + return { id, client: TEST_CLIENT, chain: ANVIL_CHAIN }; + }, + }; - expect(mocks.sendAndConfirmTransaction).toHaveBeenCalledTimes(2); - }); + wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); + wallet.getAccount = vi.fn().mockReturnValue(inAppAccount); - test("without account should fail", async () => { - wallet.getAccount = vi.fn().mockReturnValue(undefined); - const promise = sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); - await expect(promise).rejects.toMatchInlineSnapshot( - "[Error: Cannot send calls, no account connected for wallet: inApp]", - ); + const result = await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); + + expect(result.id).toEqual("0x789abc"); + expect(mocks.inAppWalletSendCalls).toHaveBeenCalledWith({ + account: inAppAccount, + calls: SEND_CALLS_OPTIONS.calls, + }); }); test("without account should fail", async () => { @@ -267,21 +283,6 @@ describe.sequential("in-app wallet", () => { "[Error: Cannot send calls, no account connected for wallet: inApp]", ); }); - - test("with smart account should send batch calls", async () => { - wallet = createWallet("inApp", { - smartAccount: { chain: FORKED_ETHEREUM_CHAIN, sponsorGas: true }, - }); - wallet.getChain = vi.fn().mockReturnValue(FORKED_ETHEREUM_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue({ - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), // must specify this to make it behave like a smart account without connecting - }); - - await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); - - expect(mocks.sendBatchTransaction).toHaveBeenCalledTimes(1); - }); }); describe.sequential("smart wallet", () => { @@ -289,44 +290,37 @@ describe.sequential("smart wallet", () => { chain: FORKED_ETHEREUM_CHAIN, sponsorGas: true, }); - wallet.getAccount = vi.fn().mockReturnValue({ - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), // must specify this to make it behave like a smart account without connecting - }); afterEach(() => { vi.clearAllMocks(); }); - test("should send batch calls", async () => { - wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - - await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); + test("should send calls via inAppWalletSendCalls", async () => { + // Configure the mock to return the expected value + mocks.inAppWalletSendCalls.mockResolvedValue("0x789abc"); - expect(mocks.sendBatchTransaction).toHaveBeenCalledTimes(1); - }); -}); - -describe.sequential("smart wallet", () => { - const wallet: Wallet = createWallet("smart", { - chain: FORKED_ETHEREUM_CHAIN, - sponsorGas: true, - }); - - afterEach(() => { - vi.clearAllMocks(); - }); + const smartAccount = { + ...TEST_ACCOUNT_A, + sendBatchTransaction: vi.fn(), + sendCalls: async (options: SendCallsOptions) => { + const id = await mocks.inAppWalletSendCalls({ + account: smartAccount, + calls: options.calls, + }); + return { id, client: TEST_CLIENT, chain: ANVIL_CHAIN }; + }, + }; - test("should send batch transacition", async () => { wallet.getChain = vi.fn().mockReturnValue(ANVIL_CHAIN); - wallet.getAccount = vi.fn().mockReturnValue({ - ...TEST_ACCOUNT_A, - sendBatchTransaction: vi.fn(), // we have to mock this because it doesn't get set until the wallet is connected - }); + wallet.getAccount = vi.fn().mockReturnValue(smartAccount); - await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); + const result = await sendCalls({ wallet, ...SEND_CALLS_OPTIONS }); - expect(mocks.sendBatchTransaction).toHaveBeenCalledTimes(1); + expect(result.id).toEqual("0x789abc"); + expect(mocks.inAppWalletSendCalls).toHaveBeenCalledWith({ + account: smartAccount, + calls: SEND_CALLS_OPTIONS.calls, + }); }); }); diff --git a/packages/thirdweb/src/wallets/eip5792/send-calls.ts b/packages/thirdweb/src/wallets/eip5792/send-calls.ts index a4e616e4d0d..dd51602740b 100644 --- a/packages/thirdweb/src/wallets/eip5792/send-calls.ts +++ b/packages/thirdweb/src/wallets/eip5792/send-calls.ts @@ -4,24 +4,14 @@ import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import { encode } from "../../transaction/actions/encode.js"; import type { PreparedTransaction } from "../../transaction/prepare-transaction.js"; -import { type Address, getAddress } from "../../utils/address.js"; +import { getAddress } from "../../utils/address.js"; import { type Hex, numberToHex } from "../../utils/encoding/hex.js"; -import { stringify } from "../../utils/json.js"; import { type PromisedObject, resolvePromisedValue, } from "../../utils/promise/resolve-promised-value.js"; import type { OneOf, Prettify } from "../../utils/type-utils.js"; -import { - type CoinbaseWalletCreationOptions, - isCoinbaseSDKWallet, -} from "../coinbase/coinbase-web.js"; -import { isInAppWallet } from "../in-app/core/wallet/index.js"; -import { getInjectedProvider } from "../injected/index.js"; -import type { Ethereum } from "../interfaces/ethereum.js"; -import type { Wallet } from "../interfaces/wallet.js"; -import { isSmartWallet } from "../smart/index.js"; -import { isWalletConnect } from "../wallet-connect/controller.js"; +import type { Account, Wallet } from "../interfaces/wallet.js"; import type { WalletId } from "../wallet-types.js"; import type { EIP5792Call, @@ -129,19 +119,7 @@ export type SendCallsResult = Prettify<{ export async function sendCalls( options: SendCallsOptions, ): Promise { - const { - wallet, - calls, - capabilities, - version = "2.0.0", - chain = wallet.getChain(), - } = options; - - if (!chain) { - throw new Error( - `Cannot send calls, no active chain found for wallet: ${wallet.id}`, - ); - } + const { wallet, chain } = options; const account = wallet.getAccount(); if (!account) { @@ -154,17 +132,44 @@ export async function sendCalls( if (!firstCall) { throw new Error("No calls to send"); } - const client = firstCall.client; - // These conveniently operate the same - if (isSmartWallet(wallet) || isInAppWallet(wallet)) { - const { inAppWalletSendCalls } = await import( - "../in-app/core/eip5972/in-app-wallet-calls.js" - ); - const id = await inAppWalletSendCalls({ account, calls }); - return { chain, client, id, wallet }; + const callChain = firstCall.chain || chain; + + if (wallet.getChain()?.id !== callChain.id) { + await wallet.switchChain(callChain); } + // check internal implementations + if (account.sendCalls) { + const { wallet: _, ...optionsWithoutWallet } = options; + const result = await account.sendCalls(optionsWithoutWallet); + return { + ...result, + wallet, + }; + } + + throw new Error( + `Cannot send calls, wallet ${wallet.id} does not support EIP-5792`, + ); +} + +export async function toProviderCallParams( + options: Omit, + account: Account, +): Promise<{ callParams: ViemWalletSendCallsParameters; chain: Chain }> { + const firstCall = options.calls[0]; + if (!firstCall) { + throw new Error("No calls to send"); + } + + const { + calls, + capabilities, + version = "2.0.0", + chain = firstCall.chain, + } = options; + const preparedCalls: EIP5792Call[] = await Promise.all( calls.map(async (call) => { const { to, value } = call; @@ -178,18 +183,26 @@ export async function sendCalls( resolvePromisedValue(value), ]); + if (_to) { + return { + data: _data as Hex, + to: getAddress(_to), + value: + typeof _value === "bigint" || typeof _value === "number" + ? numberToHex(_value) + : undefined, + }; + } + return { data: _data as Hex, - to: _to as Address, - value: - typeof _value === "bigint" || typeof _value === "number" - ? numberToHex(_value) - : undefined, + to: undefined, + value: undefined, }; }), ); - const injectedWalletCallParams: WalletSendCallsParameters = [ + const injectedWalletCallParams: ViemWalletSendCallsParameters = [ { // see: https://eips.ethereum.org/EIPS/eip-5792#wallet_sendcalls atomicRequired: options.atomicRequired ?? false, @@ -201,36 +214,5 @@ export async function sendCalls( }, ]; - if (isWalletConnect(wallet)) { - throw new Error("sendCalls is not yet supported for Wallet Connect"); - } - - let provider: Ethereum; - if (isCoinbaseSDKWallet(wallet)) { - const { getCoinbaseWebProvider } = await import( - "../coinbase/coinbase-web.js" - ); - const config = wallet.getConfig() as CoinbaseWalletCreationOptions; - provider = (await getCoinbaseWebProvider(config)) as Ethereum; - } else { - provider = getInjectedProvider(wallet.id); - } - - try { - const callId = await provider.request({ - method: "wallet_sendCalls", - params: injectedWalletCallParams as ViemWalletSendCallsParameters, // The viem type definition is slightly different - }); - if (typeof callId === "object" && "id" in callId) { - return { chain, client, id: callId.id, wallet }; - } - return { chain, client, id: callId, wallet }; - } catch (error) { - if (/unsupport|not support/i.test((error as Error).message)) { - throw new Error( - `${wallet.id} errored calling wallet_sendCalls, with error: ${error instanceof Error ? error.message : stringify(error)}`, - ); - } - throw error; - } + return { callParams: injectedWalletCallParams, chain }; } diff --git a/packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-calls.ts b/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts similarity index 92% rename from packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-calls.ts rename to packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts index c940d4dffa8..5944308e28a 100644 --- a/packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-calls.ts +++ b/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts @@ -1,3 +1,4 @@ +import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { eth_getTransactionReceipt } from "../../../../rpc/actions/eth_getTransactionReceipt.js"; import { getRpcClient } from "../../../../rpc/rpc.js"; @@ -11,7 +12,7 @@ import type { GetCallsStatusResponse, WalletCallReceipt, } from "../../../eip5792/types.js"; -import type { Account, Wallet } from "../../../interfaces/wallet.js"; +import type { Account } from "../../../interfaces/wallet.js"; const bundlesToTransactions = new LruMap(1000); @@ -52,16 +53,11 @@ export async function inAppWalletSendCalls(args: { * @internal */ export async function inAppWalletGetCallsStatus(args: { - wallet: Wallet; + chain: Chain; client: ThirdwebClient; id: string; }): Promise { - const { wallet, client, id } = args; - - const chain = wallet.getChain(); - if (!chain) { - throw new Error("Failed to get calls status, no active chain found"); - } + const { chain, client, id } = args; const bundle = bundlesToTransactions.get(id); if (!bundle) { diff --git a/packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-capabilities.ts b/packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-capabilities.ts deleted file mode 100644 index e377580e727..00000000000 --- a/packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-capabilities.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Wallet } from "../../../interfaces/wallet.js"; - -/** - * @internal - */ -export function inAppWalletGetCapabilities(args: { - wallet: Wallet<"inApp" | "embedded">; -}) { - const { wallet } = args; - - const chain = wallet.getChain(); - if (chain === undefined) { - return { - message: `Can't get capabilities, no active chain found for wallet: ${wallet.id}`, - }; - } - - const account = wallet.getAccount(); - - const config = wallet.getConfig(); - const sponsorGas = - config?.smartAccount && "sponsorGas" in config.smartAccount - ? config.smartAccount.sponsorGas - : config?.executionMode - ? config.executionMode.mode === "EIP4337" && - config.executionMode.smartAccount && - "sponsorGas" in config.executionMode.smartAccount - ? config.executionMode.smartAccount.sponsorGas - : config.executionMode.mode === "EIP7702" - ? config.executionMode.sponsorGas - : false - : false; - - return { - [chain.id]: { - atomic: { - status: - account?.sendBatchTransaction !== undefined - ? "supported" - : "unsupported", - }, - paymasterService: { - supported: sponsorGas, - }, - }, - }; -} diff --git a/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts b/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts index 262cc263a50..4cbabd58815 100644 --- a/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts +++ b/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts @@ -176,6 +176,40 @@ export const create7702MinimalAccount = (args: { >( _typedData: Definition, ): Promise => adminAccount.signTypedData(_typedData), + sendCalls: async (options) => { + const { inAppWalletSendCalls } = await import( + "../eip5792/in-app-wallet-calls.js" + ); + const firstCall = options.calls[0]; + if (!firstCall) { + throw new Error("No calls to send"); + } + const client = firstCall.client; + const chain = firstCall.chain || options.chain; + const id = await inAppWalletSendCalls({ + account: minimalAccount, + calls: options.calls, + }); + return { chain, client, id }; + }, + getCallsStatus: async (options) => { + const { inAppWalletGetCallsStatus } = await import( + "../eip5792/in-app-wallet-calls.js" + ); + return inAppWalletGetCallsStatus(options); + }, + getCapabilities: async (options) => { + return { + [options.chainId ?? 1]: { + atomic: { + status: "supported", + }, + paymasterService: { + supported: sponsorGas ?? false, + }, + }, + }; + }, }; return minimalAccount; }; @@ -202,6 +236,7 @@ async function getNonce(args: { } async function is7702MinimalAccount( + // biome-ignore lint/suspicious/noExplicitAny: TODO properly type tw contract eoaContract: ThirdwebContract, ): Promise { const code = await getBytecode(eoaContract); @@ -220,7 +255,7 @@ async function waitForTransactionHash(args: { timeoutMs?: number; intervalMs?: number; }): Promise { - const timeout = args.timeoutMs || 120000; // 2mins + const timeout = args.timeoutMs || 300000; // 5mins const interval = args.intervalMs || 1000; // 1s const endtime = Date.now() + timeout; while (Date.now() < endtime) { diff --git a/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts b/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts index dc57b31478b..b94bc9c5ba2 100644 --- a/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts +++ b/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts @@ -179,7 +179,7 @@ export class EnclaveWallet implements IWebWallet { storage, }); }; - return { + const account: Account = { address: getAddress(address), async sendTransaction(tx) { const rpcRequest = getRpcClient({ @@ -264,7 +264,42 @@ export class EnclaveWallet implements IWebWallet { return signature as Hex; }, + sendCalls: async (options) => { + const { inAppWalletSendCalls } = await import( + "../eip5792/in-app-wallet-calls.js" + ); + const firstCall = options.calls[0]; + if (!firstCall) { + throw new Error("No calls to send"); + } + const client = firstCall.client; + const chain = firstCall.chain || options.chain; + const id = await inAppWalletSendCalls({ + account: account, + calls: options.calls, + }); + return { chain, client, id }; + }, + getCallsStatus: async (options) => { + const { inAppWalletGetCallsStatus } = await import( + "../eip5792/in-app-wallet-calls.js" + ); + return inAppWalletGetCallsStatus(options); + }, + getCapabilities: async (options) => { + return { + [options.chainId ?? 1]: { + atomic: { + status: "unsupported", + }, + paymasterService: { + supported: false, + }, + }, + }; + }, }; + return account; } } diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index ac61afbab62..7ac61953255 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -3,6 +3,7 @@ import { getTypesForEIP712Domain, type SignTypedDataParameters, serializeTypedData, + stringify, validateTypedData, } from "viem"; import { isInsufficientFundsError } from "../../analytics/track/helpers.js"; @@ -22,6 +23,10 @@ import { } from "../../utils/encoding/hex.js"; import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js"; import type { InjectedSupportedWalletIds } from "../__generated__/wallet-ids.js"; +import { toGetCallsStatusResponse } from "../eip5792/get-calls-status.js"; +import { toGetCapabilitiesResult } from "../eip5792/get-capabilities.js"; +import { toProviderCallParams } from "../eip5792/send-calls.js"; +import type { GetCallsStatusRawResponse } from "../eip5792/types.js"; import type { Account, SendTransactionOption } from "../interfaces/wallet.js"; import type { DisconnectFn, SwitchChainFn } from "../types.js"; import { getValidPublicRPCUrl } from "../utils/chains.js"; @@ -300,6 +305,64 @@ function createAccount({ ); return result; }, + async sendCalls(options) { + try { + const { callParams, chain } = await toProviderCallParams( + options, + account, + ); + const callId = await provider.request({ + method: "wallet_sendCalls", + params: callParams, + }); + if (callId && typeof callId === "object" && "id" in callId) { + return { chain, client, id: callId.id }; + } + return { chain, client, id: callId }; + } catch (error) { + if (/unsupport|not support/i.test((error as Error).message)) { + throw new Error( + `${id} errored calling wallet_sendCalls, with error: ${error instanceof Error ? error.message : stringify(error)}`, + ); + } + throw error; + } + }, + async getCallsStatus(options) { + try { + const rawResponse = (await provider.request({ + method: "wallet_getCallsStatus", + params: [options.id], + })) as GetCallsStatusRawResponse; + return toGetCallsStatusResponse(rawResponse); + } catch (error) { + if (/unsupport|not support/i.test((error as Error).message)) { + throw new Error( + `${id} does not support wallet_getCallsStatus, reach out to them directly to request EIP-5792 support.`, + ); + } + throw error; + } + }, + async getCapabilities(options) { + const chainId = options.chainId; + try { + const result = await provider.request({ + method: "wallet_getCapabilities", + params: [getAddress(account.address)], + }); + return toGetCapabilitiesResult(result, chainId); + } catch (error: unknown) { + if ( + /unsupport|not support|not available/i.test((error as Error).message) + ) { + return { + message: `${id} does not support wallet_getCapabilities, reach out to them directly to request EIP-5792 support.`, + }; + } + throw error; + } + }, }; return account; diff --git a/packages/thirdweb/src/wallets/interfaces/wallet.ts b/packages/thirdweb/src/wallets/interfaces/wallet.ts index 9eb62ebd463..9c8225de93b 100644 --- a/packages/thirdweb/src/wallets/interfaces/wallet.ts +++ b/packages/thirdweb/src/wallets/interfaces/wallet.ts @@ -2,6 +2,7 @@ import type { Address } from "abitype"; import type * as ox__TypedData from "ox/TypedData"; import type { Hex, SignableMessage } from "viem"; import type { Chain } from "../../chains/types.js"; +import type { ThirdwebClient } from "../../client/client.js"; import type { AuthorizationRequest, SignedAuthorization, @@ -12,6 +13,15 @@ import type { } from "../../transaction/prepare-transaction.js"; import type { SerializableTransaction } from "../../transaction/serialize-transaction.js"; import type { SendTransactionResult } from "../../transaction/types.js"; +import type { GetCapabilitiesResult } from "../eip5792/get-capabilities.js"; +import type { + SendCallsOptions, + SendCallsResult, +} from "../eip5792/send-calls.js"; +import type { + GetCallsStatusResponse, + WalletSendCallsId, +} from "../eip5792/types.js"; import type { WalletEmitter } from "../wallet-emitter.js"; import type { CreateWalletArgs, @@ -300,4 +310,25 @@ export type Account = { * ``` */ watchAsset?: (asset: WatchAssetParams) => Promise; + + /** + * EIP-5792: Send the given array of calls via the wallet provider + */ + sendCalls?: ( + calls: Omit, + ) => Promise>; + /** + * EIP-5792: Get the status of the given call bundle + */ + getCallsStatus?: (options: { + id: WalletSendCallsId; + chain: Chain; + client: ThirdwebClient; + }) => Promise; + /** + * EIP-5792: Get the capabilities of the wallet + */ + getCapabilities?: (options: { + chainId?: number; + }) => Promise; }; diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index 53b080e88ca..89d078b683e 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -226,7 +226,7 @@ async function createSmartAccount( ); } } - + const sponsorGas = options.sponsorGas; let accountContract = options.accountContract; const account: Account = { address: getAddress(accountContract.address), @@ -366,6 +366,40 @@ async function createSmartAccount( typedData, }); }, + sendCalls: async (options) => { + const { inAppWalletSendCalls } = await import( + "../in-app/core/eip5792/in-app-wallet-calls.js" + ); + const firstCall = options.calls[0]; + if (!firstCall) { + throw new Error("No calls to send"); + } + const client = firstCall.client; + const chain = firstCall.chain || options.chain; + const id = await inAppWalletSendCalls({ + account: account, + calls: options.calls, + }); + return { chain, client, id }; + }, + getCallsStatus: async (options) => { + const { inAppWalletGetCallsStatus } = await import( + "../in-app/core/eip5792/in-app-wallet-calls.js" + ); + return inAppWalletGetCallsStatus(options); + }, + getCapabilities: async (options) => { + return { + [options.chainId ?? 1]: { + atomic: { + status: "supported", + }, + paymasterService: { + supported: sponsorGas ?? false, + }, + }, + }; + }, }; return account; } @@ -509,6 +543,40 @@ function createZkSyncAccount(args: { const typedData = parseTypedData(_typedData); return connectionOptions.personalAccount.signTypedData(typedData); }, + sendCalls: async (options) => { + const { inAppWalletSendCalls } = await import( + "../in-app/core/eip5792/in-app-wallet-calls.js" + ); + const firstCall = options.calls[0]; + if (!firstCall) { + throw new Error("No calls to send"); + } + const client = firstCall.client; + const chain = firstCall.chain || options.chain; + const id = await inAppWalletSendCalls({ + account: account, + calls: options.calls, + }); + return { chain, client, id }; + }, + getCallsStatus: async (options) => { + const { inAppWalletGetCallsStatus } = await import( + "../in-app/core/eip5792/in-app-wallet-calls.js" + ); + return inAppWalletGetCallsStatus(options); + }, + getCapabilities: async (options) => { + return { + [options.chainId ?? 1]: { + atomic: { + status: "unsupported", + }, + paymasterService: { + supported: args.sponsorGas ?? false, + }, + }, + }; + }, }; return account; } diff --git a/packages/thirdweb/src/wallets/smart/lib/smart-wallet-capabilities.ts b/packages/thirdweb/src/wallets/smart/lib/smart-wallet-capabilities.ts deleted file mode 100644 index fb78363ec6a..00000000000 --- a/packages/thirdweb/src/wallets/smart/lib/smart-wallet-capabilities.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Wallet } from "../../interfaces/wallet.js"; - -/** - * @internal - */ -export function smartWalletGetCapabilities(args: { wallet: Wallet }) { - const { wallet } = args; - - const chain = wallet.getChain(); - if (chain === undefined) { - return { - message: `Can't get capabilities, no active chain found for wallet: ${wallet.id}`, - }; - } - - const account = wallet.getAccount(); - - const config = wallet.getConfig() ?? {}; - return { - [chain.id]: { - atomic: { - status: - account?.sendBatchTransaction !== undefined - ? "supported" - : "unsupported", - }, - paymasterService: { - supported: "sponsorGas" in config ? config.sponsorGas : false, - }, - }, - }; -}