feat: add operator ordinal validation (#23)

* chore: add ordiscan

* feat: add ordinal check

* chore: fix CSS + useMemo

* chore: show inscription ID on /profile
This commit is contained in:
Danish Arora 2025-09-12 16:03:12 +05:30 committed by GitHub
parent 849a5b41be
commit 243403652e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 266 additions and 198 deletions

19
package-lock.json generated
View File

@ -52,6 +52,7 @@
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"ordiscan": "^1.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@ -12609,6 +12610,18 @@
"node": ">= 0.8.0"
}
},
"node_modules/ordiscan": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ordiscan/-/ordiscan-1.3.0.tgz",
"integrity": "sha512-IV8yayKGIRtfkI3rQ1gu+aKQ6UmNHXB910qUuWrraCcuk6U/YkE+6X8Bh+jW2P8L8/lOfYA6DLVeKCVPkj8c6g==",
"license": "MIT",
"dependencies": {
"zod": "^3.24.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/ox": {
"version": "0.6.9",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz",
@ -15898,9 +15911,9 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -59,6 +59,7 @@
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"ordiscan": "^1.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",

View File

@ -127,7 +127,7 @@ const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => {
</span>
</div>
<ShareButton
size='sm'
size="sm"
url={`${window.location.origin}/cell/${cell.id}`}
title={cell.name}
/>

View File

@ -129,9 +129,12 @@ const CommentCard: React.FC<CommentCardProps> = ({
</div>
<div className="flex items-center gap-2">
<ShareButton
size='sm'
size="sm"
url={`${window.location.origin}/post/${postId}#comment-${comment.id}`}
title={comment.content.substring(0, 50) + (comment.content.length > 50 ? '...' : '')}
title={
comment.content.substring(0, 50) +
(comment.content.length > 50 ? '...' : '')
}
/>
<BookmarkButton
isBookmarked={isBookmarked}

View File

@ -118,7 +118,11 @@ export function CreateCellDialog({
<DialogTitle className="text-glow">Create New Cell</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} onKeyDown={handleKeyDown} className="space-y-4">
<form
onSubmit={form.handleSubmit(onSubmit)}
onKeyDown={handleKeyDown}
className="space-y-4"
>
<FormField
control={form.control}
name="title"

View File

@ -293,7 +293,7 @@ const Header = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
onSelect={e => e.preventDefault()}
className="flex items-center space-x-2 text-orange-400 focus:text-orange-400"
>
<Trash2 className="w-4 h-4" />
@ -306,7 +306,8 @@ const Header = () => {
Clear Local Database
</AlertDialogTitle>
<AlertDialogDescription className="text-cyber-neutral">
This will permanently delete all locally stored data including:
This will permanently delete all locally stored
data including:
<br /> Posts and comments
<br /> User identities and preferences
<br /> Bookmarks and votes

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowUp, ArrowDown, MessageSquare, Clipboard } from 'lucide-react';
import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { Post } from '@/types/forum';
import {
@ -70,7 +70,6 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
await toggleBookmark();
};
return (
<div className="thread-card mb-2">
<div className="flex">
@ -180,7 +179,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
</span>
)}
<ShareButton
size='sm'
size="sm"
url={`${window.location.origin}/post/${post.id}`}
title={post.title}
/>

View File

@ -255,7 +255,7 @@ const PostDetail = () => {
showText={true}
/>
<ShareButton
size='lg'
size="lg"
url={`${window.location.origin}/post/${post.id}`}
title={post.title}
/>

View File

@ -7,7 +7,7 @@ import {
usePermissions,
useUserVotes,
useAuth,
usePostComments,
useForumData,
} from '@/hooks';
import { EVerificationStatus } from '@/types/identity';
import { Button } from '@/components/ui/button';
@ -56,6 +56,7 @@ const PostList = () => {
const { canPost, canVote, canModerate } = usePermissions();
const userVotes = useUserVotes();
const { currentUser, verificationStatus } = useAuth();
const { commentsByPost } = useForumData();
const [newPostTitle, setNewPostTitle] = useState('');
const [newPostContent, setNewPostContent] = useState('');
@ -339,7 +340,7 @@ const PostList = () => {
<span></span>
<span>
<MessageSquare className="inline w-3 h-3 mr-1" />
{usePostComments(post.id).totalCount} comments
{commentsByPost[post.id]?.length || 0} comments
</span>
<ShareButton
url={`${window.location.origin}/post/${post.id}`}

View File

@ -15,8 +15,6 @@ interface ShareButtonProps {
export function ShareButton({
url,
title,
description = 'Check out this post',
size = 'sm',
variant = 'ghost',
className,
@ -73,9 +71,7 @@ export function ShareButton({
title="Copy link"
>
<Share2 size={iconSize[size]} />
{showText && (
<span className="ml-2 text-xs">Share</span>
)}
{showText && <span className="ml-2 text-xs">Share</span>}
</Button>
);
}
}

View File

@ -55,21 +55,9 @@ export function WalletWizard({
onOpenChange(false);
};
// Business logic: determine step status based on current wizard step
// Consolidated step status logic
const getStepStatus = (step: WizardStep) => {
if (step < currentStep) {
return 'complete';
} else if (step === currentStep) {
return 'current';
} else {
return 'disabled';
}
};
const renderStepIcon = (step: WizardStep) => {
const status = getStepStatus(step);
// Check if step is actually completed based on auth state
// Check actual completion status first
const isActuallyComplete = (step: WizardStep): boolean => {
switch (step) {
case 1:
@ -83,7 +71,19 @@ export function WalletWizard({
}
};
if (status === 'complete' || isActuallyComplete(step)) {
if (isActuallyComplete(step)) {
return 'complete';
} else if (step === currentStep) {
return 'current';
} else {
return 'disabled';
}
};
const renderStepIcon = (step: WizardStep) => {
const status = getStepStatus(step);
if (status === 'complete') {
return <CheckCircle className="h-5 w-5 text-green-500" />;
} else if (status === 'current') {
return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
@ -107,7 +107,7 @@ export function WalletWizard({
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md border-neutral-800 bg-black text-white">
<DialogContent className="sm:max-w-lg border-neutral-800 bg-black text-white">
<DialogHeader>
<DialogTitle className="text-xl">Setup Your Account</DialogTitle>
<DialogDescription className="text-neutral-400">
@ -116,21 +116,16 @@ export function WalletWizard({
</DialogHeader>
{/* Progress Indicator */}
<div className="flex items-center justify-between mb-6">
{[1, 2, 3].map(step => (
<div className="flex items-center justify-center mb-8">
{[1, 2, 3].map((step, index) => (
<div key={step} className="flex items-center">
<div className="flex items-center gap-2">
<div className="flex flex-col items-center gap-2">
{renderStepIcon(step as WizardStep)}
<span
className={`text-sm ${
className={`text-sm font-medium ${
getStepStatus(step as WizardStep) === 'current'
? 'text-blue-500 font-medium'
: getStepStatus(step as WizardStep) === 'complete' ||
(step === 1 && isAuthenticated) ||
(step === 2 &&
verificationStatus !==
EVerificationStatus.WALLET_UNCONNECTED) ||
(step === 3 && delegationStatus.isValid)
? 'text-blue-500'
: getStepStatus(step as WizardStep) === 'complete'
? 'text-green-500'
: 'text-gray-400'
}`}
@ -138,14 +133,10 @@ export function WalletWizard({
{getStepTitle(step as WizardStep)}
</span>
</div>
{step < 3 && (
{index < 2 && (
<div
className={`w-8 h-px mx-2 ${
getStepStatus(step as WizardStep) === 'complete' ||
(step === 1 && isAuthenticated) ||
(step === 2 &&
verificationStatus !==
EVerificationStatus.WALLET_UNCONNECTED)
className={`w-16 h-px mx-4 ${
getStepStatus(step as WizardStep) === 'complete'
? 'bg-green-500'
: 'bg-gray-600'
}`}
@ -155,8 +146,8 @@ export function WalletWizard({
))}
</div>
{/* Step Content - Fixed height container */}
<div className="h-[400px] flex flex-col">
{/* Step Content - Flexible height container */}
<div className="min-h-[400px] flex flex-col">
{currentStep === 1 && (
<WalletConnectionStep
onComplete={() => handleStepComplete(1)}
@ -185,7 +176,7 @@ export function WalletWizard({
</div>
{/* Footer */}
<div className="flex justify-between items-center pt-4 border-t border-neutral-700">
<div className="flex justify-between items-center pt-6 mt-4 border-t border-neutral-700">
<p className="text-xs text-neutral-500">Step {currentStep} of 3</p>
{currentStep > 1 && (
<Button
@ -193,7 +184,7 @@ export function WalletWizard({
size="sm"
onClick={() => setCurrentStep((currentStep - 1) as WizardStep)}
disabled={isLoading}
className="text-neutral-400 hover:text-white"
className="text-neutral-400 hover:text-white hover:bg-neutral-800"
>
Back
</Button>

View File

@ -14,6 +14,8 @@ import {
} from '@/lib/delegation';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
import { MessageService } from '@/lib/services/MessageService';
import { UserIdentityService } from '@/lib/services/UserIdentityService';
interface AuthContextType {
currentUser: User | null;
@ -56,6 +58,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Create manager instances
const delegationManager = useMemo(() => new DelegationManager(), []);
const messageService = useMemo(
() => new MessageService(delegationManager),
[delegationManager]
);
const userIdentityService = useMemo(
() => new UserIdentityService(messageService),
[messageService]
);
// Create wallet manager when we have all dependencies
useEffect(() => {
@ -89,51 +99,44 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
};
// Helper function for ownership verification
// Helper function for ownership verification (via UserIdentityService)
const verifyUserOwnership = async (user: User): Promise<User> => {
if (user.walletType === 'bitcoin') {
// TODO: revert when the API is ready
// const response = await ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
return {
...user,
ordinalDetails: hasOperators
? { ordinalId: 'mock', ordinalDetails: 'Mock ordinal for testing' }
: undefined,
verificationStatus: hasOperators
? EVerificationStatus.ENS_ORDINAL_VERIFIED
: EVerificationStatus.WALLET_CONNECTED,
lastChecked: Date.now(),
};
} else if (user.walletType === 'ethereum') {
try {
const walletInfo = WalletManager.hasInstance()
? await WalletManager.getInstance().getWalletInfo()
: null;
const hasENS = !!walletInfo?.ensName;
const ensName = walletInfo?.ensName;
return {
...user,
ensDetails: hasENS && ensName ? { ensName } : undefined,
verificationStatus: hasENS
? EVerificationStatus.ENS_ORDINAL_VERIFIED
: EVerificationStatus.WALLET_CONNECTED,
lastChecked: Date.now(),
};
} catch (error) {
console.error('Error verifying ENS ownership:', error);
try {
// Force fresh resolution to ensure API call happens during verification
const identity = await userIdentityService.getUserIdentityFresh(
user.address
);
if (!identity) {
return {
...user,
ensDetails: undefined,
ordinalDetails: undefined,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
lastChecked: Date.now(),
};
}
} else {
throw new Error('Unknown wallet type');
return {
...user,
ensDetails: identity.ensName
? { ensName: identity.ensName }
: undefined,
ordinalDetails: identity.ordinalDetails,
verificationStatus: identity.verificationStatus,
lastChecked: Date.now(),
};
} catch (error) {
console.error(
'Error verifying ownership via UserIdentityService:',
error
);
return {
...user,
ensDetails: undefined,
ordinalDetails: undefined,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
lastChecked: Date.now(),
};
}
};
@ -212,12 +215,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
EVerificationStatus.ENS_ORDINAL_VERIFIED
);
await saveUser(updatedUser);
await localDatabase.upsertUserIdentity(updatedUser.address, {
ensName: walletInfo.ensName,
verificationStatus:
EVerificationStatus.ENS_ORDINAL_VERIFIED,
lastUpdated: Date.now(),
});
await localDatabase.upsertUserIdentity(
updatedUser.address,
{
ensName: walletInfo.ensName,
verificationStatus:
EVerificationStatus.ENS_ORDINAL_VERIFIED,
lastUpdated: Date.now(),
}
);
} else {
setCurrentUser(newUser);
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);

View File

@ -121,7 +121,7 @@ export class LocalDatabase {
public async clearAll(): Promise<void> {
// Clear in-memory cache
this.clear();
// Clear all IndexedDB stores
if (!this.db) return;
@ -140,7 +140,7 @@ export class LocalDatabase {
];
const tx = this.db.transaction(storeNames, 'readwrite');
await Promise.all(
storeNames.map(storeName => {
return new Promise<void>((resolve, reject) => {
@ -632,7 +632,8 @@ export class LocalDatabase {
record: Partial<UserIdentityCache[string]> & { lastUpdated?: number }
): Promise<void> {
const existing: UserIdentityCache[string] =
this.cache.userIdentities[address] || {
this.cache.userIdentities[address] ||
({
ensName: undefined,
ordinalDetails: undefined,
callSign: undefined,
@ -644,12 +645,15 @@ export class LocalDatabase {
// Casting below ensures the object satisfies the interface at compile time.
lastUpdated: 0,
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
} as unknown as UserIdentityCache[string];
} as unknown as UserIdentityCache[string]);
const merged: UserIdentityCache[string] = {
...existing,
...record,
lastUpdated: Math.max(existing.lastUpdated ?? 0, record.lastUpdated ?? Date.now()),
lastUpdated: Math.max(
existing.lastUpdated ?? 0,
record.lastUpdated ?? Date.now()
),
} as UserIdentityCache[string];
this.cache.userIdentities[address] = merged;

View File

@ -1,54 +0,0 @@
import { OrdinalApiResponse } from './types';
const BASE_URL = 'https://dashboard.logos.co/api/operators/wallet';
export class OrdinalAPI {
/**
* Fetches Ordinal operator details for a given Bitcoin address.
* @param address - The Bitcoin address to query.
* @returns A promise that resolves with the API response.
*/
async getOperatorDetails(address: string): Promise<OrdinalApiResponse> {
if (import.meta.env.VITE_OPCHAN_MOCK_ORDINAL_CHECK === 'true') {
console.log(
`[DEV] Bypassing ordinal verification for address: ${address}`
);
return {
has_operators: true,
error_message: '',
data: [],
};
}
const url = `${BASE_URL}/${address}/detail/`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (!response.ok) {
const errorBody = await response.text().catch(() => '');
throw new Error(
`HTTP error! status: ${response.status}, message: ${errorBody || response.statusText}`
);
}
const data: OrdinalApiResponse = await response.json();
if (data.error_message) {
console.warn(
`API returned an error message for address ${address}: ${data.error_message}`
);
}
return data;
} catch (error) {
console.error(
`Failed to fetch ordinal details for address ${address}:`,
error
);
throw error;
}
}
}

View File

@ -1,25 +0,0 @@
export interface OrdinalDetail {
name: string;
archetype_name: string;
comp: string;
background: string;
skin: string;
helmet: string;
jacket: string;
image_200_url: string;
image_200_jpeg_url: string;
image_400_url: string;
image_400_jpeg_url: string;
image_1024_url: string;
image_1024_jpeg_url: string;
image_2048_url: string;
image_2048_jpeg_url: string;
image_pixalated_url: string;
mp4_url: string;
}
export interface OrdinalApiResponse {
has_operators: boolean;
error_message: string;
data: OrdinalDetail[];
}

View File

@ -0,0 +1,47 @@
import { Ordiscan, Inscription } from 'ordiscan';
const API_KEY = import.meta.env.VITE_ORDISCAN_API;
class Ordinals {
private static instance: Ordinals | null = null;
private ordiscan: Ordiscan;
private readonly PARENT_INSCRIPTION_ID =
'add60add0325f7c82e80d4852a8b8d5c46dbde4317e76fe4def2e718dd84b87ci0';
private constructor(ordiscan: Ordiscan) {
this.ordiscan = ordiscan;
}
static getInstance(): Ordinals {
if (!Ordinals.instance) {
Ordinals.instance = new Ordinals(new Ordiscan(API_KEY));
}
return Ordinals.instance;
}
/**
* 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 = Ordinals.getInstance();

View File

@ -8,6 +8,7 @@ import {
import { MessageService } from './MessageService';
import messageManager from '@/lib/waku';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { WalletManager } from '@/lib/wallet';
export interface UserIdentity {
address: string;
@ -155,6 +156,29 @@ export class UserIdentityService {
return identity;
}
/**
* Force a fresh identity resolution bypassing caches and LocalDatabase.
* Useful for explicit verification flows where we must hit upstream resolvers.
*/
async getUserIdentityFresh(address: string): Promise<UserIdentity | null> {
if (import.meta.env?.DEV) {
console.debug('UserIdentityService: fresh resolve requested');
}
const identity = await this.resolveUserIdentity(address);
if (identity) {
// Update in-memory cache to reflect the fresh result
this.userIdentityCache[address] = {
ensName: identity.ensName,
ordinalDetails: identity.ordinalDetails,
callSign: identity.callSign,
displayPreference: identity.displayPreference,
lastUpdated: identity.lastUpdated,
verificationStatus: identity.verificationStatus,
};
}
return identity;
}
/**
* Get all cached user identities
*/
@ -329,8 +353,19 @@ export class UserIdentityService {
address: string
): Promise<{ ordinalId: string; ordinalDetails: string } | null> {
try {
//TODO: add Ordinal API call
console.log('resolveOrdinalDetails', address);
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);
@ -375,12 +410,22 @@ export class UserIdentityService {
*/
private mapVerificationStatus(status: string): EVerificationStatus {
switch (status) {
// Legacy message-cache statuses
case 'verified-basic':
return EVerificationStatus.WALLET_CONNECTED;
case 'verified-owner':
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
case 'verifying':
return EVerificationStatus.WALLET_CONNECTED; // Temporary state during verification
// Enum string values persisted in LocalDatabase
case EVerificationStatus.WALLET_UNCONNECTED:
return EVerificationStatus.WALLET_UNCONNECTED;
case EVerificationStatus.WALLET_CONNECTED:
return EVerificationStatus.WALLET_CONNECTED;
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return EVerificationStatus.ENS_ORDINAL_VERIFIED;
default:
return EVerificationStatus.WALLET_UNCONNECTED;
}

View File

@ -1,5 +1,6 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit';
import { ordinals } from '@/lib/services/Ordinals';
import {
getEnsName,
verifyMessage as verifyEthereumMessage,
@ -8,6 +9,7 @@ import { ChainNamespace } from '@reown/appkit-common';
import { config } from './config';
import { Provider } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types';
import { Inscription } from 'ordiscan';
export class WalletManager {
private static instance: WalletManager | null = null;
@ -95,6 +97,20 @@ export class WalletManager {
}
}
/**
* 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;
}
}
/**
* Get the currently active wallet
*/

View File

@ -15,7 +15,9 @@ export default function DebugPage() {
useEffect(() => {
// Subscribe to inbound messages from reliable channel
unsubscribeRef.current = messageManager.onMessageReceived(msg => {
setMessages(prev => [{ receivedAt: Date.now(), message: msg }, ...prev].slice(0, 500));
setMessages(prev =>
[{ receivedAt: Date.now(), message: msg }, ...prev].slice(0, 500)
);
});
return () => {
@ -47,7 +49,9 @@ export default function DebugPage() {
Total received: {messages.length}
</div>
<div style={{ marginTop: 12, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<div
style={{ marginTop: 12, display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
{Object.values(MessageType).map(t => (
<div
key={t}
@ -59,13 +63,22 @@ export default function DebugPage() {
background: 'rgba(51,65,85,0.2)',
}}
>
<strong style={{ textTransform: 'capitalize' }}>{t}</strong>: {typeCounts[t] || 0}
<strong style={{ textTransform: 'capitalize' }}>{t}</strong>:{' '}
{typeCounts[t] || 0}
</div>
))}
</div>
<div style={{ marginTop: 16, borderTop: '1px solid #334155', paddingTop: 12 }}>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 8 }}>Recent messages</div>
<div
style={{
marginTop: 16,
borderTop: '1px solid #334155',
paddingTop: 12,
}}
>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 8 }}>
Recent messages
</div>
<div
style={{
display: 'grid',
@ -79,9 +92,11 @@ export default function DebugPage() {
<div style={{ fontWeight: 700, color: '#cbd5e1' }}>ID / Author</div>
<div style={{ fontWeight: 700, color: '#cbd5e1' }}>Msg Timestamp</div>
{messages.map(m => (
<Fragment key={`${m.message.id}:${m.receivedAt}`}>
<Fragment key={`${m.message.id}:${m.receivedAt}`}>
<div style={{ color: '#e5e7eb' }}>{formatTs(m.receivedAt)}</div>
<div style={{ textTransform: 'capitalize', color: '#e5e7eb' }}>{m.message.type}</div>
<div style={{ textTransform: 'capitalize', color: '#e5e7eb' }}>
{m.message.type}
</div>
<div
style={{
overflow: 'hidden',
@ -91,9 +106,12 @@ export default function DebugPage() {
}}
title={`${m.message.id}${m.message.author}`}
>
{m.message.id} <span style={{ color: '#94a3b8' }}>{m.message.author}</span>
{m.message.id} {' '}
<span style={{ color: '#94a3b8' }}>{m.message.author}</span>
</div>
<div style={{ color: '#e5e7eb' }}>
{formatTs(m.message.timestamp)}
</div>
<div style={{ color: '#e5e7eb' }}>{formatTs(m.message.timestamp)}</div>
</Fragment>
))}
</div>

View File

@ -277,7 +277,9 @@ export default function ProfilePage() {
{userInfo.displayName}
</div>
<div className="text-sm text-cyber-neutral">
{currentUser.ensDetails?.ensName || 'No ENS name'}
{userInfo.ordinalDetails || currentUser.ordinalDetails?.ordinalDetails
? `Ordinal: ${userInfo.ordinalDetails || currentUser.ordinalDetails?.ordinalDetails}`
: currentUser.ensDetails?.ensName || 'No ENS name'}
</div>
<div className="flex items-center gap-2 mt-2">
{getVerificationIcon()}