mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore: remove bitcoin + appkit, use eth + viem/wagmi
This commit is contained in:
parent
45fea2397a
commit
05fc7b6da3
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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' };
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -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
1078
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
142
packages/core/src/lib/wallet/EthereumWallet.ts
Normal file
142
packages/core/src/lib/wallet/EthereumWallet.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -121,7 +121,6 @@ export interface UserVerificationStatus {
|
||||
[address: string]: {
|
||||
isVerified: boolean;
|
||||
hasENS: boolean;
|
||||
hasOrdinal: boolean;
|
||||
ensName?: string;
|
||||
verificationStatus?: EVerificationStatus;
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
71
packages/react/src/v1/hooks/useEthereumWallet.ts
Normal file
71
packages/react/src/v1/hooks/useEthereumWallet.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user