In this guide, we’ll explore how to leverage the WalletClient in the Turnkey SDK to authenticate requests to Turnkey’s API using either Solana or Ethereum wallets.
Begin by initializing the Turnkey SDK by passing in a config object containing:
apiBaseUrl: The base URL of the Turnkey API: https://api.turnkey.com.
defaultOrganizationId: Your parent organization ID, which you can find in the Turnkey dashboard.
wallet: The wallet interface used to sign requests. In this example, we’ll use the EthereumWallet interface.
config.ts
Copy
Ask AI
import { EthereumWallet } from '@turnkey/wallet-stamper';export const turnkeyConfig = { // Turnkey API base URL apiBaseUrl: 'https://api.turnkey.com', // Your parent organization ID defaultOrganizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!, // The wallet interface used to sign requests wallet: new EthereumWallet(),};
First, wrap your application with the TurnkeyProvider in your app/layout.tsx file.
As this file is required by Next.js to be a server component, we need to define a TurnkeyClientProvider client component.
app/TurnkeyClientProvider.tsx
Copy
Ask AI
'use client';import { TurnkeyProvider } from '@turnkey/sdk-react';import { turnkeyConfig } from './config';export function TurnkeyClientProvider({ children,}: { children: React.ReactNode;}) { return <TurnkeyProvider config={turnkeyConfig}>{children}</TurnkeyProvider>;}
Then, create a new page component app/page.tsx where we’ll implement the wallet authentication functionality:
app/page.tsx
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";export default function WalletAuth() { const { walletClient } = useTurnkey();// We'll add more functionality here in the following stepsreturn <div>{/* We'll add UI elements here */}</div>;}
app/page.tsx
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";export default function WalletAuth() { const { walletClient } = useTurnkey();// We'll add more functionality here in the following stepsreturn <div>{/* We'll add UI elements here */}</div>;}
Create a new file src/wallet-auth.ts where we’ll implement the wallet authentication functionality:
src/wallet-auth.ts
Copy
Ask AI
import { Turnkey } from "@turnkey/sdk-browser";import { EthereumWallet } from "@turnkey/wallet-stamper";import { turnkeyConfig } from "./config";// Initialize the Turnkey SDK with the config object defined aboveconst turnkey = new Turnkey(turnkeyConfig);// Initialize the Wallet Client with the EthereumWallet interfaceconst walletClient = turnkey.walletClient(new EthereumWallet());// We'll add more functionality here in the following steps
In this section, we’ll guide you through the process of implementing a sign-up flow using an Ethereum wallet for authentication. The sign-up process involves creating a new sub-organization within your existing organization. This requires authentication of the parent organization using its public/private key pair. Additionally, we’ll cover how to verify if a user already has an associated sub-organization before proceeding.
Initialize the Turnkey SDK on the server-side using the @turnkey/sdk-server package. This setup enables you to authenticate requests to Turnkey’s API using the parent organization’s public/private API key pair. This is required to create new sub-organizations on behalf of a user.
For Next.js, add the "use server" directive at the top of the file where you’re initializing the Turnkey server client. This will ensure that the function is executed on the server-side and will have access to the server-side environment variables e.g. your parent organization’s public/private API key pair. For more information on Next.js server actions, see the Next.js documentation on Server Actions and Mutations.
app/actions.ts
Copy
Ask AI
'use server';import { Turnkey } from '@turnkey/sdk-server';import { turnkeyConfig } from './config';const { apiBaseUrl, defaultOrganizationId } = turnkeyConfig;// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({ apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!, apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!, apiBaseUrl, defaultOrganizationId,}).apiClient();
For Next.js, add the "use server" directive at the top of the file where you’re initializing the Turnkey server client. This will ensure that the function is executed on the server-side and will have access to the server-side environment variables e.g. your parent organization’s public/private API key pair. For more information on Next.js server actions, see the Next.js documentation on Server Actions and Mutations.
app/actions.ts
Copy
Ask AI
'use server';import { Turnkey } from '@turnkey/sdk-server';import { turnkeyConfig } from './config';const { apiBaseUrl, defaultOrganizationId } = turnkeyConfig;// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({ apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!, apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!, apiBaseUrl, defaultOrganizationId,}).apiClient();
src/wallet-auth-server.ts
Copy
Ask AI
import { Turnkey } from "@turnkey/sdk-server";import { turnkeyConfig } from "./config";const { apiBaseUrl, defaultOrganizationId } = turnkeyConfig;// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,apiBaseUrl,defaultOrganizationId,}).apiClient();
Before signing up a new user, we can try and retrieve the user’s sub-organization ID using the public key associated with the Ethereum or Solana account they want to authenticate with. If a sub-organization is found, we can proceed with authentication; otherwise, we assume the user is signing up.
We’ll use the getPublicKey method on the WalletClient instance which will retrieve the public key from the user’s wallet.
The main distinction between signing with an Ethereum Wallet and a Solana Wallet lies in how the public key is obtained. For Solana, the public key can be directly derived from the wallet. In contrast, with Ethereum, the secp256k1 public key isn’t directly accessible. Instead, you need to first obtain a signature from the user and then recover the public key from that signature. This requires an additional step of signing a message with the user’s Ethereum wallet before we can retrieve the public key.
We’ll define this function in the server-side code we initialized earlier.
Next, we’ll add the client-side functionality to the app/page.tsx file we created earlier importing the getSubOrg function we defined in our server action. We’ll use the getSubOrg function in the login method to check if a user already has a sub-organization.
app/page.tsx
Copy
Ask AI
'use client';import { useState } from 'react';import { useTurnkey } from '@turnkey/sdk-react';// Import the getSubOrg function we defined earlierimport { getSubOrg } from './actions';export default function WalletAuth() { const { walletClient } = useTurnkey();const login = async () => {// Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a messageconst publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { // User does not have a sub-organization, proceed with sign-up } // User has a sub-organization, proceed with login};return (<div><button onClick={login}>Sign In</button></div>);}
app/page.tsx
Copy
Ask AI
'use client';import { useState } from 'react';import { useTurnkey } from '@turnkey/sdk-react';// Import the getSubOrg function we defined earlierimport { getSubOrg } from './actions';export default function WalletAuth() { const { walletClient } = useTurnkey();const login = async () => {// Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a messageconst publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { // User does not have a sub-organization, proceed with sign-up } // User has a sub-organization, proceed with login};return (<div><button onClick={login}>Sign In</button></div>);}
src/wallet-auth.ts
Copy
Ask AI
import { getSubOrg } from './wallet-auth-server';// ...const walletClient = turnkey.walletClient(new EthereumWallet());export const login = async () => { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { // User does not have a sub-organization, proceed with sign-up } // User has a sub-organization, proceed with login};
We’ll define another server action createSubOrg to create a sub-organization for new user sign-ups.
app/actions.ts
Copy
Ask AI
'use server';// ...// Import the default Ethereum accounts helperimport { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';export const createSubOrg = async ( publicKey: string, curveType: 'API_KEY_CURVE_ED25519' | 'API_KEY_CURVE_SECP256K1') => { const apiKeys = [ { apiKeyName: `Wallet Auth - ${publicKey}`, // The public key of the wallet that will be added as an API key and used to stamp future requests publicKey, // We set the curve type to 'API_KEY_CURVE_ED25519' for solana wallets // If using an Ethereum wallet, set the curve type to 'API_KEY_CURVE_SECP256K1' curveType, }, ]; const subOrg = await turnkeyServer.createSubOrganization({ // The parent organization ID organizationId: turnkeyConfig.defaultOrganizationId, subOrganizationName: 'New Sub Org', rootUsers: [ { // Replace with user provided values if desired userName: 'New User', userEmail: 'wallet@domain.com', apiKeys, authenticators: [], oauthProviders: [], }, ], rootQuorumThreshold: 1, wallet: { walletName: 'Default Wallet', // This is used to create a new Ethereum wallet for the sub-organization accounts: DEFAULT_ETHEREUM_ACCOUNTS, }, }); return subOrg;};
Then, we’ll import and use this createSubOrg function within the login method. The curve type is set to API_KEY_CURVE_SECP256K1 since we’re using an Ethereum wallet in this example.
app/page.tsx
Copy
Ask AI
'use client';import { getSubOrg, createSubOrg } from './actions';// ...export default function WalletAuth() { const { walletClient } = useTurnkey(); const login = async () => { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); } // In the next step we'll sign in the user }; return ( <div> <button onClick={login}>Sign In</button> </div> );}
We’ll define another server action createSubOrg to create a sub-organization for new user sign-ups.
app/actions.ts
Copy
Ask AI
'use server';// ...// Import the default Ethereum accounts helperimport { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';export const createSubOrg = async ( publicKey: string, curveType: 'API_KEY_CURVE_ED25519' | 'API_KEY_CURVE_SECP256K1') => { const apiKeys = [ { apiKeyName: `Wallet Auth - ${publicKey}`, // The public key of the wallet that will be added as an API key and used to stamp future requests publicKey, // We set the curve type to 'API_KEY_CURVE_ED25519' for solana wallets // If using an Ethereum wallet, set the curve type to 'API_KEY_CURVE_SECP256K1' curveType, }, ]; const subOrg = await turnkeyServer.createSubOrganization({ // The parent organization ID organizationId: turnkeyConfig.defaultOrganizationId, subOrganizationName: 'New Sub Org', rootUsers: [ { // Replace with user provided values if desired userName: 'New User', userEmail: 'wallet@domain.com', apiKeys, authenticators: [], oauthProviders: [], }, ], rootQuorumThreshold: 1, wallet: { walletName: 'Default Wallet', // This is used to create a new Ethereum wallet for the sub-organization accounts: DEFAULT_ETHEREUM_ACCOUNTS, }, }); return subOrg;};
Then, we’ll import and use this createSubOrg function within the login method. The curve type is set to API_KEY_CURVE_SECP256K1 since we’re using an Ethereum wallet in this example.
app/page.tsx
Copy
Ask AI
'use client';import { getSubOrg, createSubOrg } from './actions';// ...export default function WalletAuth() { const { walletClient } = useTurnkey(); const login = async () => { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); } // In the next step we'll sign in the user }; return ( <div> <button onClick={login}>Sign In</button> </div> );}
We’ll define another server-side function createSubOrg, to create a sub-organization for new user sign-ups.
src/wallet-auth-server.ts
Copy
Ask AI
// ...// Import the default Ethereum accounts helperimport { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';export const createSubOrg = async ( publicKey: string, curveType: 'API_KEY_CURVE_ED25519' | 'API_KEY_CURVE_SECP256K1') => { const apiKeys = [ { apiKeyName: `Wallet Auth - ${publicKey}`, // The public key of the wallet that will be added as an API key and used to stamp future requests publicKey, // We set the curve type to 'API_KEY_CURVE_ED25519' for solana wallets // If using an Ethereum wallet, set the curve type to 'API_KEY_CURVE_SECP256K1' curveType, }, ]; const subOrg = await turnkeyServer.createSubOrganization({ // The parent organization ID organizationId: turnkeyConfig.defaultOrganizationId, subOrganizationName: 'New Sub Org', rootUsers: [ { // Replace with user provided values if desired userName: 'New User', userEmail: 'wallet@domain.com', apiKeys, authenticators: [], oauthProviders: [], }, ], rootQuorumThreshold: 1, wallet: { walletName: 'Default Wallet', // This is used to create a new Ethereum wallet for the sub-organization accounts: DEFAULT_ETHEREUM_ACCOUNTS, }, }); return subOrg;};
Then, we’ll import and use this createSubOrg function within the login method. The curve type is set to API_KEY_CURVE_SECP256K1 since we’re using an Ethereum wallet in this example.
src/wallet-auth.ts
Copy
Ask AI
import { getSubOrg, createSubOrg } from './wallet-auth-server';// ...const walletClient = turnkey.walletClient(new EthereumWallet());export const login = async () => { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); } // In the next step we'll sign in the user};
At this point, we have a working sign-up flow. Next, we’ll implement the signing in functionality by creating a read-write session, retrieving the user’s wallets and adding a new one.
Create a read-write session for the user by calling the loginWithWallet method on the WalletClient instance which will use a newly generated indexedDb API key. This will save a read-write session token to the localStorage to authenticate future read-write requests.
app/page.tsx
Copy
Ask AI
'use client';import { useState } from 'react';import { useTurnkey } from '@turnkey/sdk-react';import { getSubOrg, createSubOrg } from './actions';import { SessionType } from '@turnkey/sdk-types';import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';export default function WalletAuth() { const [wallets, setWallets] = useState<any[]>([]); const [session, setSession] = useState<any | null>(null); const { walletClient, indexedDbClient, turnkey } = useTurnkey(); const login = async () => { try { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) throw new Error('No public key found'); if (!walletClient) { throw new Error('Wallet client not initialized'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); console.log('Sub-Organization created:', subOrg); } if (!indexedDbClient) throw new Error('IndexedDb client not available'); // Reset the indexedDb key pair and session before each login // Note that session reset is important when switching between multiple wallets within the same browser await turnkey?.logout(); await client?.clear(); await indexedDbClient.resetKeyPair(); const pubKey = await indexedDbClient.getPublicKey(); await walletClient!.loginWithWallet({ sessionType: SessionType.READ_WRITE, // use SessionType.READ_ONLY for read-only sessions publicKey: pubKey!, }); console.log('Login successful'); const session = await turnkey?.getSession(); setSession(session); const subOrganizationId = session!.organizationId; // get existing suborg wallets const wallets = await indexedDbClient.getWallets({ organizationId: subOrgId!, }); setWallets(wallets.wallets); // create a new wallet with an Ethereum wallet account const newWalletResponse = await indexedDbClient.createWallet({ walletName: 'New Wallet 1', accounts: DEFAULT_ETHEREUM_ACCOUNTS, }); console.log('Created new wallet:', newWalletResponse); const updatedWallets = await indexedDbClient.getWallets({ organizationId: subOrganizationId, }); setWallets(updatedWallets.wallets); } catch (err) { console.error('Login error:', err); } }; return ( <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6"> <h2 className="text-xl font-bold mb-4 text-gray-800"> Turnkey Wallet Auth </h2> {/* If logged in: Show wallets */} {session && wallets.length > 0 && ( <div className="space-y-4 mb-6"> <h3 className="text-lg font-semibold text-gray-700">🧾 Wallets</h3> {wallets.map((wallet) => ( <div key={wallet.walletId} className="border border-gray-200 rounded-md p-3 bg-gray-50 text-sm" > <div className="font-medium text-gray-800"> {wallet.walletName} </div> <div className="text-gray-500 text-xs"> Wallet ID: {wallet.walletId} </div> </div> ))} </div> )} {/* If not logged in: Show Sign In */} {walletClient && !session && ( <button onClick={login} className="w-full sm:w-auto bg-gray-700 hover:bg-gray-800 text-white font-semibold py-2 px-4 rounded-md transition" > Sign In </button> )} </div> </div> );}
app/page.tsx
Copy
Ask AI
'use client';import { useState } from 'react';import { useTurnkey } from '@turnkey/sdk-react';import { getSubOrg, createSubOrg } from './actions';import { SessionType } from '@turnkey/sdk-types';import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';export default function WalletAuth() { const [wallets, setWallets] = useState<any[]>([]); const [session, setSession] = useState<any | null>(null); const { walletClient, indexedDbClient, turnkey } = useTurnkey(); const login = async () => { try { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) throw new Error('No public key found'); if (!walletClient) { throw new Error('Wallet client not initialized'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); console.log('Sub-Organization created:', subOrg); } if (!indexedDbClient) throw new Error('IndexedDb client not available'); // Reset the indexedDb key pair and session before each login // Note that session reset is important when switching between multiple wallets within the same browser await turnkey?.logout(); await client?.clear(); await indexedDbClient.resetKeyPair(); const pubKey = await indexedDbClient.getPublicKey(); await walletClient!.loginWithWallet({ sessionType: SessionType.READ_WRITE, // use SessionType.READ_ONLY for read-only sessions publicKey: pubKey!, }); console.log('Login successful'); const session = await turnkey?.getSession(); setSession(session); const subOrganizationId = session!.organizationId; // get existing suborg wallets const wallets = await indexedDbClient.getWallets({ organizationId: subOrgId!, }); setWallets(wallets.wallets); // create a new wallet with an Ethereum wallet account const newWalletResponse = await indexedDbClient.createWallet({ walletName: 'New Wallet 1', accounts: DEFAULT_ETHEREUM_ACCOUNTS, }); console.log('Created new wallet:', newWalletResponse); const updatedWallets = await indexedDbClient.getWallets({ organizationId: subOrganizationId, }); setWallets(updatedWallets.wallets); } catch (err) { console.error('Login error:', err); } }; return ( <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6"> <h2 className="text-xl font-bold mb-4 text-gray-800"> Turnkey Wallet Auth </h2> {/* If logged in: Show wallets */} {session && wallets.length > 0 && ( <div className="space-y-4 mb-6"> <h3 className="text-lg font-semibold text-gray-700">🧾 Wallets</h3> {wallets.map((wallet) => ( <div key={wallet.walletId} className="border border-gray-200 rounded-md p-3 bg-gray-50 text-sm" > <div className="font-medium text-gray-800"> {wallet.walletName} </div> <div className="text-gray-500 text-xs"> Wallet ID: {wallet.walletId} </div> </div> ))} </div> )} {/* If not logged in: Show Sign In */} {walletClient && !session && ( <button onClick={login} className="w-full sm:w-auto bg-gray-700 hover:bg-gray-800 text-white font-semibold py-2 px-4 rounded-md transition" > Sign In </button> )} </div> </div> );}
src/wallet-auth.ts
Copy
Ask AI
import { Turnkey } from '@turnkey/sdk-browser';import { EthereumWallet } from '@turnkey/wallet-stamper';import { turnkeyConfig } from './config';import { getSubOrg, createSubOrg } from './wallet-auth-server';import { SessionType } from '@turnkey/sdk-types';import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';// Initialize the Turnkey SDK with the config object defined aboveconst turnkey = new Turnkey(turnkeyConfig);// Initialize the Wallet Client with the EthereumWallet interfaceconst walletClient = turnkey.walletClient(new EthereumWallet());export const login = async () => { try { // Get the public key of the wallet, for Ethereum wallets this will trigger a prompt for the user to sign a message const publicKey = await walletClient?.getPublicKey(); if (!publicKey) { throw new Error('No public key found'); } if (!walletClient) { throw new Error('Wallet client not initialized'); } const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_SECP256K1' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); console.log('Sub-Organization created:', subOrg); } // Initialize the indexedDbClient const client = await turnkey.indexedDbClient(); if (!client) { throw new Error('indexedDbClient not initialized'); } // Reset the indexedDb key pair and session before each login // Note that session reset is important when switching between multiple wallets within the same browser await turnkey?.logout(); await client?.clear(); await client!.resetKeyPair(); const pubKey = await client!.getPublicKey(); await walletClient!.loginWithWallet({ sessionType: SessionType.READ_WRITE, // use SessionType.READ_ONLY for read-only sessions publicKey: pubKey!, }); console.log('Login successful'); const session = await turnkey?.getSession(); const subOrganizationId = session!.organizationId; // get existing suborg wallets const wallets = await client.getWallets({ organizationId: subOrgId!, }); // create a new wallet with an Ethereum wallet account const newWalletResponse = await client.createWallet({ walletName: 'New Wallet 1', accounts: DEFAULT_ETHEREUM_ACCOUNTS, }); console.log('Created new wallet:', newWalletResponse); const updatedWallets = await client.getWallets({ organizationId: subOrganizationId, }); } catch (err) { console.error('Login error:', err); }};
As with Solana wallets there’s not standard API like personal_sign for Ethereum, we’ll need to build a couple of things:
Use the Turnkey SolanaWalletInterface to build our own SolanaWallet() function that would get the public key and sign a message. Create this new SolanaWalletFactory.ts component:
app/SolanaWalletFactory.ts
Copy
Ask AI
// This wrapper implements SolanaWalletInterface for WalletStamperimport { WalletType, SolanaWalletInterface } from "@turnkey/wallet-stamper";export function SolanaWallet(wallet: { publicKey: { toBytes(): Uint8Array } | null; signMessage?: (msg: Uint8Array) => Promise<Uint8Array>;}): SolanaWalletInterface { return { type: WalletType.Solana, async getPublicKey() { if (!wallet.publicKey) throw new Error("No public key"); return Buffer.from(wallet.publicKey.toBytes()).toString("hex"); }, async signMessage(message: string) { if (!wallet.signMessage) { throw new Error("Wallet does not support signMessage"); } const encoded = new TextEncoder().encode(message); const signature = await wallet.signMessage(encoded); return Buffer.from(signature).toString("hex"); }, };}
Use the Solana wallet-addapter to detect and connect the installed wallets. Create this SolanaWalletProvider.tsx component:
app/SolanaWalletProvider.tsx
Copy
Ask AI
"use client";import { FC, ReactNode } from "react";import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";export const SolanaWalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => { const network = WalletAdapterNetwork.Mainnet; const endpoint = "https://api.mainnet-beta.solana.com"; return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={[]} autoConnect> // you can add adapters for walllets not auto-detected here <WalletModalProvider>{children}</WalletModalProvider> </WalletProvider> </ConnectionProvider> );};
Update the layout.tsx file:
app/layout.tsx
Copy
Ask AI
import "./globals.css";import "@solana/wallet-adapter-react-ui/styles.css";import { SolanaWalletContextProvider } from "./SolanaWalletProvider";export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <SolanaWalletContextProvider> {children} </SolanaWalletContextProvider> </body> </html> );}
Update the config.ts file to include Solana:
config.ts
Copy
Ask AI
import { EthereumWallet } from "@turnkey/wallet-stamper";import { SolanaWallet } from "./SolanaWalletFactory";export const turnkeyConfig = { apiBaseUrl: "https://api.turnkey.com", defaultOrganizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!,};export const turnkeyEthereumConfig = { ...turnkeyConfig, wallet: new EthereumWallet(),};// Factory function for Solanaexport function createSolanaConfig(wallet: Parameters<typeof SolanaWallet>[0]) { return { ...turnkeyConfig, wallet: SolanaWallet(wallet), };}
Now let’s put everything together:
app/page.tsx
Copy
Ask AI
'use client';import { useWallet } from '@solana/wallet-adapter-react';import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';import { Turnkey } from '@turnkey/sdk-browser';import { getSubOrg, createSubOrg } from './actions';import { useCallback, useEffect, useState } from 'react';import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-browser';import { SessionType } from '@turnkey/sdk-types';import { SolanaWallet } from "./SolanaWalletFactory";import { createSolanaConfig } from "./config";export default function WalletAuth() { const wallet = useWallet(); const [mounted, setMounted] = useState(false); const [session, setSession] = useState<any | null>(null); const [wallets, setWallets] = useState<any[]>([]); useEffect(() => { setMounted(true); }, []); const login = useCallback(async () => { try { if (!wallet.connected || !wallet.publicKey) { throw new Error('Wallet not connected'); } const turnkeyConfig = createSolanaConfig(wallet); const turnkey = new Turnkey(turnkeyConfig); const walletClient = turnkey.walletClient(SolanaWallet(wallet)); // Get the injected wallet public key const publicKey = await walletClient?.getPublicKey(); const subOrgId = await getSubOrg(publicKey); if (!subOrgId) { const subOrgResponse = await createSubOrg( publicKey, 'API_KEY_CURVE_ED25519' ); const subOrg = subOrgResponse?.subOrganizationId ?? null; if (!subOrg) throw new Error('Failed to create sub-organization'); console.log('Sub-Organization created:', subOrg); } // Initialize the indexedDbClient const client = await turnkey.indexedDbClient(); if (!client) { throw new Error('indexedDbClient not initialized'); } // Reset the indexedDb key pair and session before each login // Note that session reset is important when switching between multiple wallets within the same browser await turnkey?.logout(); await client?.clear(); await client!.resetKeyPair(); // Get the indexedDbClient public key const pubKey = await client!.getPublicKey(); await walletClient!.loginWithWallet({ sessionType: SessionType.READ_WRITE, // use SessionType.READ_ONLY for read-only sessions publicKey: pubKey!, }); console.log('Login successful'); const session = await turnkey?.getSession(); setSession(session); const subOrganizationId = session!.organizationId; // Get existing suborg wallets const wallets = await client.getWallets({ organizationId: subOrgId!, }); setWallets(wallets.wallets); // Create a new wallet with an Ethereum wallet account const newWalletResponse = await client.createWallet({ walletName: 'New Wallet 1', accounts: DEFAULT_ETHEREUM_ACCOUNTS, }); const updatedWallets = await client.getWallets({ organizationId: subOrganizationId, }); setWallets(updatedWallets.wallets); } catch (err) { console.error('Login error:', err); } }, [wallet]); if (!mounted) return null; return ( <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6 space-y-4"> <h2 className="text-xl font-bold mb-4 text-gray-800"> Turnkey Solana Wallet Auth </h2> {session && wallets.length > 0 && ( <div className="space-y-4 mb-6"> <h3 className="text-lg font-semibold text-gray-700">🧾 Wallets</h3> {wallets.map((wallet) => ( <div key={wallet.walletId} className="border border-gray-200 rounded-md p-3 bg-gray-50 text-sm" > <div className="font-medium text-gray-800"> {wallet.walletName} </div> <div className="text-gray-500 text-xs"> Wallet ID: {wallet.walletId} </div> </div> ))} </div> )} {!session && ( <> {!wallet.connected && <WalletMultiButton />} {wallet.connected && ( <div className="flex flex-wrap gap-2"> <button onClick={login} className="bg-purple-700 hover:bg-purple-800 text-white font-semibold py-2 px-4 rounded-md transition" > Sign In </button> <WalletMultiButton /> </div> )} </> )} </div> );}