Step {currentStep} of 3
{currentStep > 1 && (
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index 870cc44..cd531e9 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -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
=> {
- 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);
diff --git a/src/lib/database/LocalDatabase.ts b/src/lib/database/LocalDatabase.ts
index dcacd6f..3d4658e 100644
--- a/src/lib/database/LocalDatabase.ts
+++ b/src/lib/database/LocalDatabase.ts
@@ -121,7 +121,7 @@ export class LocalDatabase {
public async clearAll(): Promise {
// 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((resolve, reject) => {
@@ -632,7 +632,8 @@ export class LocalDatabase {
record: Partial & { lastUpdated?: number }
): Promise {
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;
diff --git a/src/lib/services/Ordinal/index.ts b/src/lib/services/Ordinal/index.ts
deleted file mode 100644
index 12ae4b0..0000000
--- a/src/lib/services/Ordinal/index.ts
+++ /dev/null
@@ -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 {
- 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;
- }
- }
-}
diff --git a/src/lib/services/Ordinal/types.ts b/src/lib/services/Ordinal/types.ts
deleted file mode 100644
index ca19ab4..0000000
--- a/src/lib/services/Ordinal/types.ts
+++ /dev/null
@@ -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[];
-}
diff --git a/src/lib/services/Ordinals.ts b/src/lib/services/Ordinals.ts
new file mode 100644
index 0000000..9513d71
--- /dev/null
+++ b/src/lib/services/Ordinals.ts
@@ -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 {
+ 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();
diff --git a/src/lib/services/UserIdentityService.ts b/src/lib/services/UserIdentityService.ts
index d477c87..26619e6 100644
--- a/src/lib/services/UserIdentityService.ts
+++ b/src/lib/services/UserIdentityService.ts
@@ -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 {
+ 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;
}
diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts
index e5a05ab..90d0f8b 100644
--- a/src/lib/wallet/index.ts
+++ b/src/lib/wallet/index.ts
@@ -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 {
+ try {
+ return await ordinals.getOrdinalDetails(address);
+ } catch (error) {
+ console.warn('Failed to resolve Ordinal details:', error);
+ return null;
+ }
+ }
+
/**
* Get the currently active wallet
*/
diff --git a/src/pages/DebugPage.tsx b/src/pages/DebugPage.tsx
index f151ba3..d4483f1 100644
--- a/src/pages/DebugPage.tsx
+++ b/src/pages/DebugPage.tsx
@@ -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}
+
{Object.values(MessageType).map(t => (
- {t}: {typeCounts[t] || 0}
+ {t}:{' '}
+ {typeCounts[t] || 0}
))}
-
-
Recent messages
+
+
+ Recent messages
+
ID / Author
Msg Timestamp
{messages.map(m => (
-
+
{formatTs(m.receivedAt)}
- {m.message.type}
+
+ {m.message.type}
+
- {m.message.id} — {m.message.author}
+ {m.message.id} —{' '}
+ {m.message.author}
+
+
+ {formatTs(m.message.timestamp)}
- {formatTs(m.message.timestamp)}
))}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 8fcfba0..e4b60f8 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -277,7 +277,9 @@ export default function ProfilePage() {
{userInfo.displayName}
- {currentUser.ensDetails?.ensName || 'No ENS name'}
+ {userInfo.ordinalDetails || currentUser.ordinalDetails?.ordinalDetails
+ ? `Ordinal: ${userInfo.ordinalDetails || currentUser.ordinalDetails?.ordinalDetails}`
+ : currentUser.ensDetails?.ensName || 'No ENS name'}
{getVerificationIcon()}