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.
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.
The interaction sequence generally follows these steps:
Connection:
connect
method on your Wagmi connector.provider.request({ method: 'eth_requestAccounts' })
, which opens your Embedded Wallet pop-up.chainId
to the provider via postMessage
.eth_requestAccounts
, and the connector returns the account(s) and chainId
to the dApp.RPC Request (e.g., eth_sendTransaction
):
useSendTransaction
) which triggers a request.eth_sendTransaction
request to your connector.postMessage
.postMessage
.RPC Request (e.g., eth_blockNumber
):
eth_blockNumber
request to your connector.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
.
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
https://<your-wallet-host>/?request=<encoded-json-rpc>
.request
query parameter.AuthButton
performs your authentication logic (Turnkey passkey, email-magic-link, etc.).postMessage
back to the opener containing the selected account(s) and chainId
.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.
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.
Create eip1193-provider.ts
in your dApp project. For the connection flow we only need to implement eth_requestAccounts
and eth_accounts
:
This provider:
postMessage
containing ETH_ACCOUNTS
.eth_requestAccounts
promise and lets Wagmi continue.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:
localhost:3001
.🎉 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.
Connection works. Next, implement signing so the dApp can call useSendTransaction
, useSignMessage
, and related Wagmi hooks.
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
.
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.
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.
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.
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.
The interaction sequence generally follows these steps:
Connection:
connect
method on your Wagmi connector.provider.request({ method: 'eth_requestAccounts' })
, which opens your Embedded Wallet pop-up.chainId
to the provider via postMessage
.eth_requestAccounts
, and the connector returns the account(s) and chainId
to the dApp.RPC Request (e.g., eth_sendTransaction
):
useSendTransaction
) which triggers a request.eth_sendTransaction
request to your connector.postMessage
.postMessage
.RPC Request (e.g., eth_blockNumber
):
eth_blockNumber
request to your connector.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
.
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
https://<your-wallet-host>/?request=<encoded-json-rpc>
.request
query parameter.AuthButton
performs your authentication logic (Turnkey passkey, email-magic-link, etc.).postMessage
back to the opener containing the selected account(s) and chainId
.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.
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.
Create eip1193-provider.ts
in your dApp project. For the connection flow we only need to implement eth_requestAccounts
and eth_accounts
:
This provider:
postMessage
containing ETH_ACCOUNTS
.eth_requestAccounts
promise and lets Wagmi continue.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:
localhost:3001
.🎉 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.
Connection works. Next, implement signing so the dApp can call useSendTransaction
, useSignMessage
, and related Wagmi hooks.
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
.
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.
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.