Passkey Flow
Implement direct transaction signing with wallet approval popups
Implementing Passkey Flow
The passkey flow demonstrates transaction signing where users approve each transaction via a wallet popup.
Create the Passkey Page
"use client";
import { useEffect, useState } from "react";
import { useChainId, useConnection, useReadContract } from "wagmi";
import { createWalletClient, custom, encodeFunctionData, parseEther } from "viem";
import { Navbar } from "@/components/Navbar";
import {
SIMPLE_TOKEN_ADDRESS,
SIMPLE_TOKEN_ABI,
COUNTER_ADDRESS,
COUNTER_ABI,
} from "@/constants";
export default function PasskeyPage() {
const [mounted, setMounted] = useState(false);
const [txHash, setTxHash] = useState<string>();
const [pendingAction, setPendingAction] = useState<string>();
const { address, connector } = useConnection();
const chainId = useChainId();
// Read token balance
const { data: balance, refetch: refetchBalance } = useReadContract({
address: SIMPLE_TOKEN_ADDRESS,
abi: SIMPLE_TOKEN_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});
// Read counter value
const { data: count, refetch: refetchCount } = useReadContract({
address: COUNTER_ADDRESS,
abi: COUNTER_ABI,
functionName: "count",
query: { enabled: !!address },
});
useEffect(() => {
setMounted(true);
}, []);
const executeTransaction = async (
action: string,
contractAddress: string,
data: string
) => {
if (!connector?.provider || !address) return;
try {
setPendingAction(action);
setTxHash(undefined);
const walletClient = createWalletClient({
account: address,
chain: { id: chainId } as any,
transport: custom(connector.provider),
});
const hash = await walletClient.sendCallsSync({
account: address,
calls: [{ to: contractAddress as `0x${string}`, data: data as `0x${string}` }],
});
setTxHash(hash);
// Refresh contract state
refetchBalance();
refetchCount();
} catch (error) {
console.error(`${action} failed:`, error);
} finally {
setPendingAction(undefined);
}
};
const mintTokens = async () => {
if (!address) return;
const data = encodeFunctionData({
abi: SIMPLE_TOKEN_ABI,
functionName: "mint",
args: [address, parseEther("1000")],
});
await executeTransaction("Minting", SIMPLE_TOKEN_ADDRESS, data);
};
const spendTokens = async () => {
if (!address) return;
const data = encodeFunctionData({
abi: SIMPLE_TOKEN_ABI,
functionName: "transfer",
args: ["0x0000000000000000000000000000000000000000", parseEther("5")],
});
await executeTransaction("Spending", SIMPLE_TOKEN_ADDRESS, data);
};
const incrementCounter = async () => {
const data = encodeFunctionData({
abi: COUNTER_ABI,
functionName: "increment",
});
await executeTransaction("Incrementing", COUNTER_ADDRESS, data);
};
if (!mounted) return null;
if (!address) {
return (
<div className="min-h-screen bg-black text-white">
<Navbar />
<div className="max-w-4xl mx-auto px-4 py-16 text-center">
<p className="text-xl text-gray-400">
Connect your wallet to use passkey flow
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white">
<Navbar />
<div className="max-w-4xl mx-auto px-4 py-16">
<h1 className="text-4xl font-bold mb-8 text-green-500">
Passkey Flow
</h1>
<p className="text-gray-400 mb-8">
Each transaction requires wallet approval via popup
</p>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
<h3 className="text-sm text-gray-400 mb-2">STK Balance</h3>
<p className="text-3xl font-bold text-green-500">
{balance ? (Number(balance) / 1e18).toLocaleString() : "0"}
</p>
</div>
<div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
<h3 className="text-sm text-gray-400 mb-2">Counter Value</h3>
<p className="text-3xl font-bold text-green-500">
{count?.toString() || "0"}
</p>
</div>
</div>
<div className="space-y-4">
<button
onClick={mintTokens}
disabled={!!pendingAction}
className="w-full px-6 py-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{pendingAction === "Minting" ? "Minting..." : "Mint 1000 STK"}
</button>
<button
onClick={spendTokens}
disabled={!!pendingAction}
className="w-full px-6 py-4 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{pendingAction === "Spending" ? "Spending..." : "Spend 5 STK"}
</button>
<button
onClick={incrementCounter}
disabled={!!pendingAction}
className="w-full px-6 py-4 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{pendingAction === "Incrementing"
? "Incrementing..."
: "Increment Counter"}
</button>
</div>
{txHash && (
<div className="mt-8 p-4 bg-gray-900 rounded-lg border border-green-500">
<p className="text-sm text-gray-400 mb-2">Transaction Hash:</p>
<a
href={`https://testnet.risescan.com/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-green-500 hover:underline break-all"
>
{txHash}
</a>
</div>
)}
</div>
</div>
);
}How It Works
Transaction Flow:
- User clicks an action button (Mint, Spend, Increment)
encodeFunctionData()creates the contract call datawalletClient.sendCallsSync()triggers the wallet popup- User approves the transaction in the popup
- Wallet broadcasts the transaction and polls for confirmation
- Transaction hash is displayed with explorer link
- Contract state is refreshed automatically
When a user triggers a transaction, they'll see the RISE Wallet popup:

Now move on to the Session Key Flow to implement background signing without popups.