Frontend Implementation
Create the registration UI
Building the Frontend
The frontend is a single-page app that handles wallet connection and session key registration.
Configure Wagmi
Create the wagmi config:
import { Chains, RiseWallet, riseWallet } from 'rise-wallet';
import { createConfig, http } from 'wagmi';
export const config = createConfig({
chains: [Chains.riseTestnet],
connectors: [riseWallet(RiseWallet.defaultConfig)],
transports: {
[Chains.riseTestnet.id]: http('https://testnet.riselabs.xyz'),
},
});
declare module 'wagmi' {
interface Register {
config: typeof config;
}
}Create RISEx Constants
export const AUTH_CONTRACT = '0x3f12C76bbeB8df0e7616fBE097fA760A23929c7c';
export const domain = {
name: 'RISEx',
version: '1',
chainId: 8008135,
verifyingContract: AUTH_CONTRACT as `0x${string}`,
};
export const types = {
RegisterSigner: [
{ name: 'trader', type: 'address' },
{ name: 'signer', type: 'address' },
{ name: 'expiry', type: 'uint256' },
],
};Create Signature Helper
This is critical. RISE Wallet returns wrapped signatures that must be unwrapped:
import { Signature as OxSignature } from 'ox';
import { SignatureErc8010 } from 'ox/erc8010';
export function formatSignature(sig: string): string {
// Unwrap ERC-8010 if needed
if (SignatureErc8010.validate(sig)) {
const { signature } = SignatureErc8010.unwrap(sig);
sig = signature;
}
// Parse signature
const { r, s, yParity } = OxSignature.from(sig);
// Normalize v value
let v = yParity === 0 ? 27 : 28;
// Format as 0x + r + s + v
return `0x${r.slice(2)}${s.slice(2)}${v.toString(16).padStart(2, '0')}`;
}Registration Page
Build the main registration UI:
"use client";
import { useState } from 'react';
import { useConnection, useSignTypedData } from 'wagmi';
import { useConnect, useDisconnect } from 'wagmi';
import { P256, PublicKey } from 'ox';
import { parseAbi } from 'viem';
import { writeContract, waitForTransactionReceipt } from 'wagmi/actions';
import { config } from '@/config/wagmi';
import { AUTH_CONTRACT, domain, types } from '@/lib/risex';
import { formatSignature } from '@/lib/signature-helper';
const AUTH_ABI = parseAbi([
'function registerSigner(address trader, address signer, uint256 expiry, bytes memory signature) external',
]);
export default function Home() {
const [telegramId, setTelegramId] = useState('');
const [status, setStatus] = useState('');
const [sessionKey, setSessionKey] = useState('');
const { address } = useConnection();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const { signTypedDataAsync } = useSignTypedData();
const handleRegister = async () => {
if (!address || !telegramId) {
setStatus('❌ Connect wallet and enter Telegram ID');
return;
}
try {
setStatus('Generating session key...');
// Generate P256 session key
const privateKey = P256.randomPrivateKey();
const publicKey = P256.getPublicKey({ privateKey });
const signerAddress = PublicKey.toAddress(publicKey);
setSessionKey(PublicKey.toHex(publicKey));
// Prepare typed data
const expiry = BigInt(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60); // 30 days
setStatus('Sign permission in wallet...');
// Sign with RISE Wallet
let signature = await signTypedDataAsync({
domain,
types,
primaryType: 'RegisterSigner',
message: {
trader: address,
signer: signerAddress,
expiry,
},
});
// Format signature (unwrap and normalize)
signature = formatSignature(signature);
setStatus('Registering on-chain...');
// Call contract
const hash = await writeContract(config, {
address: AUTH_CONTRACT,
abi: AUTH_ABI,
functionName: 'registerSigner',
args: [address, signerAddress, expiry, signature as `0x${string}`],
});
setStatus('Waiting for confirmation...');
await waitForTransactionReceipt(config, { hash });
setStatus('Storing signer...');
// Store encrypted signer in backend
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/store-signer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
walletAddress: address,
privateKey,
}),
});
// Link Telegram ID
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/link-telegram`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
telegramId,
walletAddress: address,
}),
});
setStatus('✅ Registration complete! Open your Telegram bot.');
} catch (error: any) {
console.error('Registration error:', error);
setStatus(`❌ Error: ${error.message}`);
}
};
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-4">
<div className="max-w-md w-full space-y-6">
<div className="text-center">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-blue-500 text-transparent bg-clip-text">
RISEx Trading Bot
</h1>
<p className="text-gray-400">Register to start trading via Telegram</p>
</div>
{!address ? (
<button
onClick={() => connect({ connector: connectors[0] })}
className="w-full py-3 bg-purple-600 hover:bg-purple-700 rounded-lg font-semibold transition"
>
Connect RISE Wallet
</button>
) : (
<div className="space-y-4">
<div className="p-4 bg-gray-900 rounded-lg">
<p className="text-sm text-gray-400">Connected:</p>
<p className="font-mono text-sm">{address.slice(0, 6)}...{address.slice(-4)}</p>
<button
onClick={() => disconnect()}
className="mt-2 text-sm text-red-400 hover:text-red-300"
>
Disconnect
</button>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">
Telegram ID (from /debug command)
</label>
<input
type="text"
value={telegramId}
onChange={(e) => setTelegramId(e.target.value)}
placeholder="123456789"
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:border-purple-500 outline-none"
/>
</div>
<button
onClick={handleRegister}
disabled={!telegramId || !!status}
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg font-semibold transition"
>
Register Trading Signer
</button>
{status && (
<div className="p-4 bg-gray-900 rounded-lg">
<p className="text-sm">{status}</p>
{sessionKey && (
<p className="text-xs text-gray-500 mt-2 break-all">
Session Key: {sessionKey}
</p>
)}
</div>
)}
</div>
)}
</div>
</div>
);
}After connecting and registering, the page should look like this:

Add Provider Wrapper
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { type ReactNode, useState } from 'react';
import { WagmiProvider } from 'wagmi';
import { config } from '@/config/wagmi';
export function Provider(props: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</WagmiProvider>
);
}Update layout:
import type { Metadata } from 'next';
import './globals.css';
import { Provider } from './provider';
export const metadata: Metadata = {
title: 'RISEx Trading Bot',
description: 'Register for AI-powered Telegram trading',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}Your frontend is complete! Next, we'll test everything.