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 viapostMessage.
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 or wagmi-demo’s @/src/lib/connector.ts and @/src/lib/wagmi.ts for 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
connectmethod 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
chainIdto the provider viapostMessage. - The provider resolves
eth_requestAccounts, and the connector returns the account(s) andchainIdto the dApp.
eth_sendTransaction):
- The dApp uses a Wagmi hook (e.g.,
useSendTransaction) which triggers a request. - Wagmi sends the
eth_sendTransactionrequest 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.
eth_blockNumber):
- The dApp triggers a read-only request.
- Wagmi sends the
eth_blockNumberrequest 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 executeseth_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.
- The provider opens the pop-up at
https://<your-wallet-host>/?request=<encoded-json-rpc>. - The page decodes the
requestquery parameter.
For the connection flow this will look like: - When the user clicks Connect Wallet the
AuthButtonperforms your authentication logic (Turnkey passkey, email-magic-link, etc.). - Once authenticated, the wallet sends a
postMessageback to the opener containing the selected account(s) andchainId.
We will wire up that message handling in the next section.
Post the account back to the opener
Inside yourAuthButton (or wherever your auth logic resolves) send a message with the newly authenticated account:
components/auth.tsx
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
Createeip1193-provider.ts in your dApp project. For the connection flow we only need to implement eth_requestAccounts and eth_accounts:
eip1193-provider.ts
- Caches accounts after the first successful connection.
- Opens the wallet pop-up and waits for a
postMessagecontainingETH_ACCOUNTS. - Resolves the
eth_requestAccountspromise and lets Wagmi continue.
Wire it up in a Wagmi connector
Your minimal connector only needs theconnect 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:
connector.ts
- 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.
Add transaction and message signing
Connection works. Next, implement signing so the dApp can calluseSendTransaction, useSignMessage, and related Wagmi hooks.
Wallet-side components
Provider extensions
Augment the provider so signing requests are routed through the popup.eip1193-provider.ts (diff)
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 uniqueid 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.