How to use an Openfort signer with permissionless.js
permissionless.js allows you to plug in custom signers to control the accounts that you create. Openfort is an embedded wallet provider that allows you to easily onboard users to your dapp.
Install the dependencies
npm i @openfort/react @tanstack/react-query permissionless viem wagmiCreate the Openfort provider
Following Openfort's quickstart guide, set up the Openfort provider in your app. This will allow you to use Openfort as a signer with permissionless.js.
import { OpenfortProvider, AccountTypeEnum, AuthProvider } from "@openfort/react";
import { WagmiProvider, createConfig, http } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { sepolia } from "viem/chains";
const wagmiConfig = createConfig({
chains: [sepolia],
transports: {
[sepolia.id]: http(),
},
});
const queryClient = new QueryClient();
export const Providers = ({ children }: { children: React.ReactNode }) => {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<OpenfortProvider
publishableKey={process.env.NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY!}
walletConfig={{
shieldPublishableKey: process.env.NEXT_PUBLIC_SHIELD_PUBLISHABLE_KEY!,
accountType: AccountTypeEnum.EOA,
}}
uiConfig={{
authProviders: [
AuthProvider.EMAIL,
AuthProvider.GOOGLE,
AuthProvider.GUEST,
],
}}
>
{children}
</OpenfortProvider>
</QueryClientProvider>
</WagmiProvider>
);
};Add authentication UI
Add the OpenfortButton component to allow users to authenticate:
import { OpenfortButton, useUser } from "@openfort/react";
// In your component
const { user, isAuthenticated } = useUser();
if (!isAuthenticated) {
return <OpenfortButton />;
}
// Show authenticated content
return <div>Welcome, {user?.player?.name}!</div>;The OpenfortButton must be placed inside the OpenfortProvider and will handle the authentication flow automatically.
Set up hooks and wallet configuration
Set up the required hooks and configure the embedded wallet to ensure you're on the correct network:
import { useWallets, use7702Authorization, useUser, type UserWallet } from "@openfort/react";
import { useEffect, useState } from "react";
import { useWalletClient, useAccount, useSwitchChain } from "wagmi";
import { createPublicClient, http, zeroAddress } from "viem";
import { sepolia } from "viem/chains";
import { createSmartAccountClient } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { entryPoint08Address } from "viem/account-abstraction";
export function SmartAccountDemo() {
const { user } = useUser();
const { wallets, setActiveWallet } = useWallets();
const walletClient = useWalletClient();
const { isConnected, chainId } = useAccount();
const { switchChain } = useSwitchChain();
const { signAuthorization } = use7702Authorization();
const [embeddedWallet, setEmbeddedWallet] = useState<UserWallet | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Set up the embedded wallet
useEffect(() => {
if (wallets.length > 0) {
setActiveWallet({
walletId: wallets[0].id,
address: wallets[0].address,
}).then((activeWallet) => {
setEmbeddedWallet(activeWallet.wallet);
});
}
}, [wallets.length]);
useEffect(() => {
if (isConnected && chainId !== sepolia.id) {
console.log('Switching to Sepolia network...');
switchChain(
{ chainId: sepolia.id },
{
onError: (error) => {
console.error('Failed to switch chain:', error);
setError(`Please switch to Sepolia network manually. ${error.message}`);
},
}
);
}
}, [chainId, isConnected, switchChain]);Create and send transaction with EIP-7702
Create and send a transaction with EIP-7702 authorization using the Openfort wallet and Pimlico paymaster.
const sendUserOperation = async () => {
// Validate prerequisites
if (!user || !embeddedWallet) {
setError("No wallet connected");
return;
}
setIsLoading(true);
setError(null);
setTxHash(null);
try {
const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_API_KEY;
if (!pimlicoApiKey) {
throw new Error("Please set a valid Pimlico API key in your .env file");
}
const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${pimlicoApiKey}`;
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const pimlicoClient = createPimlicoClient({
transport: http(pimlicoUrl),
});
// Validate wallet client exists before proceeding
if (!walletClient.data) {
throw new Error("No wallet found");
}
// Create simple smart account with EntryPoint v0.8
const simpleSmartAccount = await toSimpleSmartAccount({
owner: walletClient.data,
entryPoint: {
address: entryPoint08Address,
version: "0.8",
},
client: publicClient,
address: walletClient.data.account.address,
});
// Create the smart account client
const smartAccountClient = createSmartAccountClient({
account: simpleSmartAccount,
chain: sepolia,
bundlerTransport: http(pimlicoUrl),
paymaster: pimlicoClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast;
},
},
});
// Sign EIP-7702 authorization
const authorization = await signAuthorization({
contractAddress: "0xe6Cae83BdE06E4c305530e199D7217f42808555B",
chainId: sepolia.id,
nonce: await publicClient.getTransactionCount({
address: walletClient.data.account.address,
}),
});
// Send transaction with EIP-7702 authorization
const txnHash = await smartAccountClient.sendTransaction({
calls: [
{
to: zeroAddress,
data: "0x",
value: BigInt(0),
},
],
factory: "0x7702",
factoryData: "0x",
paymasterContext: {
sponsorshipPolicyId: process.env.NEXT_PUBLIC_SPONSORSHIP_POLICY_ID,
},
authorization,
});
setTxHash(txnHash);
} catch (err) {
console.error("Error sending user operation:", err);
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setIsLoading(false);
}
};- The
contractAddressis the EIP-7702 delegation contract factory: "0x7702"identifies this as an EIP-7702 transactionpaymasterContextwithsponsorshipPolicyIdenables gasless transactions- The authorization must be signed before each transaction
Complete Working Example
For a complete working example with full UI, see the implementation in this repository or the Openfort + Pimlico EIP-7702 recipe.
Environment Variables Required
NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY=your_openfort_publishable_key
NEXT_PUBLIC_SHIELD_PUBLISHABLE_KEY=your_shield_publishable_key
NEXT_PUBLIC_SPONSORSHIP_POLICY_ID=your_pimlico_sponsorship_policy_id
NEXT_PUBLIC_PIMLICO_API_KEY=your_pimlico_api_key