diff --git a/.changeset/smooth-ads-bathe.md b/.changeset/smooth-ads-bathe.md new file mode 100644 index 00000000000..2ed1b39cfb4 --- /dev/null +++ b/.changeset/smooth-ads-bathe.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Add EIP-5792 support for EIP1193.toProvider() diff --git a/apps/portal/src/app/wallets/adapters/page.mdx b/apps/portal/src/app/wallets/adapters/page.mdx index 8ee33a49ad6..3c760a5ce61 100644 --- a/apps/portal/src/app/wallets/adapters/page.mdx +++ b/apps/portal/src/app/wallets/adapters/page.mdx @@ -63,10 +63,48 @@ export const config = createConfig({ }); ``` -Then in your application, you can use the connector to trigger the in-app wallet connection. +### Using the ConnectButton or ConnectEmbed with a wagmi application + +You can use the thirdweb react connection components and hooks like ConnectButton, ConnectEmbed, useConnectModal, etc, but still keep using wagmi for the rest of the application. + +To do so, you will need to connect the `inAppWalletConnector` connector from the `@thirdweb-dev/wagmi-adapter` package with the connected thirdweb wallet. + +You can do this at any time after connecting the thirdweb wallet, a convenient place to do this is in the `onConnect` callback of the ConnectButton or ConnectEmbed. + +```tsx +const { connectors, connect } = useConnect(); // from wagmi + + { + // connect the wagmi connector with the connected thirdweb wallet + const twConnector = connectors.find( + (c) => c.id === "in-app-wallet", + ); + if (twConnector) { + const options = { + wallet, // pass the connected wallet + } satisfies ConnectionOptions; // for type safety + connect({ + connector: twConnector, + chainId: chain.id, + ...options, + }); + } + }} +/> +``` + +Make sure your app is wrapped in a `` as well as a `` since both contexts will be used here. From there, the wallet state will be in sync between the 2 libraries. + +**Note:** the ConnectButton and ConnectEmbed handle reconnecting automatically on page reload. If not using those components (ie. useConnectModal or useConnect hooks directly), you will need to handle reconnecting by explicitely calling `useAutoConnect()`, and doing the same wagmi connection from the `onConnect` callback. + +### Using the connector directly (headless mode) + +You can also use the `inAppWalletConnector` connector directly in your application to connect to the thirdweb wallet without needing a `` at all. ```ts -const { connect, connectors } = useConnect(); +const { connect, connectors } = useConnect(); // from wagmi const onClick = () => { // grab the connector @@ -79,9 +117,10 @@ const onClick = () => { }; ``` + ### Converting a wagmi wallet client to a thirdweb wallet -You can use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc. +You can also use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc. ```ts // Assumes you've wrapped your application in a `` diff --git a/apps/wagmi-demo/src/App.tsx b/apps/wagmi-demo/src/App.tsx index 8dd24916819..43b1a20c8ba 100644 --- a/apps/wagmi-demo/src/App.tsx +++ b/apps/wagmi-demo/src/App.tsx @@ -2,11 +2,13 @@ import type { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter"; import { ConnectButton } from "thirdweb/react"; import { useAccount, + useCallsStatus, useConnect, useDisconnect, + useSendCalls, useSendTransaction, } from "wagmi"; -import { chain, client } from "./wagmi.js"; +import { chain, client, thirdwebChainForWallet, wallet } from "./wagmi.js"; function App() { const account = useAccount(); @@ -20,6 +22,17 @@ function App() { error: sendTxError, data: sendTxData, } = useSendTransaction(); + const { sendCalls, data, isPending: isPendingSendCalls } = useSendCalls(); + const { + data: callStatus, + isLoading: isLoadingCallStatus, + error: callStatusError, + } = useCallsStatus({ + id: data?.id || "", + query: { + enabled: !!data?.id, + }, + }); return ( <>
@@ -44,6 +57,8 @@ function App() {

Connect

{ // auto connect to wagmi on tw connect const twConnector = connectors.find( @@ -95,7 +110,26 @@ function App() { > Send Tx -
{isPending ? "Sending..." : ""}
+ +
+ {isPending || isPendingSendCalls || isLoadingCallStatus + ? "Sending..." + : ""} +
{isSuccess ? `Success: ${sendTxData}` @@ -103,6 +137,13 @@ function App() { ? sendTxError?.message : ""}
+
+ {callStatus + ? `Success: ${JSON.stringify(callStatus, null, 2)}` + : callStatusError + ? callStatusError?.message + : ""} +
)} diff --git a/apps/wagmi-demo/src/wagmi.ts b/apps/wagmi-demo/src/wagmi.ts index 7d9149a0298..4bc5fa57880 100644 --- a/apps/wagmi-demo/src/wagmi.ts +++ b/apps/wagmi-demo/src/wagmi.ts @@ -1,5 +1,6 @@ import { inAppWalletConnector } from "@thirdweb-dev/wagmi-adapter"; import { createThirdwebClient, defineChain as thirdwebChain } from "thirdweb"; +import { inAppWallet } from "thirdweb/wallets/in-app"; import { createConfig, http } from "wagmi"; import { baseSepolia } from "wagmi/chains"; @@ -15,6 +16,13 @@ export const client = createThirdwebClient({ clientId, }); +export const wallet = inAppWallet({ + executionMode: { + mode: "EIP7702", + sponsorGas: true, + }, +}); + export const chain = baseSepolia; export const thirdwebChainForWallet = thirdwebChain(baseSepolia.id); @@ -23,8 +31,8 @@ export const config = createConfig({ connectors: [ inAppWalletConnector({ client, - smartAccount: { - chain: thirdwebChain(chain.id), + executionMode: { + mode: "EIP7702", sponsorGas: true, }, }), diff --git a/packages/thirdweb/src/adapters/eip1193/to-eip1193.ts b/packages/thirdweb/src/adapters/eip1193/to-eip1193.ts index 21621cd8080..6480d4e03e7 100644 --- a/packages/thirdweb/src/adapters/eip1193/to-eip1193.ts +++ b/packages/thirdweb/src/adapters/eip1193/to-eip1193.ts @@ -64,100 +64,138 @@ export function toProvider(options: ToEip1193ProviderOptions): EIP1193Provider { // should invoke the return fn from subscribe instead }, request: async (request) => { - if (request.method === "eth_sendTransaction") { - const account = wallet.getAccount(); - if (!account) { - throw new Error("Account not connected"); + switch (request.method) { + case "eth_sendTransaction": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + const result = await sendTransaction({ + account: account, + transaction: prepareTransaction({ + ...request.params[0], + chain, + client, + }), + }); + return result.transactionHash; } - const result = await sendTransaction({ - account: account, - transaction: prepareTransaction({ - ...request.params[0], - chain, - client, - }), - }); - return result.transactionHash; - } - if (request.method === "eth_estimateGas") { - const account = wallet.getAccount(); - if (!account) { - throw new Error("Account not connected"); + case "eth_estimateGas": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + return estimateGas({ + account, + transaction: prepareTransaction({ + ...request.params[0], + chain, + client, + }), + }); } - return estimateGas({ - account, - transaction: prepareTransaction({ - ...request.params[0], - chain, - client, - }), - }); - } - if (request.method === "personal_sign") { - const account = wallet.getAccount(); - if (!account) { - throw new Error("Account not connected"); + case "personal_sign": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + return account.signMessage({ + message: { + raw: request.params[0], + }, + }); } - return account.signMessage({ - message: { - raw: request.params[0], - }, - }); - } - if (request.method === "eth_signTypedData_v4") { - const account = wallet.getAccount(); - if (!account) { - throw new Error("Account not connected"); + case "eth_signTypedData_v4": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + const data = JSON.parse(request.params[1]); + return account.signTypedData(data); } - const data = JSON.parse(request.params[1]); - return account.signTypedData(data); - } - if (request.method === "eth_accounts") { - const account = wallet.getAccount(); - if (!account) { - return []; + case "eth_accounts": { + const account = wallet.getAccount(); + if (!account) { + return []; + } + return [account.address]; } - return [account.address]; - } - if (request.method === "eth_requestAccounts") { - const connectedAccount = wallet.getAccount(); - if (connectedAccount) { - return [connectedAccount.address]; + case "eth_requestAccounts": { + const connectedAccount = wallet.getAccount(); + if (connectedAccount) { + return [connectedAccount.address]; + } + const account = connectOverride + ? await connectOverride(wallet) + : await wallet + .connect({ + client, + }) + .catch((e) => { + console.error("Error connecting wallet", e); + return null; + }); + if (!account) { + throw new Error( + "Unable to connect wallet - try passing a connectOverride function", + ); + } + return [account.address]; } - const account = connectOverride - ? await connectOverride(wallet) - : await wallet - .connect({ - client, - }) - .catch((e) => { - console.error("Error connecting wallet", e); - return null; - }); - if (!account) { - throw new Error( - "Unable to connect wallet - try passing a connectOverride function", - ); + case "wallet_switchEthereumChain": + case "wallet_addEthereumChain": { + const data = request.params[0]; + const chainIdHex = data.chainId; + if (!chainIdHex) { + throw new Error("Chain ID is required"); + } + // chainId is hex most likely, convert to number + const chainId = isHex(chainIdHex) + ? hexToNumber(chainIdHex) + : chainIdHex; + const chain = getCachedChain(chainId); + return wallet.switchChain(chain); } - return [account.address]; - } - if ( - request.method === "wallet_switchEthereumChain" || - request.method === "wallet_addEthereumChain" - ) { - const data = request.params[0]; - const chainIdHex = data.chainId; - if (!chainIdHex) { - throw new Error("Chain ID is required"); + case "wallet_getCapabilities": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + if (!account.getCapabilities) { + throw new Error("Wallet does not support EIP-5792"); + } + return account.getCapabilities({ chainId: chain.id }); + } + case "wallet_sendCalls": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + if (!account.sendCalls) { + throw new Error("Wallet does not support EIP-5792"); + } + return account.sendCalls({ + ...request.params[0], + chain: chain, + }); + } + case "wallet_getCallsStatus": { + const account = wallet.getAccount(); + if (!account) { + throw new Error("Account not connected"); + } + if (!account.getCallsStatus) { + throw new Error("Wallet does not support EIP-5792"); + } + return account.getCallsStatus({ + id: request.params[0], + chain: chain, + client: client, + }); } - // chainId is hex most likely, convert to number - const chainId = isHex(chainIdHex) - ? hexToNumber(chainIdHex) - : chainIdHex; - const chain = getCachedChain(chainId); - return wallet.switchChain(chain); + default: + return rpcClient(request); } - return rpcClient(request); }, }; } diff --git a/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts b/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts index 5944308e28a..45529af57f1 100644 --- a/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts +++ b/packages/thirdweb/src/wallets/in-app/core/eip5792/in-app-wallet-calls.ts @@ -4,6 +4,7 @@ import { eth_getTransactionReceipt } from "../../../../rpc/actions/eth_getTransa import { getRpcClient } from "../../../../rpc/rpc.js"; import { sendAndConfirmTransaction } from "../../../../transaction/actions/send-and-confirm-transaction.js"; import { sendBatchTransaction } from "../../../../transaction/actions/send-batch-transaction.js"; +import type { SendTransactionOptions } from "../../../../transaction/actions/send-transaction.js"; import { LruMap } from "../../../../utils/caching/lru.js"; import type { Hex } from "../../../../utils/encoding/hex.js"; import { randomBytesHex } from "../../../../utils/random.js"; @@ -22,21 +23,29 @@ const bundlesToTransactions = new LruMap(1000); export async function inAppWalletSendCalls(args: { account: Account; calls: PreparedSendCall[]; + chain: Chain; }): Promise { const { account, calls } = args; + const transactions: SendTransactionOptions["transaction"][] = calls.map( + (call) => ({ + ...call, + chain: args.chain, + }), + ); + const hashes: Hex[] = []; const id = randomBytesHex(65); bundlesToTransactions.set(id, hashes); if (account.sendBatchTransaction) { const receipt = await sendBatchTransaction({ account, - transactions: calls, + transactions, }); hashes.push(receipt.transactionHash); bundlesToTransactions.set(id, hashes); } else { - for (const tx of calls) { + for (const tx of transactions) { const receipt = await sendAndConfirmTransaction({ account, transaction: tx, 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 b0bc1a044b4..efba63ba33a 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 @@ -341,6 +341,7 @@ export const create7702MinimalAccount = (args: { const id = await inAppWalletSendCalls({ account: minimalAccount, calls: options.calls, + chain, }); return { chain, client, id }; }, 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 b94bc9c5ba2..519884e59c7 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 @@ -277,6 +277,7 @@ export class EnclaveWallet implements IWebWallet { const id = await inAppWalletSendCalls({ account: account, calls: options.calls, + chain, }); return { chain, client, id }; }, diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index 89d078b683e..76b1645e9c1 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -379,6 +379,7 @@ async function createSmartAccount( const id = await inAppWalletSendCalls({ account: account, calls: options.calls, + chain, }); return { chain, client, id }; }, @@ -556,6 +557,7 @@ function createZkSyncAccount(args: { const id = await inAppWalletSendCalls({ account: account, calls: options.calls, + chain, }); return { chain, client, id }; },