chore: ens resolution

This commit is contained in:
Danish Arora 2025-08-05 10:10:08 +05:30
parent 509faae6c9
commit e29fc8ed59
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
8 changed files with 391 additions and 129 deletions

1
package-lock.json generated
View File

@ -64,7 +64,6 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.33.2",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},

View File

@ -69,7 +69,6 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.33.2",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},

View File

@ -15,7 +15,7 @@ const Header = () => {
currentUser,
isAuthenticated,
verificationStatus,
verifyOrdinal,
verifyOwnership,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
@ -49,7 +49,7 @@ const Header = () => {
};
const handleVerify = async () => {
await verifyOrdinal();
await verifyOwnership();
};
const handleDelegateKey = async () => {
@ -246,11 +246,11 @@ const Header = () => {
<Tooltip>
<TooltipTrigger asChild>
<span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
{address?.slice(0, 5)}...{address?.slice(-4)}
{currentUser?.ensName || `${address?.slice(0, 5)}...${address?.slice(-4)}`}
</span>
</TooltipTrigger>
<TooltipContent className="text-sm">
<p>{address}</p>
<p>{currentUser?.ensName ? `${currentUser.ensName} (${address})` : address}</p>
</TooltipContent>
</Tooltip>
<Tooltip>

View File

@ -12,7 +12,7 @@ interface AuthContextType {
isAuthenticated: boolean;
isAuthenticating: boolean;
verificationStatus: VerificationStatus;
verifyOrdinal: () => Promise<boolean>;
verifyOwnership: () => Promise<boolean>;
delegateKey: () => Promise<boolean>;
isDelegationValid: () => boolean;
delegationTimeRemaining: () => number;
@ -49,6 +49,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Create ref for AuthService so it persists between renders
const authServiceRef = useRef(new AuthService());
// Set AppKit accounts in AuthService
useEffect(() => {
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount]);
// Sync with AppKit wallet state
useEffect(() => {
if (isConnected && address) {
@ -58,33 +63,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (storedUser && storedUser.address === address) {
// Use stored user data
setCurrentUser(storedUser);
if ('ordinalOwnership' in storedUser) {
setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
} else {
setVerificationStatus('unverified');
}
setVerificationStatus(getVerificationStatus(storedUser));
} else {
// Create new user from AppKit wallet
const newUser: User = {
address,
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
ordinalOwnership: false,
delegationExpiry: null,
verificationStatus: 'unverified',
lastChecked: Date.now(),
};
setCurrentUser(newUser);
setVerificationStatus('unverified');
authServiceRef.current.saveUser(newUser);
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
toast({
title: "Wallet Connected",
description: `Connected to ${chainName} with address ${address.slice(0, 6)}...${address.slice(-4)}`,
});
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
toast({
title: "Wallet Connected",
description: `Connected to ${chainName} with ${displayName}`,
});
const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership';
toast({
title: "Action Required",
description: "Please verify your Ordinal ownership and delegate a signing key for better UX.",
description: `Please verify your ${verificationType} and delegate a signing key for better UX.`,
});
}
} else {
@ -92,9 +96,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setCurrentUser(null);
setVerificationStatus('unverified');
}
}, [isConnected, address, isBitcoinConnected, toast]);
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
const verifyOrdinal = async (): Promise<boolean> => {
const getVerificationStatus = (user: User): VerificationStatus => {
if (user.walletType === 'bitcoin') {
return user.ordinalOwnership ? 'verified-owner' : 'verified-none';
} else if (user.walletType === 'ethereum') {
return user.ensOwnership ? 'verified-owner' : 'verified-none';
}
return 'unverified';
};
const verifyOwnership = async (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
toast({
title: "Not Connected",
@ -108,12 +121,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setVerificationStatus('verifying');
try {
const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS';
toast({
title: "Verifying Ordinal",
description: "Checking your wallet for Ordinal Operators..."
title: `Verifying ${verificationType}`,
description: `Checking your wallet for ${verificationType} ownership...`
});
const result: AuthResult = await authServiceRef.current.verifyOrdinal(currentUser);
const result: AuthResult = await authServiceRef.current.verifyOwnership(currentUser);
if (!result.success) {
throw new Error(result.error);
@ -124,27 +138,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
authServiceRef.current.saveUser(updatedUser);
// Update verification status
setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
setVerificationStatus(getVerificationStatus(updatedUser));
if (updatedUser.ordinalOwnership) {
if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) {
toast({
title: "Ordinal Verified",
description: "You now have full access. We recommend delegating a key for better UX.",
});
} else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) {
toast({
title: "ENS Verified",
description: "You now have full access. We recommend delegating a key for better UX.",
});
} else {
const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain';
toast({
title: "Read-Only Access",
description: "No Ordinal Operators found. You have read-only access.",
description: `No ${verificationType} found. You have read-only access.`,
variant: "default",
});
}
return Boolean(updatedUser.ordinalOwnership);
return Boolean(
(updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) ||
(updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership)
);
} catch (error) {
console.error("Error verifying Ordinal:", error);
console.error("Error verifying ownership:", error);
setVerificationStatus('unverified');
let errorMessage = "Failed to verify Ordinal ownership. Please try again.";
let errorMessage = "Failed to verify ownership. Please try again.";
if (error instanceof Error) {
errorMessage = error.message;
}
@ -203,24 +226,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
console.error("Error delegating key:", error);
let errorMessage = "Failed to delegate key. Please try again.";
if (error instanceof Error) {
// Provide specific guidance based on error type
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
} else if (error.message.includes("timeout")) {
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
} else if (error.message.includes("Failed to connect wallet")) {
errorMessage = "Unable to connect to wallet. Please ensure it's installed and unlocked, then try again.";
} else if (error.message.includes("Wallet is not connected")) {
errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again.";
} else {
errorMessage = error.message;
}
errorMessage = error.message;
}
toast({
title: "Delegation Failed",
title: "Delegation Error",
description: errorMessage,
variant: "destructive",
});
@ -230,37 +241,43 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsAuthenticating(false);
}
};
const isDelegationValid = (): boolean => {
return authServiceRef.current.isDelegationValid();
};
const delegationTimeRemaining = (): number => {
return authServiceRef.current.getDelegationTimeRemaining();
};
const isWalletAvailable = (): boolean => {
return isConnected && !!address;
return isConnected;
};
const messageSigning = {
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
return authServiceRef.current.signMessage(message);
},
verifyMessage: (message: OpchanMessage): boolean => {
return authServiceRef.current.verifyMessage(message);
}
};
const value: AuthContextType = {
currentUser,
isAuthenticated: Boolean(currentUser && isConnected),
isAuthenticating,
verificationStatus,
verifyOwnership,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
isWalletAvailable,
messageSigning
};
return (
<AuthContext.Provider
value={{
currentUser,
isAuthenticated: !!currentUser?.ordinalOwnership,
isAuthenticating,
verificationStatus,
verifyOrdinal,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
isWalletAvailable,
messageSigning: {
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),
},
}}
>
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);

View File

@ -1,5 +1,5 @@
import { User } from '@/types';
import { WalletService } from '../wallets';
import { WalletService, AppKitAccount } from '../wallets/index';
import { OrdinalAPI } from '../ordinal';
import { MessageSigning } from '../signatures/message-signing';
import { OpchanMessage } from '@/types';
@ -18,7 +18,14 @@ export class AuthService {
constructor() {
this.walletService = new WalletService();
this.ordinalApi = new OrdinalAPI();
this.messageSigning = new MessageSigning(this.walletService['keyDelegation']);
this.messageSigning = new MessageSigning(this.walletService.getKeyDelegation());
}
/**
* Set AppKit accounts for wallet service
*/
setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) {
this.walletService.setAccounts(bitcoinAccount, ethereumAccount);
}
/**
@ -26,20 +33,27 @@ export class AuthService {
*/
async connectWallet(): Promise<AuthResult> {
try {
if (!this.walletService.isWalletAvailable('phantom')) {
const walletInfo = await this.walletService.getWalletInfo();
if (!walletInfo) {
return {
success: false,
error: 'Phantom wallet not installed'
error: 'No wallet connected'
};
}
const address = await this.walletService.connectWallet('phantom');
const user: User = {
address,
address: walletInfo.address,
walletType: walletInfo.walletType,
verificationStatus: 'unverified',
lastChecked: Date.now(),
};
// Add ENS info for Ethereum wallets
if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) {
user.ensName = walletInfo.ensName;
user.ensOwnership = true;
}
return {
success: true,
user
@ -56,53 +70,93 @@ export class AuthService {
* Disconnect wallet and clear user data
*/
async disconnectWallet(): Promise<void> {
await this.walletService.disconnectWallet('phantom');
const walletType = this.walletService.getActiveWalletType();
if (walletType) {
await this.walletService.disconnectWallet(walletType);
}
}
/**
* Verify ordinal ownership for a user
* Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users
*/
async verifyOrdinal(user: User): Promise<AuthResult> {
async verifyOwnership(user: User): Promise<AuthResult> {
try {
// TODO: revert when the API is ready
// const response = await this.ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
const updatedUser = {
...user,
ordinalOwnership: hasOperators,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser
};
if (user.walletType === 'bitcoin') {
return await this.verifyBitcoinOrdinal(user);
} else if (user.walletType === 'ethereum') {
return await this.verifyEthereumENS(user);
} else {
return {
success: false,
error: 'Unknown wallet type'
};
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to verify ordinal'
error: error instanceof Error ? error.message : 'Failed to verify ownership'
};
}
}
/**
* Verify Bitcoin Ordinal ownership
*/
private async verifyBitcoinOrdinal(user: User): Promise<AuthResult> {
// TODO: revert when the API is ready
// const response = await this.ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
const updatedUser = {
...user,
ordinalOwnership: hasOperators,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser
};
}
/**
* Verify Ethereum ENS ownership
*/
private async verifyEthereumENS(user: User): Promise<AuthResult> {
const walletInfo = await this.walletService.getWalletInfo();
const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0;
const updatedUser = {
...user,
ensOwnership: hasENS,
ensName: walletInfo?.ensName,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser
};
}
/**
* Set up key delegation for the user
*/
async delegateKey(user: User): Promise<AuthResult> {
try {
const canConnect = await this.walletService.canConnectWallet('phantom');
const walletType = user.walletType;
const canConnect = await this.walletService.canConnectWallet(walletType);
if (!canConnect) {
return {
success: false,
error: 'Phantom wallet is not available or cannot be connected. Please ensure it is installed and unlocked.'
error: `${walletType} wallet is not available or cannot be connected. Please ensure it is installed and unlocked.`
};
}
const delegationInfo = await this.walletService.setupKeyDelegation(
user.address,
'phantom'
walletType
);
const updatedUser = {
@ -155,8 +209,8 @@ export class AuthService {
/**
* Get current wallet info
*/
getWalletInfo() {
return this.walletService.getWalletInfo();
async getWalletInfo() {
return await this.walletService.getWalletInfo();
}
/**

View File

@ -0,0 +1,225 @@
import { KeyDelegation } from '../signatures/key-delegation';
import { bytesToHex } from '@/lib/utils';
import { getEnsName } from '@wagmi/core';
import { config } from './appkit';
import { UseAppKitAccountReturn } from '@reown/appkit';
export interface WalletInfo {
address: string;
walletType: 'bitcoin' | 'ethereum';
ensName?: string;
isConnected: boolean;
}
export interface DelegationInfo {
browserPublicKey: string;
signature: string;
expiryTimestamp: number;
}
export class ReOwnWalletService {
private keyDelegation: KeyDelegation;
private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn;
constructor() {
this.keyDelegation = new KeyDelegation();
}
/**
* Set account references from AppKit hooks
*/
setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) {
this.bitcoinAccount = bitcoinAccount;
this.ethereumAccount = ethereumAccount;
}
/**
* Check if a wallet type is available and connected
*/
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
if (walletType === 'bitcoin') {
return this.bitcoinAccount?.isConnected || false;
} else {
return this.ethereumAccount?.isConnected || false;
}
}
/**
* Check if wallet can be connected
*/
async canConnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
// For ReOwn, we assume connection is always possible if AppKit is initialized
return true;
}
/**
* Get wallet connection info with ENS resolution for Ethereum
*/
async getWalletInfo(): Promise<WalletInfo | null> {
if (this.bitcoinAccount?.isConnected) {
return {
address: this.bitcoinAccount.address,
walletType: 'bitcoin',
isConnected: true
};
} else if (this.ethereumAccount?.isConnected) {
// Use Wagmi to resolve ENS name
let ensName: string | undefined;
try {
const resolvedName = await getEnsName(config, {
address: this.ethereumAccount.address as `0x${string}`
});
ensName = resolvedName || undefined;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
// Continue without ENS name
}
return {
address: this.ethereumAccount.address,
walletType: 'ethereum',
ensName,
isConnected: true
};
}
return null;
}
/**
* Get the active wallet address
*/
getActiveAddress(): string | null {
if (this.bitcoinAccount?.isConnected) {
return this.bitcoinAccount.address;
} else if (this.ethereumAccount?.isConnected) {
return this.ethereumAccount.address;
}
return null;
}
/**
* Get the active wallet type
*/
getActiveWalletType(): 'bitcoin' | 'ethereum' | null {
if (this.bitcoinAccount?.isConnected) {
return 'bitcoin';
} else if (this.ethereumAccount?.isConnected) {
return 'ethereum';
}
return null;
}
/**
* Setup key delegation for the connected wallet
*/
async setupKeyDelegation(
address: string,
walletType: 'bitcoin' | 'ethereum'
): Promise<DelegationInfo> {
// Generate browser keypair
const keypair = this.keyDelegation.generateKeypair();
// Create delegation message with chain-specific format
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
const delegationMessage = this.createDelegationMessage(
keypair.publicKey,
address,
walletType,
expiryTimestamp
);
// Get the appropriate account for signing
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
if (!account?.isConnected) {
throw new Error(`${walletType} wallet is not connected`);
}
// Sign the delegation message
const signature = await this.signMessage(delegationMessage, walletType);
// Create and store delegation
const delegationInfo = this.keyDelegation.createDelegation(
address,
signature,
keypair.publicKey,
keypair.privateKey,
24
);
this.keyDelegation.storeDelegation(delegationInfo);
return {
browserPublicKey: keypair.publicKey,
signature,
expiryTimestamp
};
}
/**
* Create chain-specific delegation message
*/
private createDelegationMessage(
browserPublicKey: string,
address: string,
walletType: 'bitcoin' | 'ethereum',
expiryTimestamp: number
): string {
const chainName = walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum';
const expiryDate = new Date(expiryTimestamp).toISOString();
return `I, ${address} (${chainName}), delegate authority to this pubkey: ${browserPublicKey} until ${expiryDate}`;
}
/**
* Sign a message with the appropriate wallet
*/
private async signMessage(message: string, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
if (!account?.isConnected) {
throw new Error(`${walletType} wallet is not connected`);
}
// Convert message to bytes for signing
const messageBytes = new TextEncoder().encode(message);
// Sign with the appropriate wallet
const signature = await account.signMessage({ message: messageBytes });
// Return hex-encoded signature
return bytesToHex(signature);
}
/**
* Disconnect wallet (handled by AppKit)
*/
async disconnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<void> {
// Clear stored delegation
this.keyDelegation.clearDelegation();
// Note: Actual disconnection is handled by AppKit's useDisconnect hook
}
/**
* Check if delegation is valid
*/
isDelegationValid(): boolean {
return this.keyDelegation.isDelegationValid();
}
/**
* Get delegation time remaining
*/
getDelegationTimeRemaining(): number {
return this.keyDelegation.getDelegationTimeRemaining();
}
/**
* Get the key delegation instance
*/
getKeyDelegation(): KeyDelegation {
return this.keyDelegation;
}
}

View File

@ -0,0 +1,2 @@
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService';

View File

@ -1,34 +0,0 @@
export enum WalletConnectionStatus {
Connected = 'connected',
Disconnected = 'disconnected',
NotDetected = 'not-detected',
Connecting = 'connecting'
}
export interface BtcAccount {
address: string;
addressType: "p2tr" | "p2wpkh" | "p2sh" | "p2pkh";
publicKey: string;
purpose: "payment" | "ordinals";
}
export interface PhantomBitcoinProvider {
isPhantom?: boolean;
signMessage?: (address: string, message: Uint8Array) => Promise<{ signature: Uint8Array }>;
connect?: () => Promise<{ publicKey: string }>;
disconnect?: () => Promise<void>;
on?: (event: string, callback: (arg: unknown) => void) => void;
off?: (event: string, callback: (arg: unknown) => void) => void;
publicKey?: string;
requestAccounts?: () => Promise<BtcAccount[]>;
}
export interface PhantomWallet {
bitcoin?: PhantomBitcoinProvider;
}
declare global {
interface Window {
phantom?: PhantomWallet;
}
}