Wallet Authentication
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.
Initialize
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 theEthereumWallet
interface.
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_TURNKEY_ORGANIZATION_ID,
// The wallet interface used to sign requests
wallet: new EthereumWallet(),
};
- Next.js
- TypeScript
First, wrap your application with the TurnkeyProvider
in your app/layout.tsx
file:
import { TurnkeyProvider } from "@turnkey/sdk-react";
import { turnkeyConfig } from "./config";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* Pass the Turnkey config defined above to the TurnkeyProvider */}
<TurnkeyProvider config={turnkeyConfig}>{children}</TurnkeyProvider>
</body>
</html>
);
}
Then, create a new page component app/page.tsx
where we'll implement the wallet authentication functionality:
"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 steps
return <div>{/* We'll add UI elements here */}</div>;
}
Create a new file src/wallet-auth.ts
where we'll implement the wallet authentication functionality:
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 above
const turnkey = new Turnkey(turnkeyConfig);
// Initialize the Wallet Client with the EthereumWallet interface
const walletClient = turnkey.walletClient(new EthereumWallet());
// We'll add more functionality here in the following steps
Sign Up
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 suborganization 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 suborganization before proceeding.
Server-side
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 by leveraging the parent organization's public/private API key pair.
- Next.js
- TypeScript
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.
"use server";
import { Turnkey } from "@turnkey/sdk-server";
import { turnkeyConfig } from "./config";
const { baseUrl, defaultOrganizationId } = turnkeyConfig;
// Initialize the Turnkey Server Client on the server-side
const turnkeyServer = new Turnkey({
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
baseUrl,
defaultOrganizationId,
}).apiClient();
import { Turnkey } from "@turnkey/sdk-server";
import { turnkeyConfig } from "./config";
const { baseUrl, defaultOrganizationId } = turnkeyConfig;
// Initialize the Turnkey Server Client on the server-side
const turnkeyServer = new Turnkey({
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
baseUrl,
defaultOrganizationId,
}).apiClient();
Check for Existing User
Before signing up a new user, we can try and retrieve the user's suborganization ID using their public key. If a suborganization is found, we can proceed with authentication, otherwise we assume this the user is signing up and we need to create a new suborganization.
- Next.js
- TypeScript
We'll define this function in the server-side code we initialized earlier.
"use server";
// ...
export const getSubOrg = async (publicKey: string) => {
const { organizationIds } = await turnkeyServer.getSubOrgIds({
// The parent organization ID
organizationId: turnkeyConfig.defaultOrganizationId,
filterType: "PUBLIC_KEY",
filterValue: publicKey,
});
return organizationIds[0];
};
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.
"use client";
import { useState } from "react";
import { useTurnkey } from "@turnkey/sdk-react";
// Import the getSubOrg function we defined earlier
import { 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 message
const publicKey = await walletClient?.getPublicKey();
if (!publicKey) {
throw new Error("No public key found");
}
const subOrg = await getSubOrg(publicKey);
if (subOrg) {
// User already has a suborganization
} else {
// User does not have a suborganization, proceed with sign-up
}
};
return (
<div>
<button onClick={login}>Sign In</button>
</div>
);
}
We'll define this function in the server-side code we initialized earlier.
"use server";
// ...
export const getSubOrg = async (publicKey: string) => {
const { organizationIds } = await turnkeyServer.getSubOrgIds({
// The parent organization ID
organizationId: turnkeyConfig.defaultOrganizationId,
filterType: "PUBLIC_KEY",
filterValue: publicKey,
});
return organizationIds[0];
};
We'll use the getSubOrg
function in the login method to check if a user already has a suborganization.
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 subOrg = await getSubOrg(publicKey);
if (subOrg) {
// User already has a suborganization
} else {
// User does not have a suborganization, proceed with sign-up
}
};
Create Suborganization
Next, we'll define a method to create a suborganization for new user sign-ups.
For more information, refer to the Suborganizations guide.
- Next.js
- TypeScript
We'll define another server action createSubOrg
to create a suborganization for new user sign-ups.
"use server";
// ...
// Import the default Ethereum accounts helper
import { 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,
},
],
rootQuorumThreshold: 1,
wallet: {
walletName: "Default Wallet",
// This is used to create a new Ethereum wallet for the suborganization
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.
"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 subOrg = await getSubOrg(publicKey);
if (subOrg) {
// User already has a suborganization
} else {
// User does not have a suborganization, proceed with sign-up
const subOrg = await createSubOrg(publicKey, "API_KEY_CURVE_SECP256K1");
// In the next step we'll add logic sign in the user
if (!subOrg) {
throw new Error("Failed to create suborganization");
}
}
};
return (
<div>
<button onClick={login}>Sign In</button>
</div>
);
}
We'll define another server-side function createSubOrg
, to create a suborganization for new user sign-ups.
// ...
// Import the default Ethereum accounts helper
import { 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,
},
],
rootQuorumThreshold: 1,
wallet: {
walletName: "Default Wallet",
// This is used to create a new Ethereum wallet for the suborganization
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.
import { getSubOrg, createSubOrg } from "./wallet-auth-server";
// ...
const walletClient = turnkey.walletClient(new EthereumWallet());
export const login = async () => {
const publicKey = await walletClient.getPublicKey();
if (!publicKey) {
throw new Error("No public key found");
}
const subOrg = await getSubOrg(publicKey);
if (subOrg) {
// User already has a suborganization
} else {
// User does not have a suborganization, proceed with sign-up
const subOrg = await createSubOrg(publicKey, "API_KEY_CURVE_SECP256K1");
// In the next step we'll add logic to sign in the user
if (!subOrg) {
throw new Error("Failed to create suborganization");
}
}
};
Sign In
At this point, we have a working sign-up flow. Next, we'll implement the signing in functionality by creating a read-only session and retrieving the user's wallets.
Read-only Session
Create a read-only session for the user by calling the login
method on the WalletClient
instance.
This will save a read-only session token to the localStorage
to authenticate future read requests.
- Next.js
- TypeScript
"use client";
import { getSubOrg, createSubOrg } from "./actions";
// ...
const walletClient = turnkey.walletClient(new EthereumWallet());
export default function WalletAuth() {
const { walletClient } = useTurnkey();
const login = async () => {
const publicKey = await walletClient?.getPublicKey();
if (!publicKey) {
throw new Error("No public key found");
}
const subOrg = await getSubOrg(publicKey);
if (subOrg) {
const loginResponse = await walletClient.login({
organizationId: subOrgId,
});
if (loginResponse?.organizationId) {
// User is authenticated 🎉
}
} else {
// ...
}
};
return (
<div>
<button onClick={login}>Sign In</button>
</div>
);
}
import { getSubOrg, createSubOrg } from "./wallet-auth-server";
// ...
export const login = async (publicKey: string) => {
//...
const subOrg = await getSubOrg(publicKey);
if (subOrg) {
const loginResponse = await walletClient.login({
organizationId: subOrgId,
});
if (loginResponse?.organizationId) {
// User is authenticated 🎉
}
} else {
// ...
}
};
Retrieve Wallets
Once the user is authenticated, we can retrieve the user's wallets by calling the getWallets
method on the WalletClient
instance.
- Next.js
- TypeScript
"use client";
import { getSubOrg, createSubOrg } from "./actions";
// ...
export default function WalletAuth() {
const { walletClient, client } = useTurnkey();
// State to hold the user's wallets
const [wallets, setWallets] = useState<Wallet[]>([]);
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 subOrg = await getSubOrg(publicKey);
if (subOrg) {
const loginResponse = await walletClient.login({
organizationId: subOrgId,
});
if (loginResponse?.organizationId) {
const wallets = await client.getWallets({
organizationId: loginResponse.organizationId,
});
setWallets(wallets);
}
} else {
// ...
}
};
// Render the user's wallets if defined
if (wallets) {
return (
<div>
{wallets.map((wallet) => (
<div key={wallet.walletId}>{wallet.walletName}</div>
))}
</div>
);
}
return {
// ...
};
}