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
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.
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.
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 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
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.
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
postMessage
containingETH_ACCOUNTS
. - Resolves the
eth_requestAccounts
promise 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.