Integrating an Embedded Wallet with Wagmi
Introduction
Turnkey wallets are embedded, web-based wallets that differ from injected wallets (like MetaMask). While injected wallets store private keys locally and decrypt them using a password to sign transactions, embedded wallets rely on UI-based authentication to access private keys that are securely stored and managed by Turnkey.
With this concept in mind, we’re going to build a custom Wagmi connector that communicates with an embedded wallet rendered in a popup, enabling integration across multiple dApps.
System Components Overview
Our system involves three key parts working together:
Embedded Wallet (Pop-up): A web application (likely React/Next.js) hosted by you.
This UI handles user authentication (passkeys via Turnkey), transaction signing, and communication with the dApp via postMessage
.
It securely interacts with the Turnkey API. Reference the popup-wallet-demo
’s @/apps/wallet
provides a concrete example.
EIP-1193 Provider: A JavaScript class implementing the EIP-1193 standard.
It acts as the intermediary between the dApp and the popup embedded wallet. Reference the popup-wallet-demo
’s @/apps/dapp/lib/eip1193-provider.ts
provides a concrete example.
Wagmi Connector: A custom connector built using Wagmi’s createConnector
utility. It wraps our EIP-1193 provider, making the wallet compatible with the Wagmi ecosystem. Reference the popup-wallet-demo
’s @/apps/dapp/lib/connector.ts
and @/apps/dapp/lib/wagmi.ts
provide concrete examples.
Architecture Flow
The interaction sequence generally follows these steps:
Connection:
- A user on a dApp clicks “Connect Wallet” and selects your wallet.
- The dApp calls the
connect
method on your Wagmi connector. - The connector initializes the EIP-1193 provider.
- The connector calls
provider.request({ method: 'eth_requestAccounts' })
, which opens your Embedded Wallet pop-up. - The user authenticates in the pop-up and chooses which wallet account to connect.
- The pop-up returns the selected account(s) and
chainId
to the provider viapostMessage
. - The provider resolves
eth_requestAccounts
, and the connector returns the account(s) andchainId
to the dApp.
RPC Request (e.g., eth_sendTransaction
):
- The dApp uses a Wagmi hook (e.g.,
useSendTransaction
) which triggers a request. - Wagmi sends the
eth_sendTransaction
request to your connector. - The connector forwards the request to the EIP-1193 provider.
- The provider identifies this as a signing request and opens the Embedded Wallet pop-up (if not already open), sending the transaction details via
postMessage
. - The user reviews and approves the transaction in the pop-up.
- The pop-up uses Turnkey to sign the transaction and potentially broadcast it (or return the signed transaction).
- The pop-up sends the transaction hash (or signed transaction) back to the provider via
postMessage
. - The provider resolves the request promise, returning the result to the dApp via the connector.
RPC Request (e.g., eth_blockNumber
):
- The dApp triggers a read-only request.
- Wagmi sends the
eth_blockNumber
request to your connector. - The connector forwards it to the EIP-1193 provider.
- The provider identifies this as a read-only request and forwards it directly to a public RPC node.
- The public RPC node returns the result.
- The provider returns the result to the dApp via the connector.
Flow Diagram
Connect flow demo
Building the Embedded Wallet (Pop-up)
The embedded wallet is a standalone web app (often a separate Next.js project)
that is opened in a pop-up when the provider executes eth_requestAccounts
.
Create the wallet page
Below is a minimal wallet UI plus a stubbed authentication button, shown side-by-side with Mintlify’s <CodeGroup>
component so you can copy either file.
How it works
- The provider opens the pop-up at
https://<your-wallet-host>/?request=<encoded-json-rpc>
. - The page decodes the
request
query parameter.
For the connection flow this will look like: - When the user clicks Connect Wallet the
AuthButton
performs your authentication logic (Turnkey passkey, email-magic-link, etc.). - Once authenticated, the wallet sends a
postMessage
back to the opener containing the selected account(s) andchainId
.
We will wire up that message handling in the next section.
With this single page in place you now have a functional authentication UI that the provider can open, fulfilling the first half of the connection flow:
Next we will implement the message bridge in the provider and then expand the wallet to handle transaction and message signing.
Post the account back to the opener
Inside your AuthButton
(or wherever your auth logic resolves) send a message with the newly authenticated account:
This keeps the wallet UI decoupled from the provider implementation—any parent window that understands the
ETH_ACCOUNTS
message can integrate.
Implement a minimal EIP-1193 provider
Create eip1193-provider.ts
in your dApp project. For the connection flow we only need to implement eth_requestAccounts
and eth_accounts
:
This provider:
- Caches accounts after the first successful connection.
- Opens the wallet pop-up and waits for a
postMessage
containingETH_ACCOUNTS
. - Resolves the
eth_requestAccounts
promise and lets Wagmi continue.
Wire it up in a Wagmi connector
Your minimal connector only needs the connect
method to use the provider you just built. The full version from popup-wallet-demo
already includes this; here’s the shortened core for reference:
At this point you can:
- Run your wallet project on
localhost:3001
. - Integrate the minimal provider + connector in a dApp.
- Click Connect Wallet in the dApp → the pop-up opens → authenticate → dApp receives the account.
🎉 You now have a working end-to-end connection flow! Next we’ll extend both the wallet UI and the provider to support signing transactions and messages.
Add transaction and message signing
Connection works. Next, implement signing so the dApp can call useSendTransaction
, useSignMessage
, and related Wagmi hooks.
Wallet-side components
Provider extensions
Augment the provider so signing requests are routed through the popup.
popupRequest
is the existing helper that opens/targets the window and resolves the corresponding RPC_RESPONSE
.
Summary
You now handle:
• Connection (eth_requestAccounts
).
• Transaction signing (eth_signTransaction
/ eth_sendTransaction
).
• Message signing (personal_sign
/ eth_sign
).
From here you can layer additional EIP-1193 methods (chain switching, asset watch, etc.) as needed.
Production considerations
The code samples target a minimal, easy-to-understand prototype. When moving to production, address the following:
• Request IDs & queueing – generate a unique id
per RPC call, store promises by id
, include it in every postMessage
so concurrent requests cannot collide.
• Session persistence – cache accounts
and chainId
in localStorage
and return them on subsequent eth_accounts
calls without re-authenticating.
• Popup cancellation & timeouts – reject pending promises if the user closes the window or after a reasonable timeout.
• Network switching – implement wallet_switchEthereumChain
, update store.chainId
, and emit chainChanged
.
• Security – validate event.origin
, allow-list dApp domains, move hard-coded URLs (localhost:3001
, RPC endpoints) into environment variables.
• Additional EIPs – support wallet_watchAsset
, eth_addEthereumChain
, etc., if dApps require them.
Addressing these items will bring the prototype to production-ready quality without altering the core architecture documented above.