chore: remove bitcoin + appkit, use eth + viem/wagmi

This commit is contained in:
Danish Arora 2025-10-28 12:45:05 +05:30
parent 45fea2397a
commit 05fc7b6da3
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
47 changed files with 550 additions and 2304 deletions

View File

@ -1,14 +1,13 @@
# OpChan
A decentralized forum application built as a Proof of Concept for a Waku-powered discussion platform. OpChan enables users to create "cells" (discussion boards), make posts, and engage in threaded conversations using Bitcoin Ordinal verification and the Waku protocol for decentralized messaging.
A decentralized forum application built as a Proof of Concept for a Waku-powered discussion platform. OpChan enables users to create "cells" (discussion boards), make posts, and engage in threaded conversations using Ethereum wallets (wagmi) with optional ENS verification and the Waku protocol for decentralized messaging.
## Quick Start
### Prerequisites
- Node.js 18+ and npm
- [Phantom Wallet](https://phantom.app/) browser extension
- Bitcoin Ordinals (required for posting, optional for reading)
- Ethereum wallet (e.g., MetaMask, Coinbase Wallet) or WalletConnect-compatible wallet
### Installation
@ -31,12 +30,7 @@ A decentralized forum application built as a Proof of Concept for a Waku-powered
cp .env.example .env
```
Edit `.env` to configure development settings:
```env
# Set to 'true' to bypass verification in development
VITE_OPCHAN_MOCK_ORDINAL_CHECK=false
```
Edit `.env` to configure development settings as needed for local testing.
4. **Start development server**
```bash
@ -69,8 +63,8 @@ src/
### Getting Started
1. **Connect Wallet**: Click "Connect Wallet" and approve the Phantom wallet connection
2. **Verify Ordinals**: The app will check if your wallet contains Logos Operator Bitcoin Ordinals
1. **Connect Wallet**: Click "Connect Wallet" and approve the wallet connection
2. **Verify ENS**: The app will resolve your ENS name (if any) and mark your account as ENS-verified
3. **Browse Cells**: View existing discussion boards on the dashboard
4. **Create Content**: Create new cells, posts, or comments (requires Ordinals)
5. **Moderate**: Cell creators can moderate their boards
@ -79,7 +73,7 @@ src/
OpChan uses a two-tier authentication system:
1. **Wallet Connection**: Initial connection to Phantom wallet
1. **Wallet Connection**: Initial connection via wagmi connectors (Injected / WalletConnect / Coinbase)
2. **Key Delegation**: Optional browser key generation for improved UX
- Reduces wallet signature prompts
- Configurable duration: 1 week or 30 days
@ -119,7 +113,7 @@ OpChan uses a two-tier authentication system:
OpChan implements a decentralized architecture with these key components:
- **Waku Protocol**: Handles peer-to-peer messaging and content distribution
- **Bitcoin Ordinals**: Provides decentralized identity verification
- **ENS**: Provides decentralized identity signal for identity and display
- **Key Delegation**: Improves UX while maintaining security
- **Content Addressing**: Messages are cryptographically signed and verifiable
- **Moderation Layer**: Cell-based moderation without global censorship

View File

@ -14,17 +14,17 @@ The OpChan application has successfully implemented most core functionality incl
### ✅ IMPLEMENTED
#### 1. Bitcoin Key Authentication
#### 1. Ethereum Wallet Authentication
- **Status**: ✅ Fully Implemented
- **Implementation**: `src/lib/identity/wallets/ReOwnWalletService.ts`, `src/contexts/AuthContext.tsx`
- **Details**: Complete Bitcoin wallet integration with message signing capabilities
- **Implementation**: `src/lib/services/UserIdentityService.ts`, wagmi wiring in provider
- **Details**: Ethereum wallet integration with message signing capabilities
#### 2. Cell Creation Restrictions
- **Status**: ✅ Fully Implemented
- **Implementation**: `src/lib/forum/ForumActions.ts`, `src/components/CreateCellDialog.tsx`
- **Details**: Only users with Logos ordinal or ENS can create cells
- **Details**: Only users with ENS (or configured policy) can create cells
#### 3. Content Visibility
@ -65,8 +65,8 @@ The OpChan application has successfully implemented most core functionality incl
#### 9. Web3 Key Authentication
- **Status**: ✅ Fully Implemented
- **Implementation**: `src/lib/identity/wallets/ReOwnWalletService.ts`
- **Details**: Ethereum wallet support alongside Bitcoin
- **Implementation**: wagmi connectors; `src/lib/services/UserIdentityService.ts`
- **Details**: Ethereum wallet support with ENS resolution
#### 10. Relevance Index System
@ -95,12 +95,12 @@ The OpChan application has successfully implemented most core functionality incl
- **Details**: Interface exists but no UI for setting up call signs
- **Missing**: User interface for call sign configuration
#### 14. Ordinal Avatar Display
#### 14. ENS Avatar Display
- **Status**: ⚠️ Partially Implemented
- **Implementation**: `src/components/ui/author-display.tsx`
- **Details**: Basic ordinal detection but limited avatar display
- **Missing**: Full ordinal image integration and display
- **Details**: ENS name display; avatar resolution depends on ENS records
- **Missing**: Consistent avatar fallback behavior
### ❌ NOT IMPLEMENTED
@ -228,7 +228,7 @@ The OpChan application has successfully implemented most core functionality incl
- **Status**: ✅ Fully Implemented
- **Implementation**: `src/contexts/AuthContext.tsx`
- **Details**: Bitcoin and Ethereum wallet integration
- **Details**: Ethereum wallet integration
---
@ -236,11 +236,11 @@ The OpChan application has successfully implemented most core functionality incl
### ✅ IMPLEMENTED
#### 1. Centralized Ordinal API
#### 1. ENS Resolution
- **Status**: ✅ Fully Implemented
- **Implementation**: `src/lib/identity/ordinal.ts`
- **Details**: Integration with Logos dashboard API
- **Implementation**: ENS resolution via viem public client in `UserIdentityService`
- **Details**: Name + avatar (when published in ENS records)
#### 2. Waku Network Integration
@ -264,9 +264,9 @@ The OpChan application has successfully implemented most core functionality incl
- Implement call sign validation and uniqueness
- Estimated effort: 3-4 days
3. **Enhanced Ordinal Display**
- Integrate full ordinal image display
- Add ordinal metadata visualization
3. **Enhanced ENS Display**
- Improve ENS avatar and fallback handling
- Cache ENS lookups with smarter TTL
- Estimated effort: 2-3 days
### Medium Priority
@ -329,7 +329,7 @@ OpChan has successfully implemented the vast majority of FURPS requirements, pro
**Areas for Improvement:**
- User personalization features (bookmarks, call signs)
- Enhanced ordinal integration
- Enhanced ENS integration
- Advanced search and filtering
The application is ready for production use with the current feature set, and the remaining features can be implemented incrementally based on user feedback and priorities.

View File

@ -47,10 +47,6 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@reown/appkit": "^1.7.17",
"@reown/appkit-adapter-bitcoin": "^1.7.17",
"@reown/appkit-adapter-wagmi": "^1.7.17",
"@reown/appkit-wallet-button": "^1.7.17",
"@tanstack/react-query": "^5.84.1",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.1",

View File

@ -32,7 +32,7 @@ const FeedSidebar: React.FC = () => {
// User's verification status display
const getVerificationBadge = () => {
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
if (verificationStatus === EVerificationStatus.ENS_VERIFIED) {
return { text: 'Verified Owner', color: 'bg-green-500' };
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
return { text: 'Verified', color: 'bg-blue-500' };

View File

@ -42,7 +42,7 @@ import {
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useToast } from '@/components/ui/use-toast';
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
import { useEthereumWallet } from '@opchan/react';
import { WalletWizard } from '@/components/ui/wallet-wizard';
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
@ -55,11 +55,7 @@ const Header = () => {
const { toast } = useToast();
const { content } = useForum();
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
const ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
const { disconnect } = useDisconnect();
const isConnected = bitcoinAccount.isConnected || ethereumAccount.isConnected;
const { isConnected, disconnect } = useEthereumWallet();
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@ -117,7 +113,7 @@ const Header = () => {
if (
currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
EVerificationStatus.ENS_VERIFIED &&
delegationInfo?.isValid
) {
return <CheckCircle className="w-4 h-4" />;
@ -127,7 +123,7 @@ const Header = () => {
return <AlertTriangle className="w-4 h-4" />;
} else if (
currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED
EVerificationStatus.ENS_VERIFIED
) {
return <Key className="w-4 h-4" />;
} else {
@ -188,11 +184,11 @@ const Header = () => {
variant="outline"
className={`font-mono text-xs border-0 ${
currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
EVerificationStatus.ENS_VERIFIED &&
delegationInfo?.isValid
? 'bg-green-500/20 text-green-400 border-green-500/30'
: currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED
EVerificationStatus.ENS_VERIFIED
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
}`}
@ -205,7 +201,7 @@ const Header = () => {
: delegationInfo?.isValid
? 'READY'
: currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED
EVerificationStatus.ENS_VERIFIED
? 'EXPIRED'
: 'DELEGATE'}
</span>

View File

@ -1,16 +1,9 @@
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Bitcoin,
Coins,
Shield,
ShieldCheck,
Loader2,
AlertCircle,
} from 'lucide-react';
import { Coins, Shield, ShieldCheck, Loader2, AlertCircle } from 'lucide-react';
import { useAuth } from '@/hooks';
import { EVerificationStatus } from '@opchan/core';
import { OrdinalDetails, EnsDetails } from '@opchan/core';
import { EnsDetails } from '@opchan/core';
interface VerificationStepProps {
onComplete: () => void;
@ -30,7 +23,7 @@ export function VerificationStep({
const [verificationResult, setVerificationResult] = React.useState<{
success: boolean;
message: string;
details?: OrdinalDetails | EnsDetails;
details?: EnsDetails;
} | null>(null);
// Watch for changes in user state after verification
@ -39,29 +32,18 @@ export function VerificationStep({
verificationResult?.success &&
verificationResult.message.includes('Checking ownership')
) {
const hasOwnership =
currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED;
const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_VERIFIED;
if (hasOwnership) {
setVerificationResult({
success: true,
message:
currentUser?.walletType === 'bitcoin'
? 'Ordinal ownership verified successfully!'
: 'ENS ownership verified successfully!',
details:
currentUser?.walletType === 'bitcoin'
? currentUser?.ordinalDetails
: currentUser?.ensDetails,
message: 'ENS ownership verified successfully!',
details: currentUser?.ensDetails,
});
} else {
setVerificationResult({
success: false,
message:
currentUser?.walletType === 'bitcoin'
? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!'
: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
message: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
});
}
}
@ -84,10 +66,7 @@ export function VerificationStep({
if (ok) {
setVerificationResult({
success: true,
message:
currentUser?.walletType === 'bitcoin'
? 'Verification process completed. Checking ownership...'
: 'Verification process completed. Checking ownership...',
message: 'Verification process completed. Checking ownership...',
details: undefined,
});
} else {
@ -116,29 +95,14 @@ export function VerificationStep({
onComplete();
};
const getVerificationType = () => {
return currentUser?.walletType === 'bitcoin'
? 'Bitcoin Ordinal'
: 'Ethereum ENS';
};
const getVerificationType = () => 'Ethereum ENS';
const getVerificationIcon = () => {
return currentUser?.walletType === 'bitcoin' ? Bitcoin : Coins;
};
const getVerificationIcon = () => Coins;
const getVerificationColor = () => {
return currentUser?.walletType === 'bitcoin'
? 'text-orange-500'
: 'text-blue-500';
};
const getVerificationColor = () => 'text-blue-500';
const getVerificationDescription = () => {
if (currentUser?.walletType === 'bitcoin') {
return "Verify your Bitcoin Ordinal ownership to unlock premium features. If you don't own any Ordinals, you can still participate in the forum with your connected wallet.";
} else {
return "Verify your Ethereum ENS ownership to unlock premium features. If you don't own any ENS, you can still participate in the forum with your connected wallet.";
}
};
const getVerificationDescription = () =>
"Verify your Ethereum ENS ownership to unlock premium features. If you don't own any ENS, you can still participate in the forum with your connected wallet.";
// Show verification result
if (verificationResult) {
@ -175,23 +139,12 @@ export function VerificationStep({
</p>
{verificationResult.details && (
<div className="text-xs text-neutral-400">
{currentUser?.walletType === 'bitcoin' ? (
<p>
Ordinal ID:{' '}
{typeof verificationResult.details === 'object' &&
'ordinalId' in verificationResult.details
? verificationResult.details.ordinalId
: 'Verified'}
</p>
) : (
<p>
ENS Name:{' '}
{typeof verificationResult.details === 'object' &&
'ensName' in verificationResult.details
{typeof verificationResult.details === 'object' && 'ensName' in verificationResult.details
? verificationResult.details.ensName
: 'Verified'}
</p>
)}
</div>
)}
</div>
@ -213,7 +166,7 @@ export function VerificationStep({
// Show verification status
if (
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
currentUser?.verificationStatus === EVerificationStatus.ENS_VERIFIED
) {
return (
<div className="flex flex-col h-full">
@ -230,12 +183,7 @@ export function VerificationStep({
</p>
{currentUser && (
<div className="text-xs text-neutral-400">
{currentUser?.walletType === 'bitcoin' && (
<p>Ordinal ID: Verified</p>
)}
{currentUser?.walletType === 'ethereum' && (
<p>ENS Name: Verified</p>
)}
</div>
)}
</div>
@ -281,19 +229,9 @@ export function VerificationStep({
</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
{currentUser?.walletType === 'bitcoin' ? (
<>
<li> We'll check your wallet for Bitcoin Ordinal ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
) : (
<>
<li> We'll check your wallet for ENS domain ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
)}
</ul>
</div>

View File

@ -1,11 +1,8 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Bitcoin, Coins, Loader2 } from 'lucide-react';
import {
useAppKit,
useAppKitAccount,
useAppKitState,
} from '@reown/appkit/react';
import { Wallet, Loader2, CheckCircle } from 'lucide-react';
import { useAuth } from '@opchan/react';
import { useEffect } from 'react';
interface WalletConnectionStepProps {
onComplete: () => void;
@ -18,92 +15,41 @@ export function WalletConnectionStep({
isLoading,
setIsLoading,
}: WalletConnectionStepProps) {
const { initialized } = useAppKitState();
const appKit = useAppKit();
const { isAuthenticated, currentUser, connect } = useAuth();
// Get account info for different chains
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
const ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
// Determine which account is connected
const isBitcoinConnected = bitcoinAccount.isConnected;
const isEthereumConnected = ethereumAccount.isConnected;
const isConnected = isBitcoinConnected || isEthereumConnected;
// Get the active account info
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
const activeAddress = activeAccount.address;
const activeChain = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
const handleBitcoinConnect = async () => {
if (!initialized || !appKit) {
console.error('AppKit not initialized');
return;
}
setIsLoading(true);
try {
await appKit.open({
view: 'Connect',
namespace: 'bip122',
});
} catch (error) {
console.error('Error connecting Bitcoin wallet:', error);
} finally {
setIsLoading(false);
}
};
const handleEthereumConnect = async () => {
if (!initialized || !appKit) {
console.error('AppKit not initialized');
return;
}
setIsLoading(true);
try {
await appKit.open({
view: 'Connect',
namespace: 'eip155',
});
} catch (error) {
console.error('Error connecting Ethereum wallet:', error);
} finally {
setIsLoading(false);
}
};
const handleNext = () => {
// Auto-complete step when wallet connects
useEffect(() => {
if (isAuthenticated && currentUser?.address) {
onComplete();
};
// Show loading state if AppKit is not initialized
if (!initialized) {
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-neutral-400 text-center">
Initializing wallet connection...
</p>
</div>
);
}
}, [isAuthenticated, currentUser, onComplete]);
const handleConnect = async () => {
setIsLoading(true);
try {
connect();
} catch (error) {
console.error('Error connecting wallet:', error);
} finally {
setIsLoading(false);
}
};
// Show connected state
if (isConnected) {
if (isAuthenticated && currentUser) {
return (
<div className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">
Wallet Connected
</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...
{activeAddress?.slice(-4)}
Connected with {currentUser.address.slice(0, 6)}...
{currentUser.address.slice(-4)}
</p>
</div>
</div>
@ -111,7 +57,7 @@ export function WalletConnectionStep({
{/* Action Button */}
<div className="mt-auto">
<Button
onClick={handleNext}
onClick={onComplete}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
@ -122,78 +68,32 @@ export function WalletConnectionStep({
);
}
// Show connection options
// Show connection option
return (
<div className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<p className="text-sm text-neutral-400 text-center">
Choose a network and wallet to connect to OpChan
Connect your Ethereum wallet to use OpChan
</p>
{/* Bitcoin Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs">
Ordinal Verification Required
</Badge>
</div>
<Button
onClick={handleBitcoinConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
'Connect Bitcoin Wallet'
)}
</Button>
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-700" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div>
{/* Ethereum Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3>
<div className="flex items-center gap-2 justify-center">
<Wallet className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum Wallet</h3>
<Badge variant="secondary" className="text-xs">
ENS Ownership Required
ENS Optional
</Badge>
</div>
<Button
onClick={handleEthereumConnect}
onClick={handleConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
style={{
height: '44px',
height: '48px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontSize: '16px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
@ -207,13 +107,16 @@ export function WalletConnectionStep({
Connecting...
</>
) : (
'Connect Ethereum Wallet'
<>
<Wallet className="h-5 w-5" />
Connect Wallet
</>
)}
</Button>
</div>
<div className="text-xs text-neutral-500 text-center pt-2">
Connect your wallet to use OpChan's features
Supports MetaMask, WalletConnect, Coinbase Wallet, and more
</div>
</div>
</div>

View File

@ -8,8 +8,8 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Bitcoin, Coins } from 'lucide-react';
import { useAuth, useAppKitWallet } from '@opchan/react';
import { Coins } from 'lucide-react';
import { useAuth } from '@opchan/react';
interface WalletDialogProps {
open: boolean;
@ -20,52 +20,20 @@ export function WalletConnectionDialog({
open,
onOpenChange,
}: WalletDialogProps) {
const { connect, disconnect } = useAuth();
const wallet = useAppKitWallet();
const { connect, disconnect, currentUser } = useAuth();
const handleDisconnect = async () => {
await disconnect();
onOpenChange(false);
};
const handleBitcoinConnect = () => {
if (!wallet.isInitialized) {
console.error('Wallet not initialized');
return;
}
connect('bitcoin');
const handleConnect = () => {
connect();
};
const handleEthereumConnect = () => {
if (!wallet.isInitialized) {
console.error('Wallet not initialized');
return;
}
connect('ethereum');
};
// Show loading state if wallet is not initialized
if (!wallet.isInitialized) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md border-neutral-800 bg-black text-white">
<DialogHeader>
<DialogTitle className="text-xl">Connect Wallet</DialogTitle>
<DialogDescription className="text-neutral-400">
Initializing wallet connection...
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>
</DialogContent>
</Dialog>
);
}
const isConnected = wallet.isConnected;
const activeChain = wallet.walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum';
const activeAddress = wallet.address;
const isConnected = Boolean(currentUser);
const activeChain = 'Ethereum';
const activeAddress = currentUser?.address;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -82,58 +50,17 @@ export function WalletConnectionDialog({
<div className="grid gap-4 py-4">
{!isConnected ? (
<div className="space-y-4">
{/* Bitcoin Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs">
Ordinal Verification Required
</Badge>
</div>
<div className="space-y-2">
<Button
onClick={handleBitcoinConnect}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
}}
>
Connect Bitcoin Wallet
</Button>
</div>
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-700" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div>
{/* Ethereum Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3>
<Badge variant="secondary" className="text-xs">
ENS Ownership Required
ENS Ownership Recommended
</Badge>
</div>
<div className="space-y-2">
<Button
onClick={handleEthereumConnect}
onClick={handleConnect}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
style={{
height: '44px',
@ -147,7 +74,7 @@ export function WalletConnectionDialog({
gap: '8px',
}}
>
Connect Ethereum Wallet
Connect Wallet
</Button>
</div>
</div>

View File

@ -153,7 +153,7 @@ const FeedPage: React.FC = () => {
Be the first to create a post in a cell!
</p>
{verificationStatus !==
EVerificationStatus.ENS_ORDINAL_VERIFIED && (
EVerificationStatus.ENS_VERIFIED && (
<p className="text-sm text-cyber-neutral/80">
Connect your wallet to start posting
</p>

View File

@ -162,7 +162,7 @@ export default function ProfilePage() {
const getVerificationIcon = () => {
// Use verification level from UserIdentityService (central database store)
switch (currentUser.verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
case EVerificationStatus.ENS_VERIFIED:
return <CheckCircle className="h-4 w-4 text-green-500" />;
case EVerificationStatus.WALLET_CONNECTED:
return <Shield className="h-4 w-4 text-blue-500" />;
@ -176,7 +176,7 @@ export default function ProfilePage() {
const getVerificationText = () => {
// Use verification level from UserIdentityService (central database store)
switch (currentUser.verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
case EVerificationStatus.ENS_VERIFIED:
return 'Owns ENS or Ordinal';
case EVerificationStatus.WALLET_CONNECTED:
return 'Connected Wallet';
@ -190,7 +190,7 @@ export default function ProfilePage() {
const getVerificationColor = () => {
// Use verification level from UserIdentityService (central database store)
switch (currentUser.verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
case EVerificationStatus.ENS_VERIFIED:
return 'bg-green-100 text-green-800 border-green-200';
case EVerificationStatus.WALLET_CONNECTED:
return 'bg-blue-100 text-blue-800 border-blue-200';

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"fileNames":[],"fileInfos":[],"root":[],"options":{"allowJs":true,"allowSyntheticDefaultImports":true,"allowUnreachableCode":false,"allowUnusedLabels":false,"esModuleInterop":true,"exactOptionalPropertyTypes":true,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitOverride":true,"noImplicitReturns":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"strict":true,"strictBindCallApply":true,"strictFunctionTypes":true,"strictNullChecks":true,"strictPropertyInitialization":true},"version":"5.6.3"}

1078
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -39,14 +39,8 @@
"dependencies": {
"@noble/ed25519": "^2.2.3",
"@noble/hashes": "^1.8.0",
"@reown/appkit": "^1.7.17",
"@reown/appkit-adapter-bitcoin": "^1.7.17",
"@reown/appkit-adapter-wagmi": "^1.7.17",
"@reown/appkit-common": "^1.7.17",
"@reown/appkit-controllers": "^1.7.17",
"@waku/sdk": "0.0.36-ff9c430.0",
"clsx": "^2.1.1",
"ordiscan": "^1.3.0",
"tailwind-merge": "^2.5.2",
"uuid": "^11.1.0",
"wagmi": "^2.17.0"

View File

@ -5,12 +5,10 @@ import { ForumActions } from '../lib/forum/ForumActions';
import { RelevanceCalculator } from '../lib/forum/RelevanceCalculator';
import { UserIdentityService } from '../lib/services/UserIdentityService';
import { DelegationManager, delegationManager } from '../lib/delegation';
import WalletManager from '../lib/wallet';
import { MessageService } from '../lib/services/MessageService';
import { WakuConfig } from '../types';
export interface OpChanClientConfig {
ordiscanApiKey: string;
wakuConfig: WakuConfig;
reownProjectId?: string;
}
@ -25,15 +23,11 @@ export class OpChanClient {
readonly messageService: MessageService;
readonly userIdentityService: UserIdentityService;
readonly delegation: DelegationManager = delegationManager;
readonly wallet = WalletManager
constructor(config: OpChanClientConfig) {
this.config = config;
const env: EnvironmentConfig = {
apiKeys: {
ordiscan: config.ordiscanApiKey,
},
reownProjectId: config.reownProjectId,
};

View File

@ -30,7 +30,6 @@ export * from './lib/forum/transformers';
export { BookmarkService } from './lib/services/BookmarkService';
export { MessageService } from './lib/services/MessageService';
export { UserIdentityService, type UserIdentity } from './lib/services/UserIdentityService';
export { ordinals } from './lib/services/Ordinals';
// Export utilities
export * from './lib/utils';
@ -42,10 +41,9 @@ export { default as messageManager } from './lib/waku';
export * from './lib/waku/network';
// Export wallet functionality
export { WalletManager } from './lib/wallet';
export * from './lib/wallet/config';
export { EthereumWallet, EthereumWalletHelpers } from './lib/wallet';
export { wagmiConfig, config } from './lib/wallet/config';
export * from './lib/wallet/types';
export { type WalletAdapter, setWalletAdapter, getWalletAdapter } from './lib/wallet/adapter';
// Primary client API
export { OpChanClient, type OpChanClientConfig } from './client/OpChanClient';

View File

@ -241,7 +241,7 @@ export class LocalDatabase {
if (!existing || timestamp > existing.lastUpdated) {
const nextRecord = {
ensName: existing?.ensName,
ordinalDetails: existing?.ordinalDetails,
ensAvatar: existing?.ensAvatar,
callSign: callSign !== undefined ? callSign : existing?.callSign,
displayPreference,
lastUpdated: timestamp,
@ -663,11 +663,12 @@ export class LocalDatabase {
this.cache.userIdentities[address] ||
{
ensName: undefined,
ordinalDetails: undefined,
ensAvatar: undefined,
callSign: undefined,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
lastUpdated: 0,
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
displayName: '',
};
const merged: UserIdentityCache[string] = {

View File

@ -1,7 +1,7 @@
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '../utils';
import { WalletManager } from '../wallet';
import { EthereumWalletHelpers } from '../wallet/EthereumWallet';
// Set up ed25519 with sha512
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
@ -31,25 +31,22 @@ export class DelegationCrypto {
}
/**
* Verify a wallet signature using WalletManager
* Verify a wallet signature using Ethereum verification
* @param authMessage - The message that was signed
* @param walletSignature - The signature to verify
* @param walletAddress - The wallet address that signed
* @param walletType - The type of wallet
* @returns Promise<boolean> - True if signature is valid
*/
static async verifyWalletSignature(
authMessage: string,
walletSignature: string,
walletAddress: string,
walletType: 'bitcoin' | 'ethereum'
walletAddress: `0x${string}`
): Promise<boolean> {
try {
return await WalletManager.verifySignature(
return await EthereumWalletHelpers.verifySignature(
authMessage,
walletSignature,
walletAddress,
walletType
walletSignature as `0x${string}`,
walletAddress
);
} catch (error) {
console.error('Error verifying wallet signature:', error);

View File

@ -11,8 +11,7 @@ import { DelegationCrypto } from './crypto';
export interface DelegationFullStatus extends DelegationStatus {
publicKey?: string;
address?: string;
walletType?: 'bitcoin' | 'ethereum';
address?: `0x${string}`;
}
export class DelegationManager {
@ -36,8 +35,7 @@ export class DelegationManager {
* Create a delegation with cryptographic proof
*/
async delegate(
address: string,
walletType: 'bitcoin' | 'ethereum',
address: `0x${string}`,
duration: DelegationDuration = '7days',
signFunction: (message: string) => Promise<string>
): Promise<boolean> {
@ -66,7 +64,6 @@ export class DelegationManager {
walletSignature,
expiryTimestamp,
walletAddress: address,
walletType,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
nonce,
@ -210,8 +207,7 @@ export class DelegationManager {
* Get delegation status
*/
async getStatus(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
currentAddress?: `0x${string}`
): Promise<DelegationFullStatus> {
const now = Date.now();
if (
@ -229,9 +225,7 @@ export class DelegationManager {
const hasExpired = now >= delegation.expiryTimestamp;
const addressMatches =
!currentAddress || delegation.walletAddress === currentAddress;
const walletTypeMatches =
!currentWalletType || delegation.walletType === currentWalletType;
const isValid = !hasExpired && addressMatches && walletTypeMatches;
const isValid = !hasExpired && addressMatches;
return {
hasDelegation: true,
@ -241,7 +235,6 @@ export class DelegationManager {
: undefined,
publicKey: delegation.browserPublicKey,
address: delegation.walletAddress,
walletType: delegation.walletType,
proof: isValid ? this.createProof(delegation) : undefined,
};
}
@ -269,7 +262,6 @@ export class DelegationManager {
walletSignature: delegation.walletSignature,
expiryTimestamp: delegation.expiryTimestamp,
walletAddress: delegation.walletAddress,
walletType: delegation.walletType,
};
}
@ -305,8 +297,7 @@ export class DelegationManager {
return await DelegationCrypto.verifyWalletSignature(
proof.authMessage,
proof.walletSignature,
proof.walletAddress,
proof.walletType
proof.walletAddress
);
}
@ -346,8 +337,7 @@ export class DelegationManager {
const walletSigOk = await DelegationCrypto.verifyWalletSignature(
proof.authMessage,
proof.walletSignature,
proof.walletAddress,
proof.walletType
proof.walletAddress
);
if (!walletSigOk) {

View File

@ -7,8 +7,7 @@ export interface DelegationProof {
authMessage: string; // "I authorize browser key: 0xabc... until 1640995200"
walletSignature: string; // Wallet's signature of authMessage
expiryTimestamp: number; // When this delegation expires
walletAddress: string; // Wallet address that signed the delegation
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
walletAddress: `0x${string}`; // Ethereum address that signed the delegation
}
/**

View File

@ -44,10 +44,10 @@ export class ForumActions {
switch (action) {
case 'createCell':
if (verificationStatus !== EVerificationStatus.ENS_ORDINAL_VERIFIED) {
if (verificationStatus !== EVerificationStatus.ENS_VERIFIED) {
return {
valid: false,
error: 'Only ENS or Logos ordinal owners can create cells',
error: 'Only ENS owners can create cells',
};
}
break;
@ -104,8 +104,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedPost);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -173,8 +172,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedComment);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -245,8 +243,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedCell);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -317,8 +314,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedVote);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -389,8 +385,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -470,8 +465,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -550,8 +544,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -618,8 +611,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -699,8 +691,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,
@ -779,8 +770,7 @@ export class ForumActions {
const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) {
const status = await this.delegationManager.getStatus(
currentUser!.address,
currentUser!.walletType
currentUser!.address
);
return {
success: false,

View File

@ -20,8 +20,8 @@ export class RelevanceCalculator {
COMMENT: 0.5,
};
private static readonly VERIFICATION_BONUS = 1.25; // 25% increase for ENS/Ordinal owners
private static readonly BASIC_VERIFICATION_BONUS = 1.1; // 10% increase for basic verified users
private static readonly VERIFICATION_BONUS = 1.25; // 25% increase for ENS verified users
private static readonly BASIC_VERIFICATION_BONUS = 1.1; // 10% increase for wallet-connected users
private static readonly VERIFIED_UPVOTE_BONUS = 0.1;
private static readonly VERIFIED_COMMENTER_BONUS = 0.05;
@ -219,12 +219,11 @@ export class RelevanceCalculator {
}
/**
* Check if a user is verified (has ENS or ordinal ownership, or basic verification)
* Check if a user is verified (has ENS or basic verification)
*/
isUserVerified(user: User): boolean {
return !!(
user.ensDetails ||
user.ordinalDetails ||
user.ensName ||
user.verificationStatus === EVerificationStatus.WALLET_CONNECTED
);
}
@ -238,9 +237,8 @@ export class RelevanceCalculator {
users.forEach(user => {
status[user.address] = {
isVerified: this.isUserVerified(user),
hasENS: !!user.ensDetails,
hasOrdinal: !!user.ordinalDetails,
ensName: user.ensDetails?.ensName,
hasENS: !!user.ensName,
ensName: user.ensName,
verificationStatus: user.verificationStatus,
};
});
@ -285,9 +283,9 @@ export class RelevanceCalculator {
}
// Apply different bonuses based on verification signals from UserVerificationStatus
// Full bonus if author owns ENS or Ordinal. Otherwise, if merely verified (wallet connected), apply basic bonus
// Full bonus if author owns ENS. Otherwise, if merely verified (wallet connected), apply basic bonus
let bonus = 0;
if (authorStatus?.hasENS || authorStatus?.hasOrdinal) {
if (authorStatus?.hasENS) {
bonus = score * (RelevanceCalculator.VERIFICATION_BONUS - 1);
} else if (authorStatus?.isVerified) {
bonus = score * (RelevanceCalculator.BASIC_VERIFICATION_BONUS - 1);

View File

@ -93,7 +93,7 @@ describe('RelevanceCalculator', () => {
const verifiedUser: User = {
address: 'user1',
walletType: 'ethereum',
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
verificationStatus: EVerificationStatus.ENS_VERIFIED,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
ensDetails: {
ensName: 'test.eth',
@ -110,7 +110,7 @@ describe('RelevanceCalculator', () => {
const verifiedUser: User = {
address: 'user3',
walletType: 'bitcoin',
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
verificationStatus: EVerificationStatus.ENS_VERIFIED,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
ordinalDetails: {
ordinalId: '1',
@ -300,7 +300,7 @@ describe('RelevanceCalculator', () => {
{
address: 'user1',
walletType: 'ethereum',
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
verificationStatus: EVerificationStatus.ENS_VERIFIED,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
ensDetails: {
ensName: 'test.eth',

View File

@ -258,9 +258,8 @@ export const getDataFromCache = async (
for (const [address, rec] of Object.entries(userIdentities)) {
userVerificationStatus[address] = {
isVerified: rec.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED,
isVerified: rec.verificationStatus === EVerificationStatus.ENS_VERIFIED,
hasENS: Boolean(rec.ensName),
hasOrdinal: Boolean(rec.ordinalDetails),
ensName: rec.ensName,
verificationStatus: rec.verificationStatus,
};

View File

@ -1,60 +0,0 @@
import { Ordiscan, Inscription } from 'ordiscan';
import { environment } from '../utils/environment';
class Ordinals {
private static instance: Ordinals | null = null;
private ordiscan: Ordiscan;
private readonly PARENT_INSCRIPTION_ID =
'add60add0325f7c82e80d4852a8b8d5c46dbde4317e76fe4def2e718dd84b87ci0';
private constructor(ordiscan: Ordiscan) {
this.ordiscan = ordiscan;
}
// ===== PUBLIC STATIC METHODS =====
static getInstance(): Ordinals {
if (!Ordinals.instance) {
const apiKey = environment.ordiscanApiKey;
if (!apiKey) {
throw new Error('Ordiscan API key is not configured. Please set up the environment.');
}
Ordinals.instance = new Ordinals(new Ordiscan(apiKey));
}
return Ordinals.instance;
}
// ===== PUBLIC INSTANCE METHODS =====
/**
* Get Ordinal details for a Bitcoin address
*/
async getOrdinalDetails(address: string): Promise<Inscription[] | null> {
const inscriptions = await this.ordiscan.address.getInscriptions({
address,
});
if (inscriptions.length > 0) {
if (
inscriptions.some(
inscription =>
inscription.parent_inscription_id === this.PARENT_INSCRIPTION_ID
)
) {
return inscriptions.filter(
inscription =>
inscription.parent_inscription_id === this.PARENT_INSCRIPTION_ID
);
} else {
return null;
}
}
return null;
}
}
export const ordinals = {
getInstance: () => Ordinals.getInstance(),
getOrdinalDetails: async (address: string) => {
return Ordinals.getInstance().getOrdinalDetails(address);
}
};

View File

@ -7,16 +7,13 @@ import {
} from '../../types/waku';
import { MessageService } from './MessageService';
import { localDatabase } from '../database/LocalDatabase';
import { WalletManager } from '../wallet';
import { getWalletAdapter } from '../wallet/adapter';
import { EthereumWalletHelpers } from '../wallet/EthereumWallet';
import type { PublicClient } from 'viem';
export interface UserIdentity {
address: string;
address: `0x${string}`;
ensName?: string;
ordinalDetails?: {
ordinalId: string;
ordinalDetails: string;
};
ensAvatar?: string;
callSign?: string;
displayPreference: EDisplayPreference;
displayName: string;
@ -26,11 +23,20 @@ export interface UserIdentity {
export class UserIdentityService {
private messageService: MessageService;
private publicClient: PublicClient | null = null;
private refreshListeners: Set<(address: string) => void> = new Set();
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
constructor(messageService: MessageService) {
constructor(messageService: MessageService, publicClient?: PublicClient) {
this.messageService = messageService;
this.publicClient = publicClient || null;
}
/**
* Set the public client for ENS resolution
*/
setPublicClient(publicClient: PublicClient): void {
this.publicClient = publicClient;
}
// ===== PUBLIC METHODS =====
@ -81,7 +87,7 @@ export class UserIdentityService {
*/
getAll(): UserIdentity[] {
return Object.entries(localDatabase.cache.userIdentities).map(([address, cached]) => ({
address,
address: address as `0x${string}`,
...cached,
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
}));
@ -224,38 +230,37 @@ export class UserIdentityService {
}
/**
* Resolve user identity from various sources
* Resolve user identity from ENS
*/
private async resolveUserIdentity(
address: string
): Promise<UserIdentity | null> {
try {
console.log('resolveUserIdentity', address);
const [ensName, ordinalDetails] = await Promise.all([
this.resolveENSName(address),
this.resolveOrdinalDetails(address),
]);
const isWalletConnected = (getWalletAdapter()?.isConnected?.() ?? (WalletManager.hasInstance()
? WalletManager.getInstance().isConnected()
: false));
// Only resolve ENS for Ethereum addresses
if (!address.startsWith('0x')) {
return null;
}
const ensData = await this.resolveENSName(address as `0x${string}`);
let verificationStatus: EVerificationStatus;
if (ensName || ordinalDetails) {
verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
if (ensData?.name) {
verificationStatus = EVerificationStatus.ENS_VERIFIED;
} else {
verificationStatus = isWalletConnected ? EVerificationStatus.WALLET_CONNECTED : EVerificationStatus.WALLET_UNCONNECTED;
verificationStatus = EVerificationStatus.WALLET_CONNECTED;
}
const displayPreference = localDatabase.cache.userIdentities[address]?.displayPreference ?? EDisplayPreference.WALLET_ADDRESS;
return {
address,
ensName: ensName || undefined,
ordinalDetails: ordinalDetails || undefined,
address: address as `0x${string}`,
ensName: ensData?.name || undefined,
ensAvatar: ensData?.avatar || undefined,
callSign: undefined, // Will be populated from Waku messages
displayPreference: displayPreference,
displayName: this.getDisplayName({address, ensName, displayPreference}),
displayName: this.getDisplayName({address, ensName: ensData?.name, displayPreference}),
lastUpdated: Date.now(),
verificationStatus,
};
@ -266,49 +271,18 @@ export class UserIdentityService {
}
/**
* Resolve ENS name from Ethereum address with caching to prevent multiple calls
* Resolve ENS name and avatar from Ethereum address with caching
*/
private async resolveENSName(address: string): Promise<string | null> {
if (!address.startsWith('0x')) {
return null; // Not an Ethereum address
}
private async resolveENSName(address: `0x${string}`): Promise<{ name: string | null; avatar: string | null }> {
// Prefer previously persisted ENS if recent
const cached = localDatabase.cache.userIdentities[address];
if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) {
return cached.ensName;
return { name: cached.ensName, avatar: cached.ensAvatar || null };
}
return this.doResolveENSName(address);
}
/**
* Resolve Ordinal details from Bitcoin address
*/
private async resolveOrdinalDetails(
address: string
): Promise<{ ordinalId: string; ordinalDetails: string } | null> {
try {
if (address.startsWith('0x')) {
return null;
}
const inscriptions = await WalletManager.resolveOperatorOrdinals(address);
if (Array.isArray(inscriptions) && inscriptions.length > 0) {
const first = inscriptions[0]!;
return {
ordinalId: first.inscription_id,
ordinalDetails:
first.parent_inscription_id || 'Operator badge present',
};
}
return null;
} catch (error) {
console.error('Failed to resolve Ordinal details:', error);
return null;
}
}
/**
* Notify all listeners that user identity data has changed
*/
@ -326,9 +300,9 @@ export class UserIdentityService {
record: UserIdentityCache[string]
): UserIdentity {
return {
address,
address: address as `0x${string}`,
ensName: record.ensName,
ordinalDetails: record.ordinalDetails,
ensAvatar: record.ensAvatar,
callSign: record.callSign,
displayPreference: record.displayPreference,
displayName: this.getDisplayName({address, ensName: record.ensName, displayPreference: record.displayPreference}),
@ -345,18 +319,20 @@ export class UserIdentityService {
identity: UserIdentity
): Promise<UserIdentity> {
if (!identity.ensName && address.startsWith('0x')) {
const ensName = await this.resolveENSName(address);
if (ensName) {
const ensData = await this.resolveENSName(address as `0x${string}`);
if (ensData.name) {
const updated: UserIdentity = {
...identity,
ensName,
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
ensName: ensData.name,
ensAvatar: ensData.avatar || undefined,
verificationStatus: EVerificationStatus.ENS_VERIFIED,
lastUpdated: Date.now(),
};
await localDatabase.upsertUserIdentity(address, {
ensName,
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
ensName: ensData.name,
ensAvatar: ensData.avatar || undefined,
verificationStatus: EVerificationStatus.ENS_VERIFIED,
lastUpdated: updated.lastUpdated,
});
@ -366,14 +342,17 @@ export class UserIdentityService {
return identity;
}
private async doResolveENSName(address: string): Promise<string | null> {
private async doResolveENSName(address: `0x${string}`): Promise<{ name: string | null; avatar: string | null }> {
try {
// Resolve ENS via centralized WalletManager helper
const ensName = await WalletManager.resolveENS(address);
return ensName || null;
if (!this.publicClient) {
console.warn('No publicClient available for ENS resolution');
return { name: null, avatar: null };
}
return await EthereumWalletHelpers.resolveENS(this.publicClient, address);
} catch (error) {
console.error('Failed to resolve ENS name:', error);
return null;
return { name: null, avatar: null };
}
}
@ -386,7 +365,8 @@ export class UserIdentityService {
case 'verified-basic':
return EVerificationStatus.WALLET_CONNECTED;
case 'verified-owner':
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
case 'ens-ordinal-verified': // Legacy value
return EVerificationStatus.ENS_VERIFIED;
case 'verifying':
return EVerificationStatus.WALLET_CONNECTED; // Temporary state during verification
@ -395,8 +375,8 @@ export class UserIdentityService {
return EVerificationStatus.WALLET_UNCONNECTED;
case EVerificationStatus.WALLET_CONNECTED:
return EVerificationStatus.WALLET_CONNECTED;
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
case EVerificationStatus.ENS_VERIFIED:
return EVerificationStatus.ENS_VERIFIED;
default:
return EVerificationStatus.WALLET_UNCONNECTED;

View File

@ -4,24 +4,16 @@
*/
export interface EnvironmentConfig {
apiKeys?: {
ordiscan?: string;
};
reownProjectId?: string;
}
class Environment {
private config: EnvironmentConfig = {
};
private config: EnvironmentConfig = {};
public configure(config: EnvironmentConfig): void {
this.config = { ...this.config, ...config };
}
public get ordiscanApiKey(): string | undefined {
return this.config.apiKeys?.ordiscan;
}
public get reownProjectId(): string | undefined {
return this.config.reownProjectId;
}

View File

@ -0,0 +1,142 @@
import {
type WalletClient,
type PublicClient,
verifyMessage
} from 'viem';
import { getEnsName, getEnsAvatar, normalize } from 'viem/ens';
/**
* Simplified Ethereum wallet management
* Direct viem integration without multi-chain complexity
*/
export class EthereumWallet {
constructor(
private walletClient: WalletClient,
private publicClient: PublicClient
) {}
/**
* Get the current wallet address
*/
async getAddress(): Promise<`0x${string}`> {
const [address] = await this.walletClient.getAddresses();
if (!address) {
throw new Error('No address found in wallet');
}
return address;
}
/**
* Sign a message with the wallet
*/
async signMessage(message: string): Promise<string> {
const address = await this.getAddress();
return this.walletClient.signMessage({
account: address,
message,
});
}
/**
* Verify a signature against an address
*/
async verifySignature(
message: string,
signature: `0x${string}`,
address: `0x${string}`
): Promise<boolean> {
try {
return await verifyMessage({
address,
message,
signature,
});
} catch (error) {
console.error('Failed to verify signature:', error);
return false;
}
}
/**
* Resolve ENS name and avatar for an address
*/
async resolveENS(address: `0x${string}`): Promise<{
name: string | null;
avatar: string | null;
}> {
try {
const name = await getEnsName(this.publicClient, {
address
});
if (!name) {
return { name: null, avatar: null };
}
const avatar = await getEnsAvatar(this.publicClient, {
name: normalize(name)
});
return { name, avatar };
} catch (error) {
console.error('Failed to resolve ENS:', error);
return { name: null, avatar: null };
}
}
/**
* Check if wallet is connected
*/
isConnected(): boolean {
return !!this.walletClient && !!this.publicClient;
}
}
// Static helper methods for when you don't have a wallet instance
export class EthereumWalletHelpers {
/**
* Resolve ENS name for an address using a public client
*/
static async resolveENS(
publicClient: PublicClient,
address: `0x${string}`
): Promise<{ name: string | null; avatar: string | null }> {
try {
const name = await getEnsName(publicClient, { address });
if (!name) {
return { name: null, avatar: null };
}
const avatar = await getEnsAvatar(publicClient, {
name: normalize(name)
});
return { name, avatar };
} catch (error) {
console.error('Failed to resolve ENS:', error);
return { name: null, avatar: null };
}
}
/**
* Verify a signature (static version)
*/
static async verifySignature(
message: string,
signature: `0x${string}`,
address: `0x${string}`
): Promise<boolean> {
try {
return await verifyMessage({
address,
message,
signature,
});
} catch (error) {
console.error('Failed to verify signature:', error);
return false;
}
}
}

View File

@ -1,56 +1,38 @@
import { AppKitOptions } from '@reown/appkit';
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin';
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { createStorage } from 'wagmi';
import { mainnet, bitcoin, AppKitNetwork } from '@reown/appkit/networks';
import { environment } from '../utils/environment';
import { createConfig, http } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
const networks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, bitcoin];
// Default WalletConnect project ID - users should override with their own
const DEFAULT_PROJECT_ID = '2ead96ea166a03e5ab50e5c190532e72';
const projectId =
environment.reownProjectId ||
process.env.VITE_REOWN_SECRET ||
'2ead96ea166a03e5ab50e5c190532e72';
export function createWagmiConfig(projectId?: string) {
const wcProjectId = projectId || DEFAULT_PROJECT_ID;
if (!projectId) {
throw new Error(
'Reown project ID is not defined. Please set it via config.reownProjectId, VITE_REOWN_SECRET environment variable, or use the default.'
);
return createConfig({
chains: [mainnet],
connectors: [
injected(), // MetaMask, Coinbase Wallet extension, etc.
walletConnect({
projectId: wcProjectId,
metadata: {
name: 'OpChan',
description: 'Decentralized Forum',
url: 'https://opchan.app',
icons: ['https://opchan.app/icon.png'],
},
showQrModal: true,
}),
coinbaseWallet({
appName: 'OpChan',
appLogoUrl: 'https://opchan.app/icon.png',
}),
],
transports: {
[mainnet.id]: http(),
},
});
}
export const wagmiAdapter = new WagmiAdapter({
storage: createStorage({ storage: localStorage }),
ssr: false, // Set to false for Vite/React apps
projectId,
networks,
});
// Export the Wagmi config for the provider
export const config = wagmiAdapter.wagmiConfig;
export const bitcoinAdapter = new BitcoinAdapter({
projectId,
});
const metadata = {
name: 'OpChan',
description: 'Decentralized forum powered by Bitcoin Ordinals',
url:
process.env.NODE_ENV === 'production'
? 'https://opchan.app'
: 'http://localhost:8080',
icons: ['https://opchan.com/logo.png'],
};
export const appkitConfig: AppKitOptions = {
adapters: [wagmiAdapter, bitcoinAdapter],
networks,
metadata,
projectId,
features: {
analytics: false,
socials: false,
allWallets: false,
},
enableWalletConnect: false,
};
// Default config export for convenience
export const wagmiConfig = createWagmiConfig();
export const config = wagmiConfig; // Alias for backward compatibility

View File

@ -1,273 +1,13 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit';
import { ordinals } from '../services/Ordinals';
import {
getEnsName,
verifyMessage as verifyEthereumMessage,
} from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common';
import { config, wagmiAdapter, bitcoinAdapter } from './config';
import { Provider, ProviderController } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types';
import { Inscription } from 'ordiscan';
export class WalletManager {
private static instance: WalletManager | null = null;
private appKit: AppKit;
private activeAccount: UseAppKitAccountReturn;
private activeWalletType: 'bitcoin' | 'ethereum';
private namespace: ChainNamespace;
private constructor(
appKit: AppKit,
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
) {
this.appKit = appKit;
// Determine active wallet (Bitcoin takes priority)
if (bitcoinAccount.isConnected && bitcoinAccount.address) {
this.activeAccount = bitcoinAccount;
this.activeWalletType = 'bitcoin';
this.namespace = 'bip122';
} else if (ethereumAccount.isConnected && ethereumAccount.address) {
this.activeAccount = ethereumAccount;
this.activeWalletType = 'ethereum';
this.namespace = 'eip155';
} else {
throw new Error('No wallet is connected');
}
}
// ===== PUBLIC STATIC METHODS =====
/**
* Create or get the singleton instance
* Simplified Ethereum-only wallet module
*
* This module provides wallet functionality for Ethereum addresses only.
* Bitcoin and Ordinals support has been removed.
*/
static create(
appKit: AppKit,
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
): WalletManager {
// Always create a new instance to reflect current wallet state
WalletManager.instance = new WalletManager(
appKit,
bitcoinAccount,
ethereumAccount
);
return WalletManager.instance;
}
/**
* Get the current instance (throws if not created)
*/
static getInstance(): WalletManager {
if (!WalletManager.instance) {
throw new Error(
'WalletManager not initialized. Call WalletManager.create() first.'
);
}
return WalletManager.instance;
}
/**
* Check if instance exists
*/
static hasInstance(): boolean {
return WalletManager.instance !== null;
}
/**
* Clear the singleton instance
*/
static clear(): void {
WalletManager.instance = null;
}
/**
* Resolve ENS name for an Ethereum address
*/
static async resolveENS(address: string): Promise<string | null> {
try {
const ensName = await getEnsName(config, {
address: address as `0x${string}`,
});
return ensName || null;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
return null;
}
}
/**
* Resolve Ordinal details for a Bitcoin address
*/
static async resolveOperatorOrdinals(
address: string
): Promise<Inscription[] | null> {
try {
return await ordinals.getOrdinalDetails(address);
} catch (error) {
console.warn('Failed to resolve Ordinal details:', error);
return null;
}
}
/**
* Verify a message signature against a wallet address
* @param message - The original message that was signed
* @param signature - The signature to verify
* @param walletAddress - The expected signer's address
* @param walletType - The type of wallet (bitcoin/ethereum)
* @returns Promise<boolean> - True if signature is valid
*/
static async verifySignature(
message: string,
signature: string,
walletAddress: string,
walletType: 'bitcoin' | 'ethereum'
): Promise<boolean> {
try {
if (walletType === 'ethereum') {
return await verifyEthereumMessage(config, {
address: walletAddress as `0x${string}`,
message,
signature: signature as `0x${string}`,
});
} else if (walletType === 'bitcoin') {
//TODO: implement bitcoin signature verification
return true;
}
console.error(
'WalletManager.verifySignature - unknown wallet type:',
walletType
);
return false;
} catch (error) {
console.error(
'WalletManager.verifySignature - error verifying signature:',
error
);
return false;
}
}
// ===== PUBLIC INSTANCE METHODS =====
/**
* Get the currently active wallet
*/
getActiveWallet(): ActiveWallet {
return {
type: this.activeWalletType,
address: this.activeAccount.address!,
isConnected: true,
};
}
/**
* Check if wallet is connected
*/
isConnected(): boolean {
return this.activeAccount.isConnected;
}
/**
* Get the active wallet type
*/
getWalletType(): 'bitcoin' | 'ethereum' {
return this.activeWalletType;
}
/**
* Get address of the active wallet
*/
getAddress(): string {
return this.activeAccount.address!;
}
/**
* Sign a message using the active wallet
*/
async signMessage(message: string): Promise<string> {
try {
// Select adapter based on active namespace using configured instances
const adapter = this.namespace === 'eip155' ? wagmiAdapter : this.namespace === 'bip122' ? bitcoinAdapter : undefined;
if (!adapter) {
throw new Error(`No adapter instance configured for namespace: ${this.namespace}`);
}
// Get the provider for the current connection via ProviderController
const provider = ProviderController.getProvider(this.namespace);
if (!provider) {
throw new Error(`No provider found for namespace: ${this.namespace}`);
}
if (!this.activeAccount.address) {
throw new Error('No address found for active account');
}
// Call the adapter's signMessage method
const result = await adapter.signMessage({
message,
address: this.activeAccount.address,
provider: provider as unknown as Provider,
});
return result.signature;
} catch (error) {
console.error(
`Error signing message with ${this.activeWalletType} wallet:`,
error
);
throw new Error(
`Failed to sign message with ${this.activeWalletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Get comprehensive wallet info including ENS resolution for Ethereum
*/
async getWalletInfo(): Promise<WalletInfo> {
const address = this.activeAccount.address!;
if (this.activeWalletType === 'bitcoin') {
return {
address,
walletType: 'bitcoin',
isConnected: true,
};
}
// For Ethereum, try to resolve ENS name
let ensName: string | undefined;
try {
const resolvedName = await getEnsName(config, {
address: address as `0x${string}`,
});
ensName = resolvedName || undefined;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
}
return {
address,
walletType: 'ethereum',
ensName,
isConnected: true,
};
}
}
export default WalletManager;
export * from './EthereumWallet';
export * from './types';
export * from './config';
// Re-export wagmi/viem for convenience
export { type WalletClient, type PublicClient } from 'viem';

View File

@ -1,12 +1,6 @@
export interface WalletInfo {
address: string;
walletType: 'bitcoin' | 'ethereum';
address: `0x${string}`;
ensName?: string;
isConnected: boolean;
}
export interface ActiveWallet {
type: 'bitcoin' | 'ethereum';
address: string;
ensAvatar?: string;
isConnected: boolean;
}

View File

@ -121,7 +121,6 @@ export interface UserVerificationStatus {
[address: string]: {
isVerified: boolean;
hasENS: boolean;
hasOrdinal: boolean;
ensName?: string;
verificationStatus?: EVerificationStatus;
};

View File

@ -1,9 +1,8 @@
export type User = {
address: string;
walletType: 'bitcoin' | 'ethereum';
address: `0x${string}`;
ordinalDetails?: OrdinalDetails;
ensDetails?: EnsDetails;
ensName?: string;
ensAvatar?: string;
callSign?: string;
displayPreference: EDisplayPreference;
@ -14,23 +13,14 @@ export type User = {
signature?: string;
lastChecked?: number;
browserPubKey?: string; // Browser-generated public key for key delegation
delegationSignature?: string; // Signature from Bitcoin/Ethereum wallet for delegation
delegationSignature?: string; // Signature from Ethereum wallet for delegation
delegationExpiry?: number; // When the delegation expires
};
export enum EVerificationStatus {
WALLET_UNCONNECTED = 'wallet-unconnected',
WALLET_CONNECTED = 'wallet-connected',
ENS_ORDINAL_VERIFIED = 'ens-ordinal-verified',
}
export interface OrdinalDetails {
ordinalId: string;
ordinalDetails: string;
}
export interface EnsDetails {
ensName: string;
ENS_VERIFIED = 'ens-verified',
}
export enum EDisplayPreference {

View File

@ -169,10 +169,7 @@ export interface VoteCache {
export interface UserIdentityCache {
[address: string]: {
ensName?: string;
ordinalDetails?: {
ordinalId: string;
ordinalDetails: string;
};
ensAvatar?: string;
callSign?: string;
displayPreference: EDisplayPreference;
lastUpdated: number;

View File

@ -10,15 +10,15 @@ npm i @opchan/react @opchan/core react react-dom
### Quickstart
#### With Reown AppKit (Recommended)
#### Quickstart Provider (wagmi-only)
OpChan integrates with Reown AppKit for wallet management. The OpChanProvider already wraps WagmiProvider and AppKitProvider internally, so you can mount it directly:
OpChan uses wagmi connectors for wallet management. The OpChanProvider already wraps WagmiProvider and React Query internally, so you can mount it directly:
```tsx
import React from 'react';
import { OpChanProvider } from '@opchan/react';
const opchanConfig = { ordiscanApiKey: 'YOUR_ORDISCAN_API_KEY' };
const opchanConfig = {};
function App() {
return (
@ -69,11 +69,8 @@ export function Connect() {
</>
) : (
<>
<button onClick={() => connect('ethereum')}>
Connect Ethereum
</button>
<button onClick={() => connect('bitcoin')}>
Connect Bitcoin
<button onClick={() => connect()}>
Connect Wallet
</button>
</>
)}
@ -85,12 +82,11 @@ export function Connect() {
### API
- **Providers**
- **`OpChanProvider`**: High-level provider that constructs an `OpChanClient` and integrates with AppKit.
- **`OpChanProvider`**: High-level provider that constructs an `OpChanClient` and wires wagmi + React Query.
- Props:
- `config: OpChanClientConfig` — core client configuration.
- `children: React.ReactNode`.
- Requirements: None — this provider already wraps `WagmiProvider` and `AppKitProvider` internally.
- Internally provides `AppKitWalletProvider` for wallet state management.
- Requirements: None — this provider already wraps `WagmiProvider` and `QueryClientProvider` internally.
- **`AppKitWalletProvider`**: Wallet context provider (automatically included in `OpChanProvider`).
- Provides wallet state and controls from AppKit.
@ -107,9 +103,7 @@ export function Connect() {
`delegate(duration)`, `delegationStatus()`, `clearDelegation()`,
`updateProfile({ callSign?, displayPreference? })`.
- **`useAppKitWallet()`** → AppKit wallet state (low-level)
- Data: `address`, `walletType`, `isConnected`, `isInitialized`.
- Actions: `connect(walletType)`, `disconnect()`.
- **`useContent()`** → forum data & actions
- Data: `cells`, `posts`, `comments`, `bookmarks`, `postsByCell`, `commentsByPost`,
@ -135,7 +129,7 @@ export function Connect() {
- Categories: `'wizardStates' | 'preferences' | 'temporaryStates'` (default `'preferences'`).
- **`useUserDisplay(address)`** → identity details for any address
- Returns `{ address, displayName, callSign?, ensName?, ordinalDetails?, verificationStatus, displayPreference, lastUpdated, isLoading, error }`.
- Returns `{ address, displayName, callSign?, ensName?, verificationStatus, displayPreference, lastUpdated, isLoading, error }`.
- Backed by a centralized identity cache; updates propagate automatically.
- **`useClient()`** → access the underlying `OpChanClient` (advanced use only).
@ -154,9 +148,7 @@ export function Connect() {
if (!(window as any).Buffer) (window as any).Buffer = Buffer
```
- Config values you likely need to pass to `OpChanProvider`:
- `ordiscanApiKey` (optional for dev)
- `wakuConfig` with `contentTopic` and `reliableChannelId`
- `reownProjectId` (e.g., from `import.meta.env.VITE_REOWN_SECRET`)
### License

View File

@ -12,7 +12,7 @@ The examples assume you install and use the `@opchan/react` and `@opchan/core` p
npm i @opchan/react @opchan/core
```
Create an app-level provider using `OpChanProvider`. You must pass a minimal client config (e.g., Ordiscan API key if you have one). OpChanProvider already wraps `WagmiProvider` and `AppKitProvider` from Reown AppKit internally, so mount it directly at the app root.
Create an app-level provider using `OpChanProvider`. The provider already wraps `WagmiProvider` and React Query; no AppKit is required. Mount it directly at the app root.
```tsx
import React from 'react';
@ -20,14 +20,14 @@ import { OpChanProvider } from '@opchan/react';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<OpChanProvider config={{ ordiscanApiKey: 'YOUR_API_KEY' }}>
<OpChanProvider config={{}}>
{children}
</OpChanProvider>
);
}
```
OpChanProvider automatically integrates with AppKit for wallet management.
OpChanProvider uses wagmi connectors (Injected/WalletConnect/Coinbase) for wallet management.
---
@ -66,17 +66,12 @@ function WalletControls() {
<>
<div>Connected: {currentUser.displayName}</div>
<button onClick={() => disconnect()}>Disconnect</button>
<button onClick={() => verifyOwnership()}>Verify ENS/Ordinal</button>
<button onClick={() => verifyOwnership()}>Verify ENS</button>
<div>Status: {verificationStatus}</div>
</>
) : (
<>
<button onClick={() => connect('bitcoin')}>
Connect Bitcoin
</button>
<button onClick={() => connect('ethereum')}>
Connect Ethereum
</button>
<button onClick={() => connect()}>Connect Wallet</button>
</>
)}
</div>
@ -85,8 +80,8 @@ function WalletControls() {
```
Notes:
- `connect(walletType)` opens the AppKit modal for the specified wallet type (bitcoin/ethereum). Upon successful connection, OpChan automatically syncs the wallet state and creates a user session.
- `verifyOwnership()` refreshes identity and sets `EVerificationStatus` appropriately (checks ENS or Bitcoin Ordinals).
- `connect()` opens the selected wagmi connector (e.g., Injected/WalletConnect). Upon successful connection, OpChan automatically syncs the wallet state and creates a user session.
- `verifyOwnership()` refreshes identity and sets `EVerificationStatus` appropriately (checks ENS).
---
@ -327,9 +322,9 @@ API: `useUserDisplay(address)` returns resolved user identity for UI labels.
import { useUserDisplay } from '@opchan/react';
function AuthorName({ address }: { address: string }) {
const { displayName, ensName, ordinalDetails, isLoading } = useUserDisplay(address);
const { displayName, ensName, isLoading } = useUserDisplay(address);
if (isLoading) return <span>Loading…</span>;
return <span title={ensName || ordinalDetails?.ordinalId}>{displayName}</span>;
return <span title={ensName || undefined}>{displayName}</span>;
}
```
@ -367,7 +362,7 @@ function Home() {
export default function App() {
return (
<OpChanProvider config={{ ordiscanApiKey: '' }}>
<OpChanProvider config={{}}>
<Home />
</OpChanProvider>
);
@ -381,6 +376,6 @@ export default function App() {
- Always gate actions with `usePermissions()` to provide clear UX reasons.
- Use `pending.isPending(id)` to show optimistic “syncing…” states for content you just created or voted on.
- The store is hydrated from IndexedDB on load; Waku live messages keep it in sync.
- Identity (ENS/Ordinals/Call Sign) is resolved and cached; calling `verifyOwnership()` or updating the profile will refresh it.
- Identity (ENS/Call Sign) is resolved and cached; calling `verifyOwnership()` or updating the profile will refresh it.

View File

@ -5,7 +5,7 @@ export {
useClient,
} from './v1/context/ClientContext';
export { useAppKitWallet } from './v1/hooks/useAppKitWallet';
export { useEthereumWallet } from './v1/hooks/useEthereumWallet';
export { OpChanProvider } from './v1/provider/OpChanProvider';
export type { OpChanProviderProps } from './v1/provider/OpChanProvider';

View File

@ -1,53 +0,0 @@
import React, { Component, ReactNode } from 'react';
interface AppKitErrorBoundaryState {
hasError: boolean;
error?: Error;
}
interface AppKitErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
export class AppKitErrorBoundary extends Component<
AppKitErrorBoundaryProps,
AppKitErrorBoundaryState
> {
constructor(props: AppKitErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): AppKitErrorBoundaryState {
// Check if this is an AppKit initialization error
if (error.message.includes('createAppKit') ||
error.message.includes('useAppKitState') ||
error.message.includes('AppKit')) {
return { hasError: true, error };
}
return { hasError: false };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.warn('[AppKitErrorBoundary] Caught AppKit error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// Render fallback UI or the children with error handling
return this.props.fallback || (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<h3 className="text-sm font-medium text-yellow-800">
Wallet Connection Initializing...
</h3>
<p className="mt-1 text-sm text-yellow-700">
The wallet system is still loading. Please wait a moment.
</p>
</div>
);
}
return this.props.children;
}
}

View File

@ -1,100 +0,0 @@
import { useCallback, useMemo } from 'react';
import {
useAppKit,
useAppKitAccount,
useDisconnect,
useAppKitState,
} from '@reown/appkit/react';
export interface AppKitWalletState {
address: string | null;
walletType: 'bitcoin' | 'ethereum' | null;
isConnected: boolean;
isInitialized: boolean;
}
export interface AppKitWalletControls {
connect: (walletType: 'bitcoin' | 'ethereum') => void;
disconnect: () => Promise<void>;
}
export type AppKitWalletValue = AppKitWalletState & AppKitWalletControls;
export function useAppKitWallet(): AppKitWalletValue {
// Add error boundary for AppKit hooks
let initialized = false;
let appKit: ReturnType<typeof useAppKit> | null = null;
let appKitDisconnect: (() => Promise<void>) | null = null;
let bitcoinAccount: ReturnType<typeof useAppKitAccount> | null = null;
let ethereumAccount: ReturnType<typeof useAppKitAccount> | null = null;
try {
const appKitState = useAppKitState();
initialized = appKitState?.initialized || false;
appKit = useAppKit();
const disconnectHook = useDisconnect();
appKitDisconnect = disconnectHook?.disconnect;
bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
} catch (error) {
console.warn('[useAppKitWallet] AppKit hooks not available yet:', error);
// Return default values if AppKit is not initialized
return {
address: null,
walletType: null,
isConnected: false,
isInitialized: false,
connect: () => console.warn('AppKit not initialized'),
disconnect: async () => console.warn('AppKit not initialized'),
};
}
// Determine wallet state
const isBitcoinConnected = bitcoinAccount?.isConnected || false;
const isEthereumConnected = ethereumAccount?.isConnected || false;
const isConnected = isBitcoinConnected || isEthereumConnected;
const walletType = useMemo<'bitcoin' | 'ethereum' | null>(() => {
if (isBitcoinConnected) return 'bitcoin';
if (isEthereumConnected) return 'ethereum';
return null;
}, [isBitcoinConnected, isEthereumConnected]);
const address = useMemo<string | null>(() => {
if (isBitcoinConnected && bitcoinAccount?.address) return bitcoinAccount.address;
if (isEthereumConnected && ethereumAccount?.address) return ethereumAccount.address;
return null;
}, [isBitcoinConnected, bitcoinAccount?.address, isEthereumConnected, ethereumAccount?.address]);
const connect = useCallback((targetWalletType: 'bitcoin' | 'ethereum') => {
if (!initialized || !appKit) {
console.error('AppKit not initialized');
return;
}
const namespace = targetWalletType === 'bitcoin' ? 'bip122' : 'eip155';
appKit.open({
view: 'Connect',
namespace,
});
}, [initialized, appKit]);
const disconnect = useCallback(async () => {
if (appKitDisconnect) {
await appKitDisconnect();
}
}, [appKitDisconnect]);
return {
address,
walletType,
isConnected,
isInitialized: initialized,
connect,
disconnect,
};
}

View File

@ -1,31 +1,29 @@
import React from 'react';
import { useClient } from '../context/ClientContext';
import { useAppKitWallet } from '../hooks/useAppKitWallet';
import { useEthereumWallet } from './useEthereumWallet';
import { useOpchanStore, setOpchanState } from '../store/opchanStore';
import {
User,
EVerificationStatus,
DelegationDuration,
EDisplayPreference,
WalletManager,
} from '@opchan/core';
import type { DelegationFullStatus } from '@opchan/core';
export function useAuth() {
const client = useClient();
const wallet = useAppKitWallet();
const wallet = useEthereumWallet();
const currentUser = useOpchanStore(s => s.session.currentUser);
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
const delegation = useOpchanStore(s => s.session.delegation);
// Sync AppKit wallet state to OpChan session
// Sync Ethereum wallet state to OpChan session
React.useEffect(() => {
const syncWallet = async () => {
if (wallet.isConnected && wallet.address && wallet.walletType) {
if (wallet.isConnected && wallet.address) {
// Wallet connected - create/update user session
const baseUser: User = {
address: wallet.address,
walletType: wallet.walletType,
displayName: wallet.address.slice(0, 6) + '...' + wallet.address.slice(-4),
displayPreference: EDisplayPreference.WALLET_ADDRESS,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
@ -33,6 +31,11 @@ export function useAuth() {
};
try {
// Set public client for ENS resolution
if (wallet.publicClient) {
client.userIdentityService.setPublicClient(wallet.publicClient);
}
await client.database.storeUser(baseUser);
// Prime identity service so display name/ens are cached
const identity = await client.userIdentityService.getIdentity(baseUser.address);
@ -80,10 +83,10 @@ export function useAuth() {
};
syncWallet();
}, [wallet.isConnected, wallet.address, wallet.walletType, client]);
}, [wallet.isConnected, wallet.address, wallet.publicClient, client, currentUser]);
const connect = React.useCallback((walletType: 'bitcoin' | 'ethereum'): void => {
wallet.connect(walletType);
const connect = React.useCallback((): void => {
wallet.connect();
}, [wallet]);
const disconnect = React.useCallback(async (): Promise<void> => {
@ -91,7 +94,7 @@ export function useAuth() {
}, [wallet]);
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
console.log('verifyOwnership')
console.log('verifyOwnership');
const user = currentUser;
if (!user) return false;
try {
@ -110,7 +113,7 @@ export function useAuth() {
await client.database.upsertUserIdentity(user.address, {
displayName: identity.displayName,
ensName: identity?.ensName || undefined,
ordinalDetails: identity?.ordinalDetails,
ensAvatar: identity?.ensAvatar || undefined,
verificationStatus: identity.verificationStatus,
lastUpdated: Date.now(),
});
@ -119,7 +122,7 @@ export function useAuth() {
...prev,
session: { ...prev.session, currentUser: updated, verificationStatus: identity.verificationStatus },
}));
return identity.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
return identity.verificationStatus === EVerificationStatus.ENS_VERIFIED;
} catch (e) {
console.error('verifyOwnership failed', e);
return false;
@ -132,15 +135,13 @@ export function useAuth() {
const user = currentUser;
if (!user) return false;
try {
const signer = ((message: string) => WalletManager.getInstance().signMessage(message));
const ok = await client.delegation.delegate(
user.address,
user.walletType,
duration,
signer,
wallet.signMessage,
);
const status = await client.delegation.getStatus(user.address, user.walletType);
const status = await client.delegation.getStatus(user.address);
setOpchanState(prev => ({
...prev,
session: { ...prev.session, delegation: status },
@ -150,12 +151,12 @@ export function useAuth() {
console.error('delegate failed', e);
return false;
}
}, [client, currentUser]);
}, [client, currentUser, wallet]);
const delegationStatus = React.useCallback(async () => {
const user = currentUser;
if (!user) return { hasDelegation: false, isValid: false } as const;
return client.delegation.getStatus(user.address, user.walletType);
return client.delegation.getStatus(user.address);
}, [client, currentUser]);
const clearDelegation = React.useCallback(async (): Promise<boolean> => {
@ -222,7 +223,3 @@ export function useAuth() {
updateProfile,
} as const;
}

View File

@ -13,7 +13,7 @@ import {
import { BookmarkService } from '@opchan/core';
function reflectCache(client: ReturnType<typeof useClient>): void {
getDataFromCache().then(({ cells, posts, comments }) => {
getDataFromCache().then(({ cells, posts, comments }: { cells: Cell[]; posts: Post[]; comments: Comment[] }) => {
setOpchanState(prev => ({
...prev,
content: {
@ -27,7 +27,7 @@ function reflectCache(client: ReturnType<typeof useClient>): void {
pendingVotes: prev.content.pendingVotes,
},
}));
}).catch(err => {
}).catch((err: Error) => {
console.error('reflectCache failed', err);
});
}
@ -77,17 +77,17 @@ export function useContent() {
const identities = client.database.cache.userIdentities;
const result: UserVerificationStatus = {};
for (const [address, rec] of Object.entries(identities)) {
if (rec) {
const hasEns = Boolean(rec.ensName);
const hasOrdinal = Boolean(rec.ordinalDetails);
const isVerified = rec.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
const isVerified = rec.verificationStatus === EVerificationStatus.ENS_VERIFIED;
result[address] = {
isVerified,
hasENS: hasEns,
hasOrdinal,
ensName: rec.ensName,
verificationStatus: rec.verificationStatus,
};
}
}
return result;
}, [client.database.cache.userIdentities]);

View File

@ -0,0 +1,71 @@
import { useCallback } from 'react';
import {
useAccount,
useConnect,
useDisconnect,
useSignMessage,
usePublicClient,
useWalletClient,
type Connector,
} from 'wagmi';
export interface EthereumWalletState {
address: `0x${string}` | null;
isConnected: boolean;
isInitialized: boolean; // Always true if wagmi is initialized
}
export interface EthereumWalletControls {
connect: (connectorId?: string) => Promise<void>;
disconnect: () => Promise<void>;
signMessage: (message: string) => Promise<string>;
connectors: readonly Connector[];
publicClient: ReturnType<typeof usePublicClient>;
walletClient: ReturnType<typeof useWalletClient>['data'];
}
export type EthereumWalletValue = EthereumWalletState & EthereumWalletControls;
export function useEthereumWallet(): EthereumWalletValue {
const { address, isConnected } = useAccount();
const { connectAsync, connectors } = useConnect();
const { disconnectAsync } = useDisconnect();
const { signMessageAsync } = useSignMessage();
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const connect = useCallback(async (connectorId?: string) => {
const connector = connectorId
? connectors.find(c => c.id === connectorId)
: connectors[0]; // Default to first connector if none specified
if (!connector) {
throw new Error('No connector found');
}
await connectAsync({ connector });
}, [connectAsync, connectors]);
const disconnect = useCallback(async () => {
await disconnectAsync();
}, [disconnectAsync]);
const signMessage = useCallback(async (message: string) => {
if (!address) {
throw new Error('No wallet connected to sign message');
}
return signMessageAsync({ message });
}, [address, signMessageAsync]);
return {
address: address ?? null,
isConnected,
isInitialized: true, // Wagmi is always "initialized" if hooks are available
connect,
disconnect,
signMessage,
connectors,
publicClient,
walletClient,
};
}

View File

@ -5,10 +5,10 @@ export function usePermissions() {
const { session, content } = useOpchanStore(s => ({ session: s.session, content: s.content }));
const currentUser = session.currentUser;
const isVerified = session.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
const isVerified = session.verificationStatus === EVerificationStatus.ENS_VERIFIED;
const isConnected = session.verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED;
const canCreateCell = isVerified || isConnected;
const canCreateCell = isVerified;
const canPost = isConnected;
const canComment = isConnected;
const canVote = isConnected;

View File

@ -78,12 +78,12 @@ export function useUserDisplay(address: string): UserDisplayInfo {
}
return {
address,
address: address as `0x${string}`,
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
lastUpdated: 0,
callSign: undefined,
ensName: undefined,
ordinalDetails: undefined,
ensAvatar: undefined,
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
isLoading,

View File

@ -2,21 +2,28 @@ import React from "react";
import { OpChanClient, type OpChanClientConfig } from "@opchan/core";
import { ClientProvider } from "../context/ClientContext";
import { StoreWiring } from "./StoreWiring";
import { WalletAdapterInitializer } from "./WalletAdapterInitializer";
import { AppKitProvider } from "@reown/appkit/react";
import { WagmiProvider } from "wagmi";
import { appkitConfig, config as wagmiConfig } from "@opchan/core";
import { AppKitErrorBoundary } from "../components/AppKitErrorBoundary";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { wagmiConfig } from "@opchan/core";
export interface OpChanProviderProps {
config: OpChanClientConfig;
children: React.ReactNode;
}
// Create a default QueryClient instance
const defaultQueryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
});
/**
* OpChan provider that constructs the OpChanClient and provides wallet context.
* This component already wraps WagmiProvider and AppKitProvider internally,
* so you can mount it directly at your app root with the required config.
* Simplified to use WagmiProvider + QueryClient only (no AppKit).
*/
export const OpChanProvider: React.FC<OpChanProviderProps> = ({
config,
@ -26,15 +33,12 @@ export const OpChanProvider: React.FC<OpChanProviderProps> = ({
return (
<WagmiProvider config={wagmiConfig}>
<AppKitErrorBoundary>
<AppKitProvider {...appkitConfig}>
<QueryClientProvider client={defaultQueryClient}>
<ClientProvider client={client}>
<WalletAdapterInitializer />
<StoreWiring />
{children}
</ClientProvider>
</AppKitProvider>
</AppKitErrorBoundary>
</QueryClientProvider>
</WagmiProvider>
);
};

View File

@ -59,10 +59,9 @@ export const StoreWiring: React.FC = () => {
// Hydrate session (user + delegation) from LocalDatabase
try {
const loadedUser = await client.database.loadUser();
const delegationStatus = await client.delegation.getStatus(
loadedUser?.address,
loadedUser?.walletType,
);
const delegationStatus = loadedUser?.address
? await client.delegation.getStatus(loadedUser.address)
: null;
setOpchanState(prev => ({
...prev,
@ -128,7 +127,7 @@ export const StoreWiring: React.FC = () => {
});
// Reactively update ALL identities when they refresh (not just current user)
unsubIdentity = client.userIdentityService.subscribe(async (address: string, identity) => {
unsubIdentity = client.userIdentityService.subscribe(async (address: string, identity: UserIdentity | null) => {
try {
if (!identity) {
// Try to fetch if not provided

View File

@ -1,57 +0,0 @@
import React from 'react';
import { useAppKit, useAppKitAccount, useAppKitState } from '@reown/appkit/react';
import type { UseAppKitAccountReturn } from '@reown/appkit/react';
import { WalletManager } from '@opchan/core';
import type { AppKit } from '@reown/appkit';
export const WalletAdapterInitializer: React.FC = () => {
// Add error boundary for AppKit hooks
let initialized = false;
let appKit: ReturnType<typeof useAppKit> | null = null;
let btc: UseAppKitAccountReturn | null = null;
let eth: UseAppKitAccountReturn | null = null;
try {
const appKitState = useAppKitState();
initialized = appKitState?.initialized || false;
appKit = useAppKit();
btc = useAppKitAccount({ namespace: 'bip122' });
eth = useAppKitAccount({ namespace: 'eip155' });
} catch (error) {
console.warn('[WalletAdapterInitializer] AppKit hooks not available yet:', error);
// Return early if AppKit is not initialized
return null;
}
React.useEffect(() => {
// Only proceed if AppKit is properly initialized
if (!initialized || !appKit || !btc || !eth) {
return;
}
const isBtc = btc.isConnected && !!btc.address;
const isEth = eth.isConnected && !!eth.address;
const anyConnected = isBtc || isEth;
if (anyConnected) {
// Initialize WalletManager for signing flows
try {
WalletManager.create(
appKit as unknown as AppKit, btc, eth
);
} catch (e) {
console.warn('[WalletAdapterInitializer] WalletManager.create failed', e);
}
return () => {
try { WalletManager.clear(); } catch (e) { console.warn('[WalletAdapterInitializer] WalletManager.clear failed', e); }
};
}
// No connection: clear manager
try { WalletManager.clear(); } catch (e) { console.warn('[WalletAdapterInitializer] WalletManager.clear failed', e); }
}, [initialized, appKit, btc?.isConnected, btc?.address, eth?.isConnected, eth?.address]);
return null;
};