feat: add ordinal check

This commit is contained in:
Danish Arora 2025-09-12 15:55:20 +05:30
parent 1dd4ca7304
commit 97d7926659
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
4 changed files with 132 additions and 39 deletions

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,39 @@ 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(),
};
}
};

View File

@ -0,0 +1,36 @@
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,18 @@ 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
*/