mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 21:33:09 +00:00
chore: remove bitcoin + appkit, use eth + viem/wagmi
This commit is contained in:
parent
45fea2397a
commit
05fc7b6da3
@ -1,14 +1,13 @@
|
|||||||
# OpChan
|
# 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
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js 18+ and npm
|
- Node.js 18+ and npm
|
||||||
- [Phantom Wallet](https://phantom.app/) browser extension
|
- Ethereum wallet (e.g., MetaMask, Coinbase Wallet) or WalletConnect-compatible wallet
|
||||||
- Bitcoin Ordinals (required for posting, optional for reading)
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@ -31,12 +30,7 @@ A decentralized forum application built as a Proof of Concept for a Waku-powered
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env` to configure development settings:
|
Edit `.env` to configure development settings as needed for local testing.
|
||||||
|
|
||||||
```env
|
|
||||||
# Set to 'true' to bypass verification in development
|
|
||||||
VITE_OPCHAN_MOCK_ORDINAL_CHECK=false
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Start development server**
|
4. **Start development server**
|
||||||
```bash
|
```bash
|
||||||
@ -69,8 +63,8 @@ src/
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
1. **Connect Wallet**: Click "Connect Wallet" and approve the Phantom wallet connection
|
1. **Connect Wallet**: Click "Connect Wallet" and approve the wallet connection
|
||||||
2. **Verify Ordinals**: The app will check if your wallet contains Logos Operator Bitcoin Ordinals
|
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
|
3. **Browse Cells**: View existing discussion boards on the dashboard
|
||||||
4. **Create Content**: Create new cells, posts, or comments (requires Ordinals)
|
4. **Create Content**: Create new cells, posts, or comments (requires Ordinals)
|
||||||
5. **Moderate**: Cell creators can moderate their boards
|
5. **Moderate**: Cell creators can moderate their boards
|
||||||
@ -79,7 +73,7 @@ src/
|
|||||||
|
|
||||||
OpChan uses a two-tier authentication system:
|
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
|
2. **Key Delegation**: Optional browser key generation for improved UX
|
||||||
- Reduces wallet signature prompts
|
- Reduces wallet signature prompts
|
||||||
- Configurable duration: 1 week or 30 days
|
- 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:
|
OpChan implements a decentralized architecture with these key components:
|
||||||
|
|
||||||
- **Waku Protocol**: Handles peer-to-peer messaging and content distribution
|
- **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
|
- **Key Delegation**: Improves UX while maintaining security
|
||||||
- **Content Addressing**: Messages are cryptographically signed and verifiable
|
- **Content Addressing**: Messages are cryptographically signed and verifiable
|
||||||
- **Moderation Layer**: Cell-based moderation without global censorship
|
- **Moderation Layer**: Cell-based moderation without global censorship
|
||||||
|
|||||||
@ -14,17 +14,17 @@ The OpChan application has successfully implemented most core functionality incl
|
|||||||
|
|
||||||
### ✅ IMPLEMENTED
|
### ✅ IMPLEMENTED
|
||||||
|
|
||||||
#### 1. Bitcoin Key Authentication
|
#### 1. Ethereum Wallet Authentication
|
||||||
|
|
||||||
- **Status**: ✅ Fully Implemented
|
- **Status**: ✅ Fully Implemented
|
||||||
- **Implementation**: `src/lib/identity/wallets/ReOwnWalletService.ts`, `src/contexts/AuthContext.tsx`
|
- **Implementation**: `src/lib/services/UserIdentityService.ts`, wagmi wiring in provider
|
||||||
- **Details**: Complete Bitcoin wallet integration with message signing capabilities
|
- **Details**: Ethereum wallet integration with message signing capabilities
|
||||||
|
|
||||||
#### 2. Cell Creation Restrictions
|
#### 2. Cell Creation Restrictions
|
||||||
|
|
||||||
- **Status**: ✅ Fully Implemented
|
- **Status**: ✅ Fully Implemented
|
||||||
- **Implementation**: `src/lib/forum/ForumActions.ts`, `src/components/CreateCellDialog.tsx`
|
- **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
|
#### 3. Content Visibility
|
||||||
|
|
||||||
@ -65,8 +65,8 @@ The OpChan application has successfully implemented most core functionality incl
|
|||||||
#### 9. Web3 Key Authentication
|
#### 9. Web3 Key Authentication
|
||||||
|
|
||||||
- **Status**: ✅ Fully Implemented
|
- **Status**: ✅ Fully Implemented
|
||||||
- **Implementation**: `src/lib/identity/wallets/ReOwnWalletService.ts`
|
- **Implementation**: wagmi connectors; `src/lib/services/UserIdentityService.ts`
|
||||||
- **Details**: Ethereum wallet support alongside Bitcoin
|
- **Details**: Ethereum wallet support with ENS resolution
|
||||||
|
|
||||||
#### 10. Relevance Index System
|
#### 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
|
- **Details**: Interface exists but no UI for setting up call signs
|
||||||
- **Missing**: User interface for call sign configuration
|
- **Missing**: User interface for call sign configuration
|
||||||
|
|
||||||
#### 14. Ordinal Avatar Display
|
#### 14. ENS Avatar Display
|
||||||
|
|
||||||
- **Status**: ⚠️ Partially Implemented
|
- **Status**: ⚠️ Partially Implemented
|
||||||
- **Implementation**: `src/components/ui/author-display.tsx`
|
- **Implementation**: `src/components/ui/author-display.tsx`
|
||||||
- **Details**: Basic ordinal detection but limited avatar display
|
- **Details**: ENS name display; avatar resolution depends on ENS records
|
||||||
- **Missing**: Full ordinal image integration and display
|
- **Missing**: Consistent avatar fallback behavior
|
||||||
|
|
||||||
### ❌ NOT IMPLEMENTED
|
### ❌ NOT IMPLEMENTED
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ The OpChan application has successfully implemented most core functionality incl
|
|||||||
|
|
||||||
- **Status**: ✅ Fully Implemented
|
- **Status**: ✅ Fully Implemented
|
||||||
- **Implementation**: `src/contexts/AuthContext.tsx`
|
- **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
|
### ✅ IMPLEMENTED
|
||||||
|
|
||||||
#### 1. Centralized Ordinal API
|
#### 1. ENS Resolution
|
||||||
|
|
||||||
- **Status**: ✅ Fully Implemented
|
- **Status**: ✅ Fully Implemented
|
||||||
- **Implementation**: `src/lib/identity/ordinal.ts`
|
- **Implementation**: ENS resolution via viem public client in `UserIdentityService`
|
||||||
- **Details**: Integration with Logos dashboard API
|
- **Details**: Name + avatar (when published in ENS records)
|
||||||
|
|
||||||
#### 2. Waku Network Integration
|
#### 2. Waku Network Integration
|
||||||
|
|
||||||
@ -264,9 +264,9 @@ The OpChan application has successfully implemented most core functionality incl
|
|||||||
- Implement call sign validation and uniqueness
|
- Implement call sign validation and uniqueness
|
||||||
- Estimated effort: 3-4 days
|
- Estimated effort: 3-4 days
|
||||||
|
|
||||||
3. **Enhanced Ordinal Display**
|
3. **Enhanced ENS Display**
|
||||||
- Integrate full ordinal image display
|
- Improve ENS avatar and fallback handling
|
||||||
- Add ordinal metadata visualization
|
- Cache ENS lookups with smarter TTL
|
||||||
- Estimated effort: 2-3 days
|
- Estimated effort: 2-3 days
|
||||||
|
|
||||||
### Medium Priority
|
### Medium Priority
|
||||||
@ -329,7 +329,7 @@ OpChan has successfully implemented the vast majority of FURPS requirements, pro
|
|||||||
**Areas for Improvement:**
|
**Areas for Improvement:**
|
||||||
|
|
||||||
- User personalization features (bookmarks, call signs)
|
- User personalization features (bookmarks, call signs)
|
||||||
- Enhanced ordinal integration
|
- Enhanced ENS integration
|
||||||
- Advanced search and filtering
|
- 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.
|
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": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@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",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const FeedSidebar: React.FC = () => {
|
|||||||
|
|
||||||
// User's verification status display
|
// User's verification status display
|
||||||
const getVerificationBadge = () => {
|
const getVerificationBadge = () => {
|
||||||
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
if (verificationStatus === EVerificationStatus.ENS_VERIFIED) {
|
||||||
return { text: 'Verified Owner', color: 'bg-green-500' };
|
return { text: 'Verified Owner', color: 'bg-green-500' };
|
||||||
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
|
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
|
||||||
return { text: 'Verified', color: 'bg-blue-500' };
|
return { text: 'Verified', color: 'bg-blue-500' };
|
||||||
|
|||||||
@ -42,7 +42,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
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 { WalletWizard } from '@/components/ui/wallet-wizard';
|
||||||
|
|
||||||
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
||||||
@ -55,11 +55,7 @@ const Header = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { content } = useForum();
|
const { content } = useForum();
|
||||||
|
|
||||||
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
const { isConnected, disconnect } = useEthereumWallet();
|
||||||
const ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
|
|
||||||
const { disconnect } = useDisconnect();
|
|
||||||
|
|
||||||
const isConnected = bitcoinAccount.isConnected || ethereumAccount.isConnected;
|
|
||||||
|
|
||||||
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
@ -117,7 +113,7 @@ const Header = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
currentUser?.verificationStatus ===
|
currentUser?.verificationStatus ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
EVerificationStatus.ENS_VERIFIED &&
|
||||||
delegationInfo?.isValid
|
delegationInfo?.isValid
|
||||||
) {
|
) {
|
||||||
return <CheckCircle className="w-4 h-4" />;
|
return <CheckCircle className="w-4 h-4" />;
|
||||||
@ -127,7 +123,7 @@ const Header = () => {
|
|||||||
return <AlertTriangle className="w-4 h-4" />;
|
return <AlertTriangle className="w-4 h-4" />;
|
||||||
} else if (
|
} else if (
|
||||||
currentUser?.verificationStatus ===
|
currentUser?.verificationStatus ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
EVerificationStatus.ENS_VERIFIED
|
||||||
) {
|
) {
|
||||||
return <Key className="w-4 h-4" />;
|
return <Key className="w-4 h-4" />;
|
||||||
} else {
|
} else {
|
||||||
@ -188,11 +184,11 @@ const Header = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={`font-mono text-xs border-0 ${
|
className={`font-mono text-xs border-0 ${
|
||||||
currentUser?.verificationStatus ===
|
currentUser?.verificationStatus ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
EVerificationStatus.ENS_VERIFIED &&
|
||||||
delegationInfo?.isValid
|
delegationInfo?.isValid
|
||||||
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
||||||
: currentUser?.verificationStatus ===
|
: currentUser?.verificationStatus ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
EVerificationStatus.ENS_VERIFIED
|
||||||
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
|
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
|
||||||
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
||||||
}`}
|
}`}
|
||||||
@ -205,7 +201,7 @@ const Header = () => {
|
|||||||
: delegationInfo?.isValid
|
: delegationInfo?.isValid
|
||||||
? 'READY'
|
? 'READY'
|
||||||
: currentUser?.verificationStatus ===
|
: currentUser?.verificationStatus ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
EVerificationStatus.ENS_VERIFIED
|
||||||
? 'EXPIRED'
|
? 'EXPIRED'
|
||||||
: 'DELEGATE'}
|
: 'DELEGATE'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Coins, Shield, ShieldCheck, Loader2, AlertCircle } from 'lucide-react';
|
||||||
Bitcoin,
|
|
||||||
Coins,
|
|
||||||
Shield,
|
|
||||||
ShieldCheck,
|
|
||||||
Loader2,
|
|
||||||
AlertCircle,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAuth } from '@/hooks';
|
import { useAuth } from '@/hooks';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
import { OrdinalDetails, EnsDetails } from '@opchan/core';
|
import { EnsDetails } from '@opchan/core';
|
||||||
|
|
||||||
interface VerificationStepProps {
|
interface VerificationStepProps {
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
@ -30,7 +23,7 @@ export function VerificationStep({
|
|||||||
const [verificationResult, setVerificationResult] = React.useState<{
|
const [verificationResult, setVerificationResult] = React.useState<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
details?: OrdinalDetails | EnsDetails;
|
details?: EnsDetails;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Watch for changes in user state after verification
|
// Watch for changes in user state after verification
|
||||||
@ -39,29 +32,18 @@ export function VerificationStep({
|
|||||||
verificationResult?.success &&
|
verificationResult?.success &&
|
||||||
verificationResult.message.includes('Checking ownership')
|
verificationResult.message.includes('Checking ownership')
|
||||||
) {
|
) {
|
||||||
const hasOwnership =
|
const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_VERIFIED;
|
||||||
currentUser?.verificationStatus ===
|
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
|
||||||
|
|
||||||
if (hasOwnership) {
|
if (hasOwnership) {
|
||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message: 'ENS ownership verified successfully!',
|
||||||
currentUser?.walletType === 'bitcoin'
|
details: currentUser?.ensDetails,
|
||||||
? 'Ordinal ownership verified successfully!'
|
|
||||||
: 'ENS ownership verified successfully!',
|
|
||||||
details:
|
|
||||||
currentUser?.walletType === 'bitcoin'
|
|
||||||
? currentUser?.ordinalDetails
|
|
||||||
: currentUser?.ensDetails,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
|
||||||
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!',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,10 +66,7 @@ export function VerificationStep({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message: 'Verification process completed. Checking ownership...',
|
||||||
currentUser?.walletType === 'bitcoin'
|
|
||||||
? 'Verification process completed. Checking ownership...'
|
|
||||||
: 'Verification process completed. Checking ownership...',
|
|
||||||
details: undefined,
|
details: undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -116,29 +95,14 @@ export function VerificationStep({
|
|||||||
onComplete();
|
onComplete();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationType = () => {
|
const getVerificationType = () => 'Ethereum ENS';
|
||||||
return currentUser?.walletType === 'bitcoin'
|
|
||||||
? 'Bitcoin Ordinal'
|
|
||||||
: 'Ethereum ENS';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVerificationIcon = () => {
|
const getVerificationIcon = () => Coins;
|
||||||
return currentUser?.walletType === 'bitcoin' ? Bitcoin : Coins;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVerificationColor = () => {
|
const getVerificationColor = () => 'text-blue-500';
|
||||||
return currentUser?.walletType === 'bitcoin'
|
|
||||||
? 'text-orange-500'
|
|
||||||
: 'text-blue-500';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVerificationDescription = () => {
|
const getVerificationDescription = () =>
|
||||||
if (currentUser?.walletType === 'bitcoin') {
|
"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.";
|
||||||
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.";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show verification result
|
// Show verification result
|
||||||
if (verificationResult) {
|
if (verificationResult) {
|
||||||
@ -175,23 +139,12 @@ export function VerificationStep({
|
|||||||
</p>
|
</p>
|
||||||
{verificationResult.details && (
|
{verificationResult.details && (
|
||||||
<div className="text-xs text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
{currentUser?.walletType === 'bitcoin' ? (
|
<p>
|
||||||
<p>
|
ENS Name:{' '}
|
||||||
Ordinal ID:{' '}
|
{typeof verificationResult.details === 'object' && 'ensName' in verificationResult.details
|
||||||
{typeof verificationResult.details === 'object' &&
|
? verificationResult.details.ensName
|
||||||
'ordinalId' in verificationResult.details
|
: 'Verified'}
|
||||||
? verificationResult.details.ordinalId
|
</p>
|
||||||
: 'Verified'}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
ENS Name:{' '}
|
|
||||||
{typeof verificationResult.details === 'object' &&
|
|
||||||
'ensName' in verificationResult.details
|
|
||||||
? verificationResult.details.ensName
|
|
||||||
: 'Verified'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -213,7 +166,7 @@ export function VerificationStep({
|
|||||||
|
|
||||||
// Show verification status
|
// Show verification status
|
||||||
if (
|
if (
|
||||||
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
currentUser?.verificationStatus === EVerificationStatus.ENS_VERIFIED
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@ -230,12 +183,7 @@ export function VerificationStep({
|
|||||||
</p>
|
</p>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className="text-xs text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
{currentUser?.walletType === 'bitcoin' && (
|
<p>ENS Name: Verified</p>
|
||||||
<p>Ordinal ID: Verified</p>
|
|
||||||
)}
|
|
||||||
{currentUser?.walletType === 'ethereum' && (
|
|
||||||
<p>ENS Name: Verified</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -281,19 +229,9 @@ export function VerificationStep({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="text-xs text-neutral-400 space-y-1">
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
{currentUser?.walletType === 'bitcoin' ? (
|
<li>• We'll check your wallet for ENS domain ownership</li>
|
||||||
<>
|
<li>• If found, you'll get full posting and voting access</li>
|
||||||
<li>• We'll check your wallet for Bitcoin Ordinal ownership</li>
|
<li>• If not found, you'll have read-only access</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Bitcoin, Coins, Loader2 } from 'lucide-react';
|
import { Wallet, Loader2, CheckCircle } from 'lucide-react';
|
||||||
import {
|
import { useAuth } from '@opchan/react';
|
||||||
useAppKit,
|
import { useEffect } from 'react';
|
||||||
useAppKitAccount,
|
|
||||||
useAppKitState,
|
|
||||||
} from '@reown/appkit/react';
|
|
||||||
|
|
||||||
interface WalletConnectionStepProps {
|
interface WalletConnectionStepProps {
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
@ -18,92 +15,41 @@ export function WalletConnectionStep({
|
|||||||
isLoading,
|
isLoading,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
}: WalletConnectionStepProps) {
|
}: WalletConnectionStepProps) {
|
||||||
const { initialized } = useAppKitState();
|
const { isAuthenticated, currentUser, connect } = useAuth();
|
||||||
const appKit = useAppKit();
|
|
||||||
|
|
||||||
// Get account info for different chains
|
// Auto-complete step when wallet connects
|
||||||
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
useEffect(() => {
|
||||||
const ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
|
if (isAuthenticated && currentUser?.address) {
|
||||||
|
onComplete();
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
}, [isAuthenticated, currentUser, onComplete]);
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await appKit.open({
|
connect();
|
||||||
view: 'Connect',
|
|
||||||
namespace: 'bip122',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting Bitcoin wallet:', error);
|
console.error('Error connecting wallet:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
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 = () => {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show connected state
|
// Show connected state
|
||||||
if (isConnected) {
|
if (isAuthenticated && currentUser) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 space-y-4">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
<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="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">
|
<span className="text-green-400 font-medium">
|
||||||
Wallet Connected
|
Wallet Connected
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
<p className="text-sm text-neutral-300 mb-2">
|
||||||
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...
|
Connected with {currentUser.address.slice(0, 6)}...
|
||||||
{activeAddress?.slice(-4)}
|
{currentUser.address.slice(-4)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -111,7 +57,7 @@ export function WalletConnectionStep({
|
|||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNext}
|
onClick={onComplete}
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
@ -122,78 +68,32 @@ export function WalletConnectionStep({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show connection options
|
// Show connection option
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 space-y-4">
|
<div className="flex-1 space-y-4">
|
||||||
<p className="text-sm text-neutral-400 text-center">
|
<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>
|
</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 */}
|
{/* Ethereum Section */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<Coins className="h-5 w-5 text-blue-500" />
|
<Wallet className="h-5 w-5 text-blue-500" />
|
||||||
<h3 className="font-semibold text-white">Ethereum</h3>
|
<h3 className="font-semibold text-white">Ethereum Wallet</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
ENS Ownership Required
|
ENS Optional
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleEthereumConnect}
|
onClick={handleConnect}
|
||||||
disabled={isLoading}
|
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"
|
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
|
||||||
style={{
|
style={{
|
||||||
height: '44px',
|
height: '48px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
fontSize: '14px',
|
fontSize: '16px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -207,13 +107,16 @@ export function WalletConnectionStep({
|
|||||||
Connecting...
|
Connecting...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Connect Ethereum Wallet'
|
<>
|
||||||
|
<Wallet className="h-5 w-5" />
|
||||||
|
Connect Wallet
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-neutral-500 text-center pt-2">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Bitcoin, Coins } from 'lucide-react';
|
import { Coins } from 'lucide-react';
|
||||||
import { useAuth, useAppKitWallet } from '@opchan/react';
|
import { useAuth } from '@opchan/react';
|
||||||
|
|
||||||
interface WalletDialogProps {
|
interface WalletDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -20,52 +20,20 @@ export function WalletConnectionDialog({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: WalletDialogProps) {
|
}: WalletDialogProps) {
|
||||||
const { connect, disconnect } = useAuth();
|
const { connect, disconnect, currentUser } = useAuth();
|
||||||
const wallet = useAppKitWallet();
|
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
const handleDisconnect = async () => {
|
||||||
await disconnect();
|
await disconnect();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBitcoinConnect = () => {
|
const handleConnect = () => {
|
||||||
if (!wallet.isInitialized) {
|
connect();
|
||||||
console.error('Wallet not initialized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connect('bitcoin');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEthereumConnect = () => {
|
const isConnected = Boolean(currentUser);
|
||||||
if (!wallet.isInitialized) {
|
const activeChain = 'Ethereum';
|
||||||
console.error('Wallet not initialized');
|
const activeAddress = currentUser?.address;
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@ -82,58 +50,17 @@ export function WalletConnectionDialog({
|
|||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{!isConnected ? (
|
{!isConnected ? (
|
||||||
<div className="space-y-4">
|
<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="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Coins className="h-5 w-5 text-blue-500" />
|
<Coins className="h-5 w-5 text-blue-500" />
|
||||||
<h3 className="font-semibold text-white">Ethereum</h3>
|
<h3 className="font-semibold text-white">Ethereum</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
ENS Ownership Required
|
ENS Ownership Recommended
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Button
|
<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"
|
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
|
||||||
style={{
|
style={{
|
||||||
height: '44px',
|
height: '44px',
|
||||||
@ -147,7 +74,7 @@ export function WalletConnectionDialog({
|
|||||||
gap: '8px',
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Connect Ethereum Wallet
|
Connect Wallet
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -153,7 +153,7 @@ const FeedPage: React.FC = () => {
|
|||||||
Be the first to create a post in a cell!
|
Be the first to create a post in a cell!
|
||||||
</p>
|
</p>
|
||||||
{verificationStatus !==
|
{verificationStatus !==
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED && (
|
EVerificationStatus.ENS_VERIFIED && (
|
||||||
<p className="text-sm text-cyber-neutral/80">
|
<p className="text-sm text-cyber-neutral/80">
|
||||||
Connect your wallet to start posting
|
Connect your wallet to start posting
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export default function ProfilePage() {
|
|||||||
const getVerificationIcon = () => {
|
const getVerificationIcon = () => {
|
||||||
// Use verification level from UserIdentityService (central database store)
|
// Use verification level from UserIdentityService (central database store)
|
||||||
switch (currentUser.verificationStatus) {
|
switch (currentUser.verificationStatus) {
|
||||||
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
|
case EVerificationStatus.ENS_VERIFIED:
|
||||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
case EVerificationStatus.WALLET_CONNECTED:
|
case EVerificationStatus.WALLET_CONNECTED:
|
||||||
return <Shield className="h-4 w-4 text-blue-500" />;
|
return <Shield className="h-4 w-4 text-blue-500" />;
|
||||||
@ -176,7 +176,7 @@ export default function ProfilePage() {
|
|||||||
const getVerificationText = () => {
|
const getVerificationText = () => {
|
||||||
// Use verification level from UserIdentityService (central database store)
|
// Use verification level from UserIdentityService (central database store)
|
||||||
switch (currentUser.verificationStatus) {
|
switch (currentUser.verificationStatus) {
|
||||||
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
|
case EVerificationStatus.ENS_VERIFIED:
|
||||||
return 'Owns ENS or Ordinal';
|
return 'Owns ENS or Ordinal';
|
||||||
case EVerificationStatus.WALLET_CONNECTED:
|
case EVerificationStatus.WALLET_CONNECTED:
|
||||||
return 'Connected Wallet';
|
return 'Connected Wallet';
|
||||||
@ -190,7 +190,7 @@ export default function ProfilePage() {
|
|||||||
const getVerificationColor = () => {
|
const getVerificationColor = () => {
|
||||||
// Use verification level from UserIdentityService (central database store)
|
// Use verification level from UserIdentityService (central database store)
|
||||||
switch (currentUser.verificationStatus) {
|
switch (currentUser.verificationStatus) {
|
||||||
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
|
case EVerificationStatus.ENS_VERIFIED:
|
||||||
return 'bg-green-100 text-green-800 border-green-200';
|
return 'bg-green-100 text-green-800 border-green-200';
|
||||||
case EVerificationStatus.WALLET_CONNECTED:
|
case EVerificationStatus.WALLET_CONNECTED:
|
||||||
return 'bg-blue-100 text-blue-800 border-blue-200';
|
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": {
|
"dependencies": {
|
||||||
"@noble/ed25519": "^2.2.3",
|
"@noble/ed25519": "^2.2.3",
|
||||||
"@noble/hashes": "^1.8.0",
|
"@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",
|
"@waku/sdk": "0.0.36-ff9c430.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"ordiscan": "^1.3.0",
|
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"wagmi": "^2.17.0"
|
"wagmi": "^2.17.0"
|
||||||
|
|||||||
@ -5,12 +5,10 @@ import { ForumActions } from '../lib/forum/ForumActions';
|
|||||||
import { RelevanceCalculator } from '../lib/forum/RelevanceCalculator';
|
import { RelevanceCalculator } from '../lib/forum/RelevanceCalculator';
|
||||||
import { UserIdentityService } from '../lib/services/UserIdentityService';
|
import { UserIdentityService } from '../lib/services/UserIdentityService';
|
||||||
import { DelegationManager, delegationManager } from '../lib/delegation';
|
import { DelegationManager, delegationManager } from '../lib/delegation';
|
||||||
import WalletManager from '../lib/wallet';
|
|
||||||
import { MessageService } from '../lib/services/MessageService';
|
import { MessageService } from '../lib/services/MessageService';
|
||||||
import { WakuConfig } from '../types';
|
import { WakuConfig } from '../types';
|
||||||
|
|
||||||
export interface OpChanClientConfig {
|
export interface OpChanClientConfig {
|
||||||
ordiscanApiKey: string;
|
|
||||||
wakuConfig: WakuConfig;
|
wakuConfig: WakuConfig;
|
||||||
reownProjectId?: string;
|
reownProjectId?: string;
|
||||||
}
|
}
|
||||||
@ -25,15 +23,11 @@ export class OpChanClient {
|
|||||||
readonly messageService: MessageService;
|
readonly messageService: MessageService;
|
||||||
readonly userIdentityService: UserIdentityService;
|
readonly userIdentityService: UserIdentityService;
|
||||||
readonly delegation: DelegationManager = delegationManager;
|
readonly delegation: DelegationManager = delegationManager;
|
||||||
readonly wallet = WalletManager
|
|
||||||
|
|
||||||
constructor(config: OpChanClientConfig) {
|
constructor(config: OpChanClientConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
const env: EnvironmentConfig = {
|
const env: EnvironmentConfig = {
|
||||||
apiKeys: {
|
|
||||||
ordiscan: config.ordiscanApiKey,
|
|
||||||
},
|
|
||||||
reownProjectId: config.reownProjectId,
|
reownProjectId: config.reownProjectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,6 @@ export * from './lib/forum/transformers';
|
|||||||
export { BookmarkService } from './lib/services/BookmarkService';
|
export { BookmarkService } from './lib/services/BookmarkService';
|
||||||
export { MessageService } from './lib/services/MessageService';
|
export { MessageService } from './lib/services/MessageService';
|
||||||
export { UserIdentityService, type UserIdentity } from './lib/services/UserIdentityService';
|
export { UserIdentityService, type UserIdentity } from './lib/services/UserIdentityService';
|
||||||
export { ordinals } from './lib/services/Ordinals';
|
|
||||||
|
|
||||||
// Export utilities
|
// Export utilities
|
||||||
export * from './lib/utils';
|
export * from './lib/utils';
|
||||||
@ -42,10 +41,9 @@ export { default as messageManager } from './lib/waku';
|
|||||||
export * from './lib/waku/network';
|
export * from './lib/waku/network';
|
||||||
|
|
||||||
// Export wallet functionality
|
// Export wallet functionality
|
||||||
export { WalletManager } from './lib/wallet';
|
export { EthereumWallet, EthereumWalletHelpers } from './lib/wallet';
|
||||||
export * from './lib/wallet/config';
|
export { wagmiConfig, config } from './lib/wallet/config';
|
||||||
export * from './lib/wallet/types';
|
export * from './lib/wallet/types';
|
||||||
export { type WalletAdapter, setWalletAdapter, getWalletAdapter } from './lib/wallet/adapter';
|
|
||||||
|
|
||||||
// Primary client API
|
// Primary client API
|
||||||
export { OpChanClient, type OpChanClientConfig } from './client/OpChanClient';
|
export { OpChanClient, type OpChanClientConfig } from './client/OpChanClient';
|
||||||
|
|||||||
@ -241,7 +241,7 @@ export class LocalDatabase {
|
|||||||
if (!existing || timestamp > existing.lastUpdated) {
|
if (!existing || timestamp > existing.lastUpdated) {
|
||||||
const nextRecord = {
|
const nextRecord = {
|
||||||
ensName: existing?.ensName,
|
ensName: existing?.ensName,
|
||||||
ordinalDetails: existing?.ordinalDetails,
|
ensAvatar: existing?.ensAvatar,
|
||||||
callSign: callSign !== undefined ? callSign : existing?.callSign,
|
callSign: callSign !== undefined ? callSign : existing?.callSign,
|
||||||
displayPreference,
|
displayPreference,
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
@ -663,11 +663,12 @@ export class LocalDatabase {
|
|||||||
this.cache.userIdentities[address] ||
|
this.cache.userIdentities[address] ||
|
||||||
{
|
{
|
||||||
ensName: undefined,
|
ensName: undefined,
|
||||||
ordinalDetails: undefined,
|
ensAvatar: undefined,
|
||||||
callSign: undefined,
|
callSign: undefined,
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
|
displayName: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const merged: UserIdentityCache[string] = {
|
const merged: UserIdentityCache[string] = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as ed from '@noble/ed25519';
|
import * as ed from '@noble/ed25519';
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
import { bytesToHex, hexToBytes } from '../utils';
|
import { bytesToHex, hexToBytes } from '../utils';
|
||||||
import { WalletManager } from '../wallet';
|
import { EthereumWalletHelpers } from '../wallet/EthereumWallet';
|
||||||
|
|
||||||
// Set up ed25519 with sha512
|
// Set up ed25519 with sha512
|
||||||
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
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 authMessage - The message that was signed
|
||||||
* @param walletSignature - The signature to verify
|
* @param walletSignature - The signature to verify
|
||||||
* @param walletAddress - The wallet address that signed
|
* @param walletAddress - The wallet address that signed
|
||||||
* @param walletType - The type of wallet
|
|
||||||
* @returns Promise<boolean> - True if signature is valid
|
* @returns Promise<boolean> - True if signature is valid
|
||||||
*/
|
*/
|
||||||
static async verifyWalletSignature(
|
static async verifyWalletSignature(
|
||||||
authMessage: string,
|
authMessage: string,
|
||||||
walletSignature: string,
|
walletSignature: string,
|
||||||
walletAddress: string,
|
walletAddress: `0x${string}`
|
||||||
walletType: 'bitcoin' | 'ethereum'
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await WalletManager.verifySignature(
|
return await EthereumWalletHelpers.verifySignature(
|
||||||
authMessage,
|
authMessage,
|
||||||
walletSignature,
|
walletSignature as `0x${string}`,
|
||||||
walletAddress,
|
walletAddress
|
||||||
walletType
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error verifying wallet signature:', error);
|
console.error('Error verifying wallet signature:', error);
|
||||||
|
|||||||
@ -11,8 +11,7 @@ import { DelegationCrypto } from './crypto';
|
|||||||
|
|
||||||
export interface DelegationFullStatus extends DelegationStatus {
|
export interface DelegationFullStatus extends DelegationStatus {
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
address?: string;
|
address?: `0x${string}`;
|
||||||
walletType?: 'bitcoin' | 'ethereum';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DelegationManager {
|
export class DelegationManager {
|
||||||
@ -36,8 +35,7 @@ export class DelegationManager {
|
|||||||
* Create a delegation with cryptographic proof
|
* Create a delegation with cryptographic proof
|
||||||
*/
|
*/
|
||||||
async delegate(
|
async delegate(
|
||||||
address: string,
|
address: `0x${string}`,
|
||||||
walletType: 'bitcoin' | 'ethereum',
|
|
||||||
duration: DelegationDuration = '7days',
|
duration: DelegationDuration = '7days',
|
||||||
signFunction: (message: string) => Promise<string>
|
signFunction: (message: string) => Promise<string>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -66,7 +64,6 @@ export class DelegationManager {
|
|||||||
walletSignature,
|
walletSignature,
|
||||||
expiryTimestamp,
|
expiryTimestamp,
|
||||||
walletAddress: address,
|
walletAddress: address,
|
||||||
walletType,
|
|
||||||
browserPublicKey: keypair.publicKey,
|
browserPublicKey: keypair.publicKey,
|
||||||
browserPrivateKey: keypair.privateKey,
|
browserPrivateKey: keypair.privateKey,
|
||||||
nonce,
|
nonce,
|
||||||
@ -210,8 +207,7 @@ export class DelegationManager {
|
|||||||
* Get delegation status
|
* Get delegation status
|
||||||
*/
|
*/
|
||||||
async getStatus(
|
async getStatus(
|
||||||
currentAddress?: string,
|
currentAddress?: `0x${string}`
|
||||||
currentWalletType?: 'bitcoin' | 'ethereum'
|
|
||||||
): Promise<DelegationFullStatus> {
|
): Promise<DelegationFullStatus> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (
|
if (
|
||||||
@ -229,9 +225,7 @@ export class DelegationManager {
|
|||||||
const hasExpired = now >= delegation.expiryTimestamp;
|
const hasExpired = now >= delegation.expiryTimestamp;
|
||||||
const addressMatches =
|
const addressMatches =
|
||||||
!currentAddress || delegation.walletAddress === currentAddress;
|
!currentAddress || delegation.walletAddress === currentAddress;
|
||||||
const walletTypeMatches =
|
const isValid = !hasExpired && addressMatches;
|
||||||
!currentWalletType || delegation.walletType === currentWalletType;
|
|
||||||
const isValid = !hasExpired && addressMatches && walletTypeMatches;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasDelegation: true,
|
hasDelegation: true,
|
||||||
@ -241,7 +235,6 @@ export class DelegationManager {
|
|||||||
: undefined,
|
: undefined,
|
||||||
publicKey: delegation.browserPublicKey,
|
publicKey: delegation.browserPublicKey,
|
||||||
address: delegation.walletAddress,
|
address: delegation.walletAddress,
|
||||||
walletType: delegation.walletType,
|
|
||||||
proof: isValid ? this.createProof(delegation) : undefined,
|
proof: isValid ? this.createProof(delegation) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -269,7 +262,6 @@ export class DelegationManager {
|
|||||||
walletSignature: delegation.walletSignature,
|
walletSignature: delegation.walletSignature,
|
||||||
expiryTimestamp: delegation.expiryTimestamp,
|
expiryTimestamp: delegation.expiryTimestamp,
|
||||||
walletAddress: delegation.walletAddress,
|
walletAddress: delegation.walletAddress,
|
||||||
walletType: delegation.walletType,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,8 +297,7 @@ export class DelegationManager {
|
|||||||
return await DelegationCrypto.verifyWalletSignature(
|
return await DelegationCrypto.verifyWalletSignature(
|
||||||
proof.authMessage,
|
proof.authMessage,
|
||||||
proof.walletSignature,
|
proof.walletSignature,
|
||||||
proof.walletAddress,
|
proof.walletAddress
|
||||||
proof.walletType
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,8 +337,7 @@ export class DelegationManager {
|
|||||||
const walletSigOk = await DelegationCrypto.verifyWalletSignature(
|
const walletSigOk = await DelegationCrypto.verifyWalletSignature(
|
||||||
proof.authMessage,
|
proof.authMessage,
|
||||||
proof.walletSignature,
|
proof.walletSignature,
|
||||||
proof.walletAddress,
|
proof.walletAddress
|
||||||
proof.walletType
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!walletSigOk) {
|
if (!walletSigOk) {
|
||||||
|
|||||||
@ -7,8 +7,7 @@ export interface DelegationProof {
|
|||||||
authMessage: string; // "I authorize browser key: 0xabc... until 1640995200"
|
authMessage: string; // "I authorize browser key: 0xabc... until 1640995200"
|
||||||
walletSignature: string; // Wallet's signature of authMessage
|
walletSignature: string; // Wallet's signature of authMessage
|
||||||
expiryTimestamp: number; // When this delegation expires
|
expiryTimestamp: number; // When this delegation expires
|
||||||
walletAddress: string; // Wallet address that signed the delegation
|
walletAddress: `0x${string}`; // Ethereum address that signed the delegation
|
||||||
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -44,10 +44,10 @@ export class ForumActions {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'createCell':
|
case 'createCell':
|
||||||
if (verificationStatus !== EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
if (verificationStatus !== EVerificationStatus.ENS_VERIFIED) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'Only ENS or Logos ordinal owners can create cells',
|
error: 'Only ENS owners can create cells',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -104,8 +104,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedPost);
|
const signed = await this.delegationManager.signMessage(unsignedPost);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -173,8 +172,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedComment);
|
const signed = await this.delegationManager.signMessage(unsignedComment);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -245,8 +243,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedCell);
|
const signed = await this.delegationManager.signMessage(unsignedCell);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -317,8 +314,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedVote);
|
const signed = await this.delegationManager.signMessage(unsignedVote);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -389,8 +385,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -470,8 +465,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -550,8 +544,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -618,8 +611,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -699,8 +691,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -779,8 +770,7 @@ export class ForumActions {
|
|||||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||||
if (!signed) {
|
if (!signed) {
|
||||||
const status = await this.delegationManager.getStatus(
|
const status = await this.delegationManager.getStatus(
|
||||||
currentUser!.address,
|
currentUser!.address
|
||||||
currentUser!.walletType
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@ -20,8 +20,8 @@ export class RelevanceCalculator {
|
|||||||
COMMENT: 0.5,
|
COMMENT: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly VERIFICATION_BONUS = 1.25; // 25% increase for ENS/Ordinal owners
|
private static readonly VERIFICATION_BONUS = 1.25; // 25% increase for ENS verified users
|
||||||
private static readonly BASIC_VERIFICATION_BONUS = 1.1; // 10% increase for basic 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_UPVOTE_BONUS = 0.1;
|
||||||
private static readonly VERIFIED_COMMENTER_BONUS = 0.05;
|
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 {
|
isUserVerified(user: User): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
user.ensDetails ||
|
user.ensName ||
|
||||||
user.ordinalDetails ||
|
|
||||||
user.verificationStatus === EVerificationStatus.WALLET_CONNECTED
|
user.verificationStatus === EVerificationStatus.WALLET_CONNECTED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -238,9 +237,8 @@ export class RelevanceCalculator {
|
|||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
status[user.address] = {
|
status[user.address] = {
|
||||||
isVerified: this.isUserVerified(user),
|
isVerified: this.isUserVerified(user),
|
||||||
hasENS: !!user.ensDetails,
|
hasENS: !!user.ensName,
|
||||||
hasOrdinal: !!user.ordinalDetails,
|
ensName: user.ensName,
|
||||||
ensName: user.ensDetails?.ensName,
|
|
||||||
verificationStatus: user.verificationStatus,
|
verificationStatus: user.verificationStatus,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -285,9 +283,9 @@ export class RelevanceCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply different bonuses based on verification signals from UserVerificationStatus
|
// 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;
|
let bonus = 0;
|
||||||
if (authorStatus?.hasENS || authorStatus?.hasOrdinal) {
|
if (authorStatus?.hasENS) {
|
||||||
bonus = score * (RelevanceCalculator.VERIFICATION_BONUS - 1);
|
bonus = score * (RelevanceCalculator.VERIFICATION_BONUS - 1);
|
||||||
} else if (authorStatus?.isVerified) {
|
} else if (authorStatus?.isVerified) {
|
||||||
bonus = score * (RelevanceCalculator.BASIC_VERIFICATION_BONUS - 1);
|
bonus = score * (RelevanceCalculator.BASIC_VERIFICATION_BONUS - 1);
|
||||||
|
|||||||
@ -93,7 +93,7 @@ describe('RelevanceCalculator', () => {
|
|||||||
const verifiedUser: User = {
|
const verifiedUser: User = {
|
||||||
address: 'user1',
|
address: 'user1',
|
||||||
walletType: 'ethereum',
|
walletType: 'ethereum',
|
||||||
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
verificationStatus: EVerificationStatus.ENS_VERIFIED,
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
ensDetails: {
|
ensDetails: {
|
||||||
ensName: 'test.eth',
|
ensName: 'test.eth',
|
||||||
@ -110,7 +110,7 @@ describe('RelevanceCalculator', () => {
|
|||||||
const verifiedUser: User = {
|
const verifiedUser: User = {
|
||||||
address: 'user3',
|
address: 'user3',
|
||||||
walletType: 'bitcoin',
|
walletType: 'bitcoin',
|
||||||
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
verificationStatus: EVerificationStatus.ENS_VERIFIED,
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
ordinalDetails: {
|
ordinalDetails: {
|
||||||
ordinalId: '1',
|
ordinalId: '1',
|
||||||
@ -300,7 +300,7 @@ describe('RelevanceCalculator', () => {
|
|||||||
{
|
{
|
||||||
address: 'user1',
|
address: 'user1',
|
||||||
walletType: 'ethereum',
|
walletType: 'ethereum',
|
||||||
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
verificationStatus: EVerificationStatus.ENS_VERIFIED,
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
ensDetails: {
|
ensDetails: {
|
||||||
ensName: 'test.eth',
|
ensName: 'test.eth',
|
||||||
|
|||||||
@ -258,9 +258,8 @@ export const getDataFromCache = async (
|
|||||||
|
|
||||||
for (const [address, rec] of Object.entries(userIdentities)) {
|
for (const [address, rec] of Object.entries(userIdentities)) {
|
||||||
userVerificationStatus[address] = {
|
userVerificationStatus[address] = {
|
||||||
isVerified: rec.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
isVerified: rec.verificationStatus === EVerificationStatus.ENS_VERIFIED,
|
||||||
hasENS: Boolean(rec.ensName),
|
hasENS: Boolean(rec.ensName),
|
||||||
hasOrdinal: Boolean(rec.ordinalDetails),
|
|
||||||
ensName: rec.ensName,
|
ensName: rec.ensName,
|
||||||
verificationStatus: rec.verificationStatus,
|
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';
|
} from '../../types/waku';
|
||||||
import { MessageService } from './MessageService';
|
import { MessageService } from './MessageService';
|
||||||
import { localDatabase } from '../database/LocalDatabase';
|
import { localDatabase } from '../database/LocalDatabase';
|
||||||
import { WalletManager } from '../wallet';
|
import { EthereumWalletHelpers } from '../wallet/EthereumWallet';
|
||||||
import { getWalletAdapter } from '../wallet/adapter';
|
import type { PublicClient } from 'viem';
|
||||||
|
|
||||||
export interface UserIdentity {
|
export interface UserIdentity {
|
||||||
address: string;
|
address: `0x${string}`;
|
||||||
ensName?: string;
|
ensName?: string;
|
||||||
ordinalDetails?: {
|
ensAvatar?: string;
|
||||||
ordinalId: string;
|
|
||||||
ordinalDetails: string;
|
|
||||||
};
|
|
||||||
callSign?: string;
|
callSign?: string;
|
||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -26,11 +23,20 @@ export interface UserIdentity {
|
|||||||
|
|
||||||
export class UserIdentityService {
|
export class UserIdentityService {
|
||||||
private messageService: MessageService;
|
private messageService: MessageService;
|
||||||
|
private publicClient: PublicClient | null = null;
|
||||||
private refreshListeners: Set<(address: string) => void> = new Set();
|
private refreshListeners: Set<(address: string) => void> = new Set();
|
||||||
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
constructor(messageService: MessageService) {
|
constructor(messageService: MessageService, publicClient?: PublicClient) {
|
||||||
this.messageService = messageService;
|
this.messageService = messageService;
|
||||||
|
this.publicClient = publicClient || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the public client for ENS resolution
|
||||||
|
*/
|
||||||
|
setPublicClient(publicClient: PublicClient): void {
|
||||||
|
this.publicClient = publicClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PUBLIC METHODS =====
|
// ===== PUBLIC METHODS =====
|
||||||
@ -81,7 +87,7 @@ export class UserIdentityService {
|
|||||||
*/
|
*/
|
||||||
getAll(): UserIdentity[] {
|
getAll(): UserIdentity[] {
|
||||||
return Object.entries(localDatabase.cache.userIdentities).map(([address, cached]) => ({
|
return Object.entries(localDatabase.cache.userIdentities).map(([address, cached]) => ({
|
||||||
address,
|
address: address as `0x${string}`,
|
||||||
...cached,
|
...cached,
|
||||||
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
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(
|
private async resolveUserIdentity(
|
||||||
address: string
|
address: string
|
||||||
): Promise<UserIdentity | null> {
|
): Promise<UserIdentity | null> {
|
||||||
try {
|
try {
|
||||||
console.log('resolveUserIdentity', address);
|
console.log('resolveUserIdentity', address);
|
||||||
const [ensName, ordinalDetails] = await Promise.all([
|
|
||||||
this.resolveENSName(address),
|
|
||||||
this.resolveOrdinalDetails(address),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isWalletConnected = (getWalletAdapter()?.isConnected?.() ?? (WalletManager.hasInstance()
|
// Only resolve ENS for Ethereum addresses
|
||||||
? WalletManager.getInstance().isConnected()
|
if (!address.startsWith('0x')) {
|
||||||
: false));
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensData = await this.resolveENSName(address as `0x${string}`);
|
||||||
|
|
||||||
let verificationStatus: EVerificationStatus;
|
let verificationStatus: EVerificationStatus;
|
||||||
if (ensName || ordinalDetails) {
|
if (ensData?.name) {
|
||||||
verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
verificationStatus = EVerificationStatus.ENS_VERIFIED;
|
||||||
} else {
|
} else {
|
||||||
verificationStatus = isWalletConnected ? EVerificationStatus.WALLET_CONNECTED : EVerificationStatus.WALLET_UNCONNECTED;
|
verificationStatus = EVerificationStatus.WALLET_CONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayPreference = localDatabase.cache.userIdentities[address]?.displayPreference ?? EDisplayPreference.WALLET_ADDRESS;
|
const displayPreference = localDatabase.cache.userIdentities[address]?.displayPreference ?? EDisplayPreference.WALLET_ADDRESS;
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address,
|
address: address as `0x${string}`,
|
||||||
ensName: ensName || undefined,
|
ensName: ensData?.name || undefined,
|
||||||
ordinalDetails: ordinalDetails || undefined,
|
ensAvatar: ensData?.avatar || undefined,
|
||||||
callSign: undefined, // Will be populated from Waku messages
|
callSign: undefined, // Will be populated from Waku messages
|
||||||
displayPreference: displayPreference,
|
displayPreference: displayPreference,
|
||||||
displayName: this.getDisplayName({address, ensName, displayPreference}),
|
displayName: this.getDisplayName({address, ensName: ensData?.name, displayPreference}),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
verificationStatus,
|
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> {
|
private async resolveENSName(address: `0x${string}`): Promise<{ name: string | null; avatar: string | null }> {
|
||||||
if (!address.startsWith('0x')) {
|
|
||||||
return null; // Not an Ethereum address
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer previously persisted ENS if recent
|
// Prefer previously persisted ENS if recent
|
||||||
const cached = localDatabase.cache.userIdentities[address];
|
const cached = localDatabase.cache.userIdentities[address];
|
||||||
if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) {
|
if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) {
|
||||||
return cached.ensName;
|
return { name: cached.ensName, avatar: cached.ensAvatar || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.doResolveENSName(address);
|
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
|
* Notify all listeners that user identity data has changed
|
||||||
*/
|
*/
|
||||||
@ -326,9 +300,9 @@ export class UserIdentityService {
|
|||||||
record: UserIdentityCache[string]
|
record: UserIdentityCache[string]
|
||||||
): UserIdentity {
|
): UserIdentity {
|
||||||
return {
|
return {
|
||||||
address,
|
address: address as `0x${string}`,
|
||||||
ensName: record.ensName,
|
ensName: record.ensName,
|
||||||
ordinalDetails: record.ordinalDetails,
|
ensAvatar: record.ensAvatar,
|
||||||
callSign: record.callSign,
|
callSign: record.callSign,
|
||||||
displayPreference: record.displayPreference,
|
displayPreference: record.displayPreference,
|
||||||
displayName: this.getDisplayName({address, ensName: record.ensName, displayPreference: record.displayPreference}),
|
displayName: this.getDisplayName({address, ensName: record.ensName, displayPreference: record.displayPreference}),
|
||||||
@ -345,18 +319,20 @@ export class UserIdentityService {
|
|||||||
identity: UserIdentity
|
identity: UserIdentity
|
||||||
): Promise<UserIdentity> {
|
): Promise<UserIdentity> {
|
||||||
if (!identity.ensName && address.startsWith('0x')) {
|
if (!identity.ensName && address.startsWith('0x')) {
|
||||||
const ensName = await this.resolveENSName(address);
|
const ensData = await this.resolveENSName(address as `0x${string}`);
|
||||||
if (ensName) {
|
if (ensData.name) {
|
||||||
const updated: UserIdentity = {
|
const updated: UserIdentity = {
|
||||||
...identity,
|
...identity,
|
||||||
ensName,
|
ensName: ensData.name,
|
||||||
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
ensAvatar: ensData.avatar || undefined,
|
||||||
|
verificationStatus: EVerificationStatus.ENS_VERIFIED,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await localDatabase.upsertUserIdentity(address, {
|
await localDatabase.upsertUserIdentity(address, {
|
||||||
ensName,
|
ensName: ensData.name,
|
||||||
verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
ensAvatar: ensData.avatar || undefined,
|
||||||
|
verificationStatus: EVerificationStatus.ENS_VERIFIED,
|
||||||
lastUpdated: updated.lastUpdated,
|
lastUpdated: updated.lastUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -366,14 +342,17 @@ export class UserIdentityService {
|
|||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doResolveENSName(address: string): Promise<string | null> {
|
private async doResolveENSName(address: `0x${string}`): Promise<{ name: string | null; avatar: string | null }> {
|
||||||
try {
|
try {
|
||||||
// Resolve ENS via centralized WalletManager helper
|
if (!this.publicClient) {
|
||||||
const ensName = await WalletManager.resolveENS(address);
|
console.warn('No publicClient available for ENS resolution');
|
||||||
return ensName || null;
|
return { name: null, avatar: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await EthereumWalletHelpers.resolveENS(this.publicClient, address);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resolve ENS name:', 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':
|
case 'verified-basic':
|
||||||
return EVerificationStatus.WALLET_CONNECTED;
|
return EVerificationStatus.WALLET_CONNECTED;
|
||||||
case 'verified-owner':
|
case 'verified-owner':
|
||||||
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
case 'ens-ordinal-verified': // Legacy value
|
||||||
|
return EVerificationStatus.ENS_VERIFIED;
|
||||||
case 'verifying':
|
case 'verifying':
|
||||||
return EVerificationStatus.WALLET_CONNECTED; // Temporary state during verification
|
return EVerificationStatus.WALLET_CONNECTED; // Temporary state during verification
|
||||||
|
|
||||||
@ -395,8 +375,8 @@ export class UserIdentityService {
|
|||||||
return EVerificationStatus.WALLET_UNCONNECTED;
|
return EVerificationStatus.WALLET_UNCONNECTED;
|
||||||
case EVerificationStatus.WALLET_CONNECTED:
|
case EVerificationStatus.WALLET_CONNECTED:
|
||||||
return EVerificationStatus.WALLET_CONNECTED;
|
return EVerificationStatus.WALLET_CONNECTED;
|
||||||
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
|
case EVerificationStatus.ENS_VERIFIED:
|
||||||
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
return EVerificationStatus.ENS_VERIFIED;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return EVerificationStatus.WALLET_UNCONNECTED;
|
return EVerificationStatus.WALLET_UNCONNECTED;
|
||||||
|
|||||||
@ -4,24 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface EnvironmentConfig {
|
export interface EnvironmentConfig {
|
||||||
apiKeys?: {
|
|
||||||
ordiscan?: string;
|
|
||||||
};
|
|
||||||
reownProjectId?: string;
|
reownProjectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Environment {
|
class Environment {
|
||||||
private config: EnvironmentConfig = {
|
private config: EnvironmentConfig = {};
|
||||||
};
|
|
||||||
|
|
||||||
public configure(config: EnvironmentConfig): void {
|
public configure(config: EnvironmentConfig): void {
|
||||||
this.config = { ...this.config, ...config };
|
this.config = { ...this.config, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
public get ordiscanApiKey(): string | undefined {
|
|
||||||
return this.config.apiKeys?.ordiscan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get reownProjectId(): string | undefined {
|
public get reownProjectId(): string | undefined {
|
||||||
return this.config.reownProjectId;
|
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 { createConfig, http } from 'wagmi';
|
||||||
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin';
|
import { mainnet } from 'wagmi/chains';
|
||||||
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
|
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
|
||||||
import { createStorage } from 'wagmi';
|
|
||||||
import { mainnet, bitcoin, AppKitNetwork } from '@reown/appkit/networks';
|
|
||||||
import { environment } from '../utils/environment';
|
|
||||||
|
|
||||||
const networks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, bitcoin];
|
// Default WalletConnect project ID - users should override with their own
|
||||||
|
const DEFAULT_PROJECT_ID = '2ead96ea166a03e5ab50e5c190532e72';
|
||||||
|
|
||||||
const projectId =
|
export function createWagmiConfig(projectId?: string) {
|
||||||
environment.reownProjectId ||
|
const wcProjectId = projectId || DEFAULT_PROJECT_ID;
|
||||||
process.env.VITE_REOWN_SECRET ||
|
|
||||||
'2ead96ea166a03e5ab50e5c190532e72';
|
|
||||||
|
|
||||||
if (!projectId) {
|
return createConfig({
|
||||||
throw new Error(
|
chains: [mainnet],
|
||||||
'Reown project ID is not defined. Please set it via config.reownProjectId, VITE_REOWN_SECRET environment variable, or use the default.'
|
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({
|
// Default config export for convenience
|
||||||
storage: createStorage({ storage: localStorage }),
|
export const wagmiConfig = createWagmiConfig();
|
||||||
ssr: false, // Set to false for Vite/React apps
|
export const config = wagmiConfig; // Alias for backward compatibility
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,273 +1,13 @@
|
|||||||
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
/**
|
||||||
import { AppKit } from '@reown/appkit';
|
* Simplified Ethereum-only wallet module
|
||||||
import { ordinals } from '../services/Ordinals';
|
*
|
||||||
import {
|
* This module provides wallet functionality for Ethereum addresses only.
|
||||||
getEnsName,
|
* Bitcoin and Ordinals support has been removed.
|
||||||
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
|
|
||||||
*/
|
|
||||||
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 './types';
|
||||||
export * from './config';
|
export * from './config';
|
||||||
|
|
||||||
|
// Re-export wagmi/viem for convenience
|
||||||
|
export { type WalletClient, type PublicClient } from 'viem';
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
export interface WalletInfo {
|
export interface WalletInfo {
|
||||||
address: string;
|
address: `0x${string}`;
|
||||||
walletType: 'bitcoin' | 'ethereum';
|
|
||||||
ensName?: string;
|
ensName?: string;
|
||||||
isConnected: boolean;
|
ensAvatar?: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActiveWallet {
|
|
||||||
type: 'bitcoin' | 'ethereum';
|
|
||||||
address: string;
|
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,6 @@ export interface UserVerificationStatus {
|
|||||||
[address: string]: {
|
[address: string]: {
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
hasENS: boolean;
|
hasENS: boolean;
|
||||||
hasOrdinal: boolean;
|
|
||||||
ensName?: string;
|
ensName?: string;
|
||||||
verificationStatus?: EVerificationStatus;
|
verificationStatus?: EVerificationStatus;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
export type User = {
|
export type User = {
|
||||||
address: string;
|
address: `0x${string}`;
|
||||||
walletType: 'bitcoin' | 'ethereum';
|
|
||||||
|
|
||||||
ordinalDetails?: OrdinalDetails;
|
ensName?: string;
|
||||||
ensDetails?: EnsDetails;
|
ensAvatar?: string;
|
||||||
|
|
||||||
callSign?: string;
|
callSign?: string;
|
||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
@ -14,23 +13,14 @@ export type User = {
|
|||||||
signature?: string;
|
signature?: string;
|
||||||
lastChecked?: number;
|
lastChecked?: number;
|
||||||
browserPubKey?: string; // Browser-generated public key for key delegation
|
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
|
delegationExpiry?: number; // When the delegation expires
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum EVerificationStatus {
|
export enum EVerificationStatus {
|
||||||
WALLET_UNCONNECTED = 'wallet-unconnected',
|
WALLET_UNCONNECTED = 'wallet-unconnected',
|
||||||
WALLET_CONNECTED = 'wallet-connected',
|
WALLET_CONNECTED = 'wallet-connected',
|
||||||
ENS_ORDINAL_VERIFIED = 'ens-ordinal-verified',
|
ENS_VERIFIED = 'ens-verified',
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrdinalDetails {
|
|
||||||
ordinalId: string;
|
|
||||||
ordinalDetails: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnsDetails {
|
|
||||||
ensName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EDisplayPreference {
|
export enum EDisplayPreference {
|
||||||
|
|||||||
@ -169,10 +169,7 @@ export interface VoteCache {
|
|||||||
export interface UserIdentityCache {
|
export interface UserIdentityCache {
|
||||||
[address: string]: {
|
[address: string]: {
|
||||||
ensName?: string;
|
ensName?: string;
|
||||||
ordinalDetails?: {
|
ensAvatar?: string;
|
||||||
ordinalId: string;
|
|
||||||
ordinalDetails: string;
|
|
||||||
};
|
|
||||||
callSign?: string;
|
callSign?: string;
|
||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
|
|||||||
@ -10,15 +10,15 @@ npm i @opchan/react @opchan/core react react-dom
|
|||||||
|
|
||||||
### Quickstart
|
### 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
|
```tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { OpChanProvider } from '@opchan/react';
|
import { OpChanProvider } from '@opchan/react';
|
||||||
|
|
||||||
const opchanConfig = { ordiscanApiKey: 'YOUR_ORDISCAN_API_KEY' };
|
const opchanConfig = {};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -69,12 +69,9 @@ export function Connect() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => connect('ethereum')}>
|
<button onClick={() => connect()}>
|
||||||
Connect Ethereum
|
Connect Wallet
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => connect('bitcoin')}>
|
|
||||||
Connect Bitcoin
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -85,12 +82,11 @@ export function Connect() {
|
|||||||
### API
|
### API
|
||||||
|
|
||||||
- **Providers**
|
- **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:
|
- Props:
|
||||||
- `config: OpChanClientConfig` — core client configuration.
|
- `config: OpChanClientConfig` — core client configuration.
|
||||||
- `children: React.ReactNode`.
|
- `children: React.ReactNode`.
|
||||||
- Requirements: None — this provider already wraps `WagmiProvider` and `AppKitProvider` internally.
|
- Requirements: None — this provider already wraps `WagmiProvider` and `QueryClientProvider` internally.
|
||||||
- Internally provides `AppKitWalletProvider` for wallet state management.
|
|
||||||
|
|
||||||
- **`AppKitWalletProvider`**: Wallet context provider (automatically included in `OpChanProvider`).
|
- **`AppKitWalletProvider`**: Wallet context provider (automatically included in `OpChanProvider`).
|
||||||
- Provides wallet state and controls from AppKit.
|
- Provides wallet state and controls from AppKit.
|
||||||
@ -107,9 +103,7 @@ export function Connect() {
|
|||||||
`delegate(duration)`, `delegationStatus()`, `clearDelegation()`,
|
`delegate(duration)`, `delegationStatus()`, `clearDelegation()`,
|
||||||
`updateProfile({ callSign?, displayPreference? })`.
|
`updateProfile({ callSign?, displayPreference? })`.
|
||||||
|
|
||||||
- **`useAppKitWallet()`** → AppKit wallet state (low-level)
|
|
||||||
- Data: `address`, `walletType`, `isConnected`, `isInitialized`.
|
|
||||||
- Actions: `connect(walletType)`, `disconnect()`.
|
|
||||||
|
|
||||||
- **`useContent()`** → forum data & actions
|
- **`useContent()`** → forum data & actions
|
||||||
- Data: `cells`, `posts`, `comments`, `bookmarks`, `postsByCell`, `commentsByPost`,
|
- Data: `cells`, `posts`, `comments`, `bookmarks`, `postsByCell`, `commentsByPost`,
|
||||||
@ -135,7 +129,7 @@ export function Connect() {
|
|||||||
- Categories: `'wizardStates' | 'preferences' | 'temporaryStates'` (default `'preferences'`).
|
- Categories: `'wizardStates' | 'preferences' | 'temporaryStates'` (default `'preferences'`).
|
||||||
|
|
||||||
- **`useUserDisplay(address)`** → identity details for any address
|
- **`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.
|
- Backed by a centralized identity cache; updates propagate automatically.
|
||||||
|
|
||||||
- **`useClient()`** → access the underlying `OpChanClient` (advanced use only).
|
- **`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
|
if (!(window as any).Buffer) (window as any).Buffer = Buffer
|
||||||
```
|
```
|
||||||
- Config values you likely need to pass to `OpChanProvider`:
|
- Config values you likely need to pass to `OpChanProvider`:
|
||||||
- `ordiscanApiKey` (optional for dev)
|
|
||||||
- `wakuConfig` with `contentTopic` and `reliableChannelId`
|
- `wakuConfig` with `contentTopic` and `reliableChannelId`
|
||||||
- `reownProjectId` (e.g., from `import.meta.env.VITE_REOWN_SECRET`)
|
|
||||||
|
|
||||||
### License
|
### 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
|
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
|
```tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -20,14 +20,14 @@ import { OpChanProvider } from '@opchan/react';
|
|||||||
|
|
||||||
export function AppProviders({ children }: { children: React.ReactNode }) {
|
export function AppProviders({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<OpChanProvider config={{ ordiscanApiKey: 'YOUR_API_KEY' }}>
|
<OpChanProvider config={{}}>
|
||||||
{children}
|
{children}
|
||||||
</OpChanProvider>
|
</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>
|
<div>Connected: {currentUser.displayName}</div>
|
||||||
<button onClick={() => disconnect()}>Disconnect</button>
|
<button onClick={() => disconnect()}>Disconnect</button>
|
||||||
<button onClick={() => verifyOwnership()}>Verify ENS/Ordinal</button>
|
<button onClick={() => verifyOwnership()}>Verify ENS</button>
|
||||||
<div>Status: {verificationStatus}</div>
|
<div>Status: {verificationStatus}</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => connect('bitcoin')}>
|
<button onClick={() => connect()}>Connect Wallet</button>
|
||||||
Connect Bitcoin
|
|
||||||
</button>
|
|
||||||
<button onClick={() => connect('ethereum')}>
|
|
||||||
Connect Ethereum
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -85,8 +80,8 @@ function WalletControls() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
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.
|
- `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 or Bitcoin Ordinals).
|
- `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';
|
import { useUserDisplay } from '@opchan/react';
|
||||||
|
|
||||||
function AuthorName({ address }: { address: string }) {
|
function AuthorName({ address }: { address: string }) {
|
||||||
const { displayName, ensName, ordinalDetails, isLoading } = useUserDisplay(address);
|
const { displayName, ensName, isLoading } = useUserDisplay(address);
|
||||||
if (isLoading) return <span>Loading…</span>;
|
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() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<OpChanProvider config={{ ordiscanApiKey: '' }}>
|
<OpChanProvider config={{}}>
|
||||||
<Home />
|
<Home />
|
||||||
</OpChanProvider>
|
</OpChanProvider>
|
||||||
);
|
);
|
||||||
@ -381,6 +376,6 @@ export default function App() {
|
|||||||
- Always gate actions with `usePermissions()` to provide clear UX reasons.
|
- 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.
|
- 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.
|
- 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,
|
useClient,
|
||||||
} from './v1/context/ClientContext';
|
} from './v1/context/ClientContext';
|
||||||
|
|
||||||
export { useAppKitWallet } from './v1/hooks/useAppKitWallet';
|
export { useEthereumWallet } from './v1/hooks/useEthereumWallet';
|
||||||
|
|
||||||
export { OpChanProvider } from './v1/provider/OpChanProvider';
|
export { OpChanProvider } from './v1/provider/OpChanProvider';
|
||||||
export type { OpChanProviderProps } 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 React from 'react';
|
||||||
import { useClient } from '../context/ClientContext';
|
import { useClient } from '../context/ClientContext';
|
||||||
import { useAppKitWallet } from '../hooks/useAppKitWallet';
|
import { useEthereumWallet } from './useEthereumWallet';
|
||||||
import { useOpchanStore, setOpchanState } from '../store/opchanStore';
|
import { useOpchanStore, setOpchanState } from '../store/opchanStore';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
EVerificationStatus,
|
EVerificationStatus,
|
||||||
DelegationDuration,
|
DelegationDuration,
|
||||||
EDisplayPreference,
|
EDisplayPreference,
|
||||||
WalletManager,
|
|
||||||
} from '@opchan/core';
|
} from '@opchan/core';
|
||||||
import type { DelegationFullStatus } from '@opchan/core';
|
import type { DelegationFullStatus } from '@opchan/core';
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const wallet = useAppKitWallet();
|
const wallet = useEthereumWallet();
|
||||||
const currentUser = useOpchanStore(s => s.session.currentUser);
|
const currentUser = useOpchanStore(s => s.session.currentUser);
|
||||||
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
|
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
|
||||||
const delegation = useOpchanStore(s => s.session.delegation);
|
const delegation = useOpchanStore(s => s.session.delegation);
|
||||||
|
|
||||||
// Sync AppKit wallet state to OpChan session
|
// Sync Ethereum wallet state to OpChan session
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const syncWallet = async () => {
|
const syncWallet = async () => {
|
||||||
if (wallet.isConnected && wallet.address && wallet.walletType) {
|
if (wallet.isConnected && wallet.address) {
|
||||||
// Wallet connected - create/update user session
|
// Wallet connected - create/update user session
|
||||||
const baseUser: User = {
|
const baseUser: User = {
|
||||||
address: wallet.address,
|
address: wallet.address,
|
||||||
walletType: wallet.walletType,
|
|
||||||
displayName: wallet.address.slice(0, 6) + '...' + wallet.address.slice(-4),
|
displayName: wallet.address.slice(0, 6) + '...' + wallet.address.slice(-4),
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||||
@ -33,6 +31,11 @@ export function useAuth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Set public client for ENS resolution
|
||||||
|
if (wallet.publicClient) {
|
||||||
|
client.userIdentityService.setPublicClient(wallet.publicClient);
|
||||||
|
}
|
||||||
|
|
||||||
await client.database.storeUser(baseUser);
|
await client.database.storeUser(baseUser);
|
||||||
// Prime identity service so display name/ens are cached
|
// Prime identity service so display name/ens are cached
|
||||||
const identity = await client.userIdentityService.getIdentity(baseUser.address);
|
const identity = await client.userIdentityService.getIdentity(baseUser.address);
|
||||||
@ -80,10 +83,10 @@ export function useAuth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
syncWallet();
|
syncWallet();
|
||||||
}, [wallet.isConnected, wallet.address, wallet.walletType, client]);
|
}, [wallet.isConnected, wallet.address, wallet.publicClient, client, currentUser]);
|
||||||
|
|
||||||
const connect = React.useCallback((walletType: 'bitcoin' | 'ethereum'): void => {
|
const connect = React.useCallback((): void => {
|
||||||
wallet.connect(walletType);
|
wallet.connect();
|
||||||
}, [wallet]);
|
}, [wallet]);
|
||||||
|
|
||||||
const disconnect = React.useCallback(async (): Promise<void> => {
|
const disconnect = React.useCallback(async (): Promise<void> => {
|
||||||
@ -91,7 +94,7 @@ export function useAuth() {
|
|||||||
}, [wallet]);
|
}, [wallet]);
|
||||||
|
|
||||||
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
|
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
|
||||||
console.log('verifyOwnership')
|
console.log('verifyOwnership');
|
||||||
const user = currentUser;
|
const user = currentUser;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
try {
|
try {
|
||||||
@ -110,7 +113,7 @@ export function useAuth() {
|
|||||||
await client.database.upsertUserIdentity(user.address, {
|
await client.database.upsertUserIdentity(user.address, {
|
||||||
displayName: identity.displayName,
|
displayName: identity.displayName,
|
||||||
ensName: identity?.ensName || undefined,
|
ensName: identity?.ensName || undefined,
|
||||||
ordinalDetails: identity?.ordinalDetails,
|
ensAvatar: identity?.ensAvatar || undefined,
|
||||||
verificationStatus: identity.verificationStatus,
|
verificationStatus: identity.verificationStatus,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
@ -119,7 +122,7 @@ export function useAuth() {
|
|||||||
...prev,
|
...prev,
|
||||||
session: { ...prev.session, currentUser: updated, verificationStatus: identity.verificationStatus },
|
session: { ...prev.session, currentUser: updated, verificationStatus: identity.verificationStatus },
|
||||||
}));
|
}));
|
||||||
return identity.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
return identity.verificationStatus === EVerificationStatus.ENS_VERIFIED;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('verifyOwnership failed', e);
|
console.error('verifyOwnership failed', e);
|
||||||
return false;
|
return false;
|
||||||
@ -132,15 +135,13 @@ export function useAuth() {
|
|||||||
const user = currentUser;
|
const user = currentUser;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
try {
|
try {
|
||||||
const signer = ((message: string) => WalletManager.getInstance().signMessage(message));
|
|
||||||
const ok = await client.delegation.delegate(
|
const ok = await client.delegation.delegate(
|
||||||
user.address,
|
user.address,
|
||||||
user.walletType,
|
|
||||||
duration,
|
duration,
|
||||||
signer,
|
wallet.signMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await client.delegation.getStatus(user.address, user.walletType);
|
const status = await client.delegation.getStatus(user.address);
|
||||||
setOpchanState(prev => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
session: { ...prev.session, delegation: status },
|
session: { ...prev.session, delegation: status },
|
||||||
@ -150,12 +151,12 @@ export function useAuth() {
|
|||||||
console.error('delegate failed', e);
|
console.error('delegate failed', e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [client, currentUser]);
|
}, [client, currentUser, wallet]);
|
||||||
|
|
||||||
const delegationStatus = React.useCallback(async () => {
|
const delegationStatus = React.useCallback(async () => {
|
||||||
const user = currentUser;
|
const user = currentUser;
|
||||||
if (!user) return { hasDelegation: false, isValid: false } as const;
|
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]);
|
}, [client, currentUser]);
|
||||||
|
|
||||||
const clearDelegation = React.useCallback(async (): Promise<boolean> => {
|
const clearDelegation = React.useCallback(async (): Promise<boolean> => {
|
||||||
@ -222,7 +223,3 @@ export function useAuth() {
|
|||||||
updateProfile,
|
updateProfile,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { BookmarkService } from '@opchan/core';
|
import { BookmarkService } from '@opchan/core';
|
||||||
|
|
||||||
function reflectCache(client: ReturnType<typeof useClient>): void {
|
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 => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
content: {
|
content: {
|
||||||
@ -27,7 +27,7 @@ function reflectCache(client: ReturnType<typeof useClient>): void {
|
|||||||
pendingVotes: prev.content.pendingVotes,
|
pendingVotes: prev.content.pendingVotes,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}).catch(err => {
|
}).catch((err: Error) => {
|
||||||
console.error('reflectCache failed', err);
|
console.error('reflectCache failed', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -77,16 +77,16 @@ export function useContent() {
|
|||||||
const identities = client.database.cache.userIdentities;
|
const identities = client.database.cache.userIdentities;
|
||||||
const result: UserVerificationStatus = {};
|
const result: UserVerificationStatus = {};
|
||||||
for (const [address, rec] of Object.entries(identities)) {
|
for (const [address, rec] of Object.entries(identities)) {
|
||||||
const hasEns = Boolean(rec.ensName);
|
if (rec) {
|
||||||
const hasOrdinal = Boolean(rec.ordinalDetails);
|
const hasEns = Boolean(rec.ensName);
|
||||||
const isVerified = rec.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
const isVerified = rec.verificationStatus === EVerificationStatus.ENS_VERIFIED;
|
||||||
result[address] = {
|
result[address] = {
|
||||||
isVerified,
|
isVerified,
|
||||||
hasENS: hasEns,
|
hasENS: hasEns,
|
||||||
hasOrdinal,
|
ensName: rec.ensName,
|
||||||
ensName: rec.ensName,
|
verificationStatus: rec.verificationStatus,
|
||||||
verificationStatus: rec.verificationStatus,
|
};
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [client.database.cache.userIdentities]);
|
}, [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 { session, content } = useOpchanStore(s => ({ session: s.session, content: s.content }));
|
||||||
const currentUser = session.currentUser;
|
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 isConnected = session.verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED;
|
||||||
|
|
||||||
const canCreateCell = isVerified || isConnected;
|
const canCreateCell = isVerified;
|
||||||
const canPost = isConnected;
|
const canPost = isConnected;
|
||||||
const canComment = isConnected;
|
const canComment = isConnected;
|
||||||
const canVote = isConnected;
|
const canVote = isConnected;
|
||||||
|
|||||||
@ -78,12 +78,12 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address,
|
address: address as `0x${string}`,
|
||||||
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
callSign: undefined,
|
callSign: undefined,
|
||||||
ensName: undefined,
|
ensName: undefined,
|
||||||
ordinalDetails: undefined,
|
ensAvatar: undefined,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@ -2,21 +2,28 @@ import React from "react";
|
|||||||
import { OpChanClient, type OpChanClientConfig } from "@opchan/core";
|
import { OpChanClient, type OpChanClientConfig } from "@opchan/core";
|
||||||
import { ClientProvider } from "../context/ClientContext";
|
import { ClientProvider } from "../context/ClientContext";
|
||||||
import { StoreWiring } from "./StoreWiring";
|
import { StoreWiring } from "./StoreWiring";
|
||||||
import { WalletAdapterInitializer } from "./WalletAdapterInitializer";
|
|
||||||
import { AppKitProvider } from "@reown/appkit/react";
|
|
||||||
import { WagmiProvider } from "wagmi";
|
import { WagmiProvider } from "wagmi";
|
||||||
import { appkitConfig, config as wagmiConfig } from "@opchan/core";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { AppKitErrorBoundary } from "../components/AppKitErrorBoundary";
|
import { wagmiConfig } from "@opchan/core";
|
||||||
|
|
||||||
export interface OpChanProviderProps {
|
export interface OpChanProviderProps {
|
||||||
config: OpChanClientConfig;
|
config: OpChanClientConfig;
|
||||||
children: React.ReactNode;
|
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.
|
* OpChan provider that constructs the OpChanClient and provides wallet context.
|
||||||
* This component already wraps WagmiProvider and AppKitProvider internally,
|
* Simplified to use WagmiProvider + QueryClient only (no AppKit).
|
||||||
* so you can mount it directly at your app root with the required config.
|
|
||||||
*/
|
*/
|
||||||
export const OpChanProvider: React.FC<OpChanProviderProps> = ({
|
export const OpChanProvider: React.FC<OpChanProviderProps> = ({
|
||||||
config,
|
config,
|
||||||
@ -26,15 +33,12 @@ export const OpChanProvider: React.FC<OpChanProviderProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WagmiProvider config={wagmiConfig}>
|
<WagmiProvider config={wagmiConfig}>
|
||||||
<AppKitErrorBoundary>
|
<QueryClientProvider client={defaultQueryClient}>
|
||||||
<AppKitProvider {...appkitConfig}>
|
<ClientProvider client={client}>
|
||||||
<ClientProvider client={client}>
|
<StoreWiring />
|
||||||
<WalletAdapterInitializer />
|
{children}
|
||||||
<StoreWiring />
|
</ClientProvider>
|
||||||
{children}
|
</QueryClientProvider>
|
||||||
</ClientProvider>
|
|
||||||
</AppKitProvider>
|
|
||||||
</AppKitErrorBoundary>
|
|
||||||
</WagmiProvider>
|
</WagmiProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -59,10 +59,9 @@ export const StoreWiring: React.FC = () => {
|
|||||||
// Hydrate session (user + delegation) from LocalDatabase
|
// Hydrate session (user + delegation) from LocalDatabase
|
||||||
try {
|
try {
|
||||||
const loadedUser = await client.database.loadUser();
|
const loadedUser = await client.database.loadUser();
|
||||||
const delegationStatus = await client.delegation.getStatus(
|
const delegationStatus = loadedUser?.address
|
||||||
loadedUser?.address,
|
? await client.delegation.getStatus(loadedUser.address)
|
||||||
loadedUser?.walletType,
|
: null;
|
||||||
);
|
|
||||||
|
|
||||||
setOpchanState(prev => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -128,7 +127,7 @@ export const StoreWiring: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reactively update ALL identities when they refresh (not just current user)
|
// 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 {
|
try {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
// Try to fetch if not provided
|
// 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