mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-07 15:23:05 +00:00
chore: ens resolution
This commit is contained in:
parent
509faae6c9
commit
e29fc8ed59
1
package-lock.json
generated
1
package-lock.json
generated
@ -64,7 +64,6 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^0.9.3",
|
"vaul": "^0.9.3",
|
||||||
"viem": "^2.33.2",
|
|
||||||
"wagmi": "^2.16.1",
|
"wagmi": "^2.16.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -69,7 +69,6 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^0.9.3",
|
"vaul": "^0.9.3",
|
||||||
"viem": "^2.33.2",
|
|
||||||
"wagmi": "^2.16.1",
|
"wagmi": "^2.16.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const Header = () => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
verificationStatus,
|
verificationStatus,
|
||||||
verifyOrdinal,
|
verifyOwnership,
|
||||||
delegateKey,
|
delegateKey,
|
||||||
isDelegationValid,
|
isDelegationValid,
|
||||||
delegationTimeRemaining,
|
delegationTimeRemaining,
|
||||||
@ -49,7 +49,7 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
await verifyOrdinal();
|
await verifyOwnership();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelegateKey = async () => {
|
const handleDelegateKey = async () => {
|
||||||
@ -246,11 +246,11 @@ const Header = () => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
|
<span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
|
||||||
{address?.slice(0, 5)}...{address?.slice(-4)}
|
{currentUser?.ensName || `${address?.slice(0, 5)}...${address?.slice(-4)}`}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-sm">
|
<TooltipContent className="text-sm">
|
||||||
<p>{address}</p>
|
<p>{currentUser?.ensName ? `${currentUser.ensName} (${address})` : address}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ interface AuthContextType {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isAuthenticating: boolean;
|
isAuthenticating: boolean;
|
||||||
verificationStatus: VerificationStatus;
|
verificationStatus: VerificationStatus;
|
||||||
verifyOrdinal: () => Promise<boolean>;
|
verifyOwnership: () => Promise<boolean>;
|
||||||
delegateKey: () => Promise<boolean>;
|
delegateKey: () => Promise<boolean>;
|
||||||
isDelegationValid: () => boolean;
|
isDelegationValid: () => boolean;
|
||||||
delegationTimeRemaining: () => number;
|
delegationTimeRemaining: () => number;
|
||||||
@ -49,6 +49,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Create ref for AuthService so it persists between renders
|
// Create ref for AuthService so it persists between renders
|
||||||
const authServiceRef = useRef(new AuthService());
|
const authServiceRef = useRef(new AuthService());
|
||||||
|
|
||||||
|
// Set AppKit accounts in AuthService
|
||||||
|
useEffect(() => {
|
||||||
|
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
|
||||||
|
}, [bitcoinAccount, ethereumAccount]);
|
||||||
|
|
||||||
// Sync with AppKit wallet state
|
// Sync with AppKit wallet state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && address) {
|
if (isConnected && address) {
|
||||||
@ -58,33 +63,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (storedUser && storedUser.address === address) {
|
if (storedUser && storedUser.address === address) {
|
||||||
// Use stored user data
|
// Use stored user data
|
||||||
setCurrentUser(storedUser);
|
setCurrentUser(storedUser);
|
||||||
if ('ordinalOwnership' in storedUser) {
|
setVerificationStatus(getVerificationStatus(storedUser));
|
||||||
setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
|
||||||
} else {
|
|
||||||
setVerificationStatus('unverified');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Create new user from AppKit wallet
|
// Create new user from AppKit wallet
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
address,
|
address,
|
||||||
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
|
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
|
||||||
ordinalOwnership: false,
|
|
||||||
delegationExpiry: null,
|
|
||||||
verificationStatus: 'unverified',
|
verificationStatus: 'unverified',
|
||||||
|
lastChecked: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
authServiceRef.current.saveUser(newUser);
|
authServiceRef.current.saveUser(newUser);
|
||||||
|
|
||||||
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
||||||
toast({
|
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
title: "Wallet Connected",
|
|
||||||
description: `Connected to ${chainName} with address ${address.slice(0, 6)}...${address.slice(-4)}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Wallet Connected",
|
||||||
|
description: `Connected to ${chainName} with ${displayName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership';
|
||||||
toast({
|
toast({
|
||||||
title: "Action Required",
|
title: "Action Required",
|
||||||
description: "Please verify your Ordinal ownership and delegate a signing key for better UX.",
|
description: `Please verify your ${verificationType} and delegate a signing key for better UX.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -92,9 +96,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
}
|
}
|
||||||
}, [isConnected, address, isBitcoinConnected, toast]);
|
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
|
||||||
|
|
||||||
const verifyOrdinal = async (): Promise<boolean> => {
|
const getVerificationStatus = (user: User): VerificationStatus => {
|
||||||
|
if (user.walletType === 'bitcoin') {
|
||||||
|
return user.ordinalOwnership ? 'verified-owner' : 'verified-none';
|
||||||
|
} else if (user.walletType === 'ethereum') {
|
||||||
|
return user.ensOwnership ? 'verified-owner' : 'verified-none';
|
||||||
|
}
|
||||||
|
return 'unverified';
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyOwnership = async (): Promise<boolean> => {
|
||||||
if (!currentUser || !currentUser.address) {
|
if (!currentUser || !currentUser.address) {
|
||||||
toast({
|
toast({
|
||||||
title: "Not Connected",
|
title: "Not Connected",
|
||||||
@ -108,12 +121,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setVerificationStatus('verifying');
|
setVerificationStatus('verifying');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS';
|
||||||
toast({
|
toast({
|
||||||
title: "Verifying Ordinal",
|
title: `Verifying ${verificationType}`,
|
||||||
description: "Checking your wallet for Ordinal Operators..."
|
description: `Checking your wallet for ${verificationType} ownership...`
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: AuthResult = await authServiceRef.current.verifyOrdinal(currentUser);
|
const result: AuthResult = await authServiceRef.current.verifyOwnership(currentUser);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
@ -124,27 +138,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
authServiceRef.current.saveUser(updatedUser);
|
authServiceRef.current.saveUser(updatedUser);
|
||||||
|
|
||||||
// Update verification status
|
// Update verification status
|
||||||
setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
setVerificationStatus(getVerificationStatus(updatedUser));
|
||||||
|
|
||||||
if (updatedUser.ordinalOwnership) {
|
if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) {
|
||||||
toast({
|
toast({
|
||||||
title: "Ordinal Verified",
|
title: "Ordinal Verified",
|
||||||
description: "You now have full access. We recommend delegating a key for better UX.",
|
description: "You now have full access. We recommend delegating a key for better UX.",
|
||||||
});
|
});
|
||||||
|
} else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) {
|
||||||
|
toast({
|
||||||
|
title: "ENS Verified",
|
||||||
|
description: "You now have full access. We recommend delegating a key for better UX.",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain';
|
||||||
toast({
|
toast({
|
||||||
title: "Read-Only Access",
|
title: "Read-Only Access",
|
||||||
description: "No Ordinal Operators found. You have read-only access.",
|
description: `No ${verificationType} found. You have read-only access.`,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(updatedUser.ordinalOwnership);
|
return Boolean(
|
||||||
|
(updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) ||
|
||||||
|
(updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error verifying Ordinal:", error);
|
console.error("Error verifying ownership:", error);
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
|
|
||||||
let errorMessage = "Failed to verify Ordinal ownership. Please try again.";
|
let errorMessage = "Failed to verify ownership. Please try again.";
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
@ -203,24 +226,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
console.error("Error delegating key:", error);
|
console.error("Error delegating key:", error);
|
||||||
|
|
||||||
let errorMessage = "Failed to delegate key. Please try again.";
|
let errorMessage = "Failed to delegate key. Please try again.";
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
// Provide specific guidance based on error type
|
|
||||||
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
|
|
||||||
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
|
|
||||||
} else if (error.message.includes("timeout")) {
|
|
||||||
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
|
|
||||||
} else if (error.message.includes("Failed to connect wallet")) {
|
|
||||||
errorMessage = "Unable to connect to wallet. Please ensure it's installed and unlocked, then try again.";
|
|
||||||
} else if (error.message.includes("Wallet is not connected")) {
|
|
||||||
errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again.";
|
|
||||||
} else {
|
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Delegation Failed",
|
title: "Delegation Error",
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@ -240,27 +251,33 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isWalletAvailable = (): boolean => {
|
const isWalletAvailable = (): boolean => {
|
||||||
return isConnected && !!address;
|
return isConnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const messageSigning = {
|
||||||
<AuthContext.Provider
|
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
|
||||||
value={{
|
return authServiceRef.current.signMessage(message);
|
||||||
|
},
|
||||||
|
verifyMessage: (message: OpchanMessage): boolean => {
|
||||||
|
return authServiceRef.current.verifyMessage(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const value: AuthContextType = {
|
||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated: !!currentUser?.ordinalOwnership,
|
isAuthenticated: Boolean(currentUser && isConnected),
|
||||||
isAuthenticating,
|
isAuthenticating,
|
||||||
verificationStatus,
|
verificationStatus,
|
||||||
verifyOrdinal,
|
verifyOwnership,
|
||||||
delegateKey,
|
delegateKey,
|
||||||
isDelegationValid,
|
isDelegationValid,
|
||||||
delegationTimeRemaining,
|
delegationTimeRemaining,
|
||||||
isWalletAvailable,
|
isWalletAvailable,
|
||||||
messageSigning: {
|
messageSigning
|
||||||
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
|
};
|
||||||
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),
|
|
||||||
},
|
return (
|
||||||
}}
|
<AuthContext.Provider value={value}>
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
import { WalletService } from '../wallets';
|
import { WalletService, AppKitAccount } from '../wallets/index';
|
||||||
import { OrdinalAPI } from '../ordinal';
|
import { OrdinalAPI } from '../ordinal';
|
||||||
import { MessageSigning } from '../signatures/message-signing';
|
import { MessageSigning } from '../signatures/message-signing';
|
||||||
import { OpchanMessage } from '@/types';
|
import { OpchanMessage } from '@/types';
|
||||||
@ -18,7 +18,14 @@ export class AuthService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.walletService = new WalletService();
|
this.walletService = new WalletService();
|
||||||
this.ordinalApi = new OrdinalAPI();
|
this.ordinalApi = new OrdinalAPI();
|
||||||
this.messageSigning = new MessageSigning(this.walletService['keyDelegation']);
|
this.messageSigning = new MessageSigning(this.walletService.getKeyDelegation());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set AppKit accounts for wallet service
|
||||||
|
*/
|
||||||
|
setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) {
|
||||||
|
this.walletService.setAccounts(bitcoinAccount, ethereumAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,20 +33,27 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async connectWallet(): Promise<AuthResult> {
|
async connectWallet(): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
if (!this.walletService.isWalletAvailable('phantom')) {
|
const walletInfo = await this.walletService.getWalletInfo();
|
||||||
|
if (!walletInfo) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Phantom wallet not installed'
|
error: 'No wallet connected'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await this.walletService.connectWallet('phantom');
|
|
||||||
|
|
||||||
const user: User = {
|
const user: User = {
|
||||||
address,
|
address: walletInfo.address,
|
||||||
|
walletType: walletInfo.walletType,
|
||||||
|
verificationStatus: 'unverified',
|
||||||
lastChecked: Date.now(),
|
lastChecked: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add ENS info for Ethereum wallets
|
||||||
|
if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) {
|
||||||
|
user.ensName = walletInfo.ensName;
|
||||||
|
user.ensOwnership = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
user
|
user
|
||||||
@ -56,14 +70,39 @@ export class AuthService {
|
|||||||
* Disconnect wallet and clear user data
|
* Disconnect wallet and clear user data
|
||||||
*/
|
*/
|
||||||
async disconnectWallet(): Promise<void> {
|
async disconnectWallet(): Promise<void> {
|
||||||
await this.walletService.disconnectWallet('phantom');
|
const walletType = this.walletService.getActiveWalletType();
|
||||||
|
if (walletType) {
|
||||||
|
await this.walletService.disconnectWallet(walletType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify ordinal ownership for a user
|
* Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users
|
||||||
*/
|
*/
|
||||||
async verifyOrdinal(user: User): Promise<AuthResult> {
|
async verifyOwnership(user: User): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
|
if (user.walletType === 'bitcoin') {
|
||||||
|
return await this.verifyBitcoinOrdinal(user);
|
||||||
|
} else if (user.walletType === 'ethereum') {
|
||||||
|
return await this.verifyEthereumENS(user);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown wallet type'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to verify ownership'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Bitcoin Ordinal ownership
|
||||||
|
*/
|
||||||
|
private async verifyBitcoinOrdinal(user: User): Promise<AuthResult> {
|
||||||
// TODO: revert when the API is ready
|
// TODO: revert when the API is ready
|
||||||
// const response = await this.ordinalApi.getOperatorDetails(user.address);
|
// const response = await this.ordinalApi.getOperatorDetails(user.address);
|
||||||
// const hasOperators = response.has_operators;
|
// const hasOperators = response.has_operators;
|
||||||
@ -79,12 +118,26 @@ export class AuthService {
|
|||||||
success: true,
|
success: true,
|
||||||
user: updatedUser
|
user: updatedUser
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to verify ordinal'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Ethereum ENS ownership
|
||||||
|
*/
|
||||||
|
private async verifyEthereumENS(user: User): Promise<AuthResult> {
|
||||||
|
const walletInfo = await this.walletService.getWalletInfo();
|
||||||
|
const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0;
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
ensOwnership: hasENS,
|
||||||
|
ensName: walletInfo?.ensName,
|
||||||
|
lastChecked: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: updatedUser
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,17 +145,18 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async delegateKey(user: User): Promise<AuthResult> {
|
async delegateKey(user: User): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
const canConnect = await this.walletService.canConnectWallet('phantom');
|
const walletType = user.walletType;
|
||||||
|
const canConnect = await this.walletService.canConnectWallet(walletType);
|
||||||
if (!canConnect) {
|
if (!canConnect) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Phantom wallet is not available or cannot be connected. Please ensure it is installed and unlocked.'
|
error: `${walletType} wallet is not available or cannot be connected. Please ensure it is installed and unlocked.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const delegationInfo = await this.walletService.setupKeyDelegation(
|
const delegationInfo = await this.walletService.setupKeyDelegation(
|
||||||
user.address,
|
user.address,
|
||||||
'phantom'
|
walletType
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
@ -155,8 +209,8 @@ export class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Get current wallet info
|
* Get current wallet info
|
||||||
*/
|
*/
|
||||||
getWalletInfo() {
|
async getWalletInfo() {
|
||||||
return this.walletService.getWalletInfo();
|
return await this.walletService.getWalletInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
225
src/lib/identity/wallets/ReOwnWalletService.ts
Normal file
225
src/lib/identity/wallets/ReOwnWalletService.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { KeyDelegation } from '../signatures/key-delegation';
|
||||||
|
import { bytesToHex } from '@/lib/utils';
|
||||||
|
import { getEnsName } from '@wagmi/core';
|
||||||
|
import { config } from './appkit';
|
||||||
|
import { UseAppKitAccountReturn } from '@reown/appkit';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface WalletInfo {
|
||||||
|
address: string;
|
||||||
|
walletType: 'bitcoin' | 'ethereum';
|
||||||
|
ensName?: string;
|
||||||
|
isConnected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DelegationInfo {
|
||||||
|
browserPublicKey: string;
|
||||||
|
signature: string;
|
||||||
|
expiryTimestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReOwnWalletService {
|
||||||
|
private keyDelegation: KeyDelegation;
|
||||||
|
private bitcoinAccount?: UseAppKitAccountReturn;
|
||||||
|
private ethereumAccount?: UseAppKitAccountReturn;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.keyDelegation = new KeyDelegation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set account references from AppKit hooks
|
||||||
|
*/
|
||||||
|
setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) {
|
||||||
|
this.bitcoinAccount = bitcoinAccount;
|
||||||
|
this.ethereumAccount = ethereumAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a wallet type is available and connected
|
||||||
|
*/
|
||||||
|
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
|
||||||
|
if (walletType === 'bitcoin') {
|
||||||
|
return this.bitcoinAccount?.isConnected || false;
|
||||||
|
} else {
|
||||||
|
return this.ethereumAccount?.isConnected || false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if wallet can be connected
|
||||||
|
*/
|
||||||
|
async canConnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
|
||||||
|
// For ReOwn, we assume connection is always possible if AppKit is initialized
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet connection info with ENS resolution for Ethereum
|
||||||
|
*/
|
||||||
|
async getWalletInfo(): Promise<WalletInfo | null> {
|
||||||
|
if (this.bitcoinAccount?.isConnected) {
|
||||||
|
return {
|
||||||
|
address: this.bitcoinAccount.address,
|
||||||
|
walletType: 'bitcoin',
|
||||||
|
isConnected: true
|
||||||
|
};
|
||||||
|
} else if (this.ethereumAccount?.isConnected) {
|
||||||
|
// Use Wagmi to resolve ENS name
|
||||||
|
let ensName: string | undefined;
|
||||||
|
try {
|
||||||
|
const resolvedName = await getEnsName(config, {
|
||||||
|
address: this.ethereumAccount.address as `0x${string}`
|
||||||
|
});
|
||||||
|
ensName = resolvedName || undefined;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to resolve ENS name:', error);
|
||||||
|
// Continue without ENS name
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: this.ethereumAccount.address,
|
||||||
|
walletType: 'ethereum',
|
||||||
|
ensName,
|
||||||
|
isConnected: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active wallet address
|
||||||
|
*/
|
||||||
|
getActiveAddress(): string | null {
|
||||||
|
if (this.bitcoinAccount?.isConnected) {
|
||||||
|
return this.bitcoinAccount.address;
|
||||||
|
} else if (this.ethereumAccount?.isConnected) {
|
||||||
|
return this.ethereumAccount.address;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active wallet type
|
||||||
|
*/
|
||||||
|
getActiveWalletType(): 'bitcoin' | 'ethereum' | null {
|
||||||
|
if (this.bitcoinAccount?.isConnected) {
|
||||||
|
return 'bitcoin';
|
||||||
|
} else if (this.ethereumAccount?.isConnected) {
|
||||||
|
return 'ethereum';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup key delegation for the connected wallet
|
||||||
|
*/
|
||||||
|
async setupKeyDelegation(
|
||||||
|
address: string,
|
||||||
|
walletType: 'bitcoin' | 'ethereum'
|
||||||
|
): Promise<DelegationInfo> {
|
||||||
|
// Generate browser keypair
|
||||||
|
const keypair = this.keyDelegation.generateKeypair();
|
||||||
|
|
||||||
|
// Create delegation message with chain-specific format
|
||||||
|
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
|
||||||
|
const delegationMessage = this.createDelegationMessage(
|
||||||
|
keypair.publicKey,
|
||||||
|
address,
|
||||||
|
walletType,
|
||||||
|
expiryTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the appropriate account for signing
|
||||||
|
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
|
||||||
|
if (!account?.isConnected) {
|
||||||
|
throw new Error(`${walletType} wallet is not connected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the delegation message
|
||||||
|
const signature = await this.signMessage(delegationMessage, walletType);
|
||||||
|
|
||||||
|
// Create and store delegation
|
||||||
|
const delegationInfo = this.keyDelegation.createDelegation(
|
||||||
|
address,
|
||||||
|
signature,
|
||||||
|
keypair.publicKey,
|
||||||
|
keypair.privateKey,
|
||||||
|
24
|
||||||
|
);
|
||||||
|
|
||||||
|
this.keyDelegation.storeDelegation(delegationInfo);
|
||||||
|
|
||||||
|
return {
|
||||||
|
browserPublicKey: keypair.publicKey,
|
||||||
|
signature,
|
||||||
|
expiryTimestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create chain-specific delegation message
|
||||||
|
*/
|
||||||
|
private createDelegationMessage(
|
||||||
|
browserPublicKey: string,
|
||||||
|
address: string,
|
||||||
|
walletType: 'bitcoin' | 'ethereum',
|
||||||
|
expiryTimestamp: number
|
||||||
|
): string {
|
||||||
|
const chainName = walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum';
|
||||||
|
const expiryDate = new Date(expiryTimestamp).toISOString();
|
||||||
|
|
||||||
|
return `I, ${address} (${chainName}), delegate authority to this pubkey: ${browserPublicKey} until ${expiryDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message with the appropriate wallet
|
||||||
|
*/
|
||||||
|
private async signMessage(message: string, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
|
||||||
|
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
|
||||||
|
if (!account?.isConnected) {
|
||||||
|
throw new Error(`${walletType} wallet is not connected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert message to bytes for signing
|
||||||
|
const messageBytes = new TextEncoder().encode(message);
|
||||||
|
|
||||||
|
// Sign with the appropriate wallet
|
||||||
|
const signature = await account.signMessage({ message: messageBytes });
|
||||||
|
|
||||||
|
// Return hex-encoded signature
|
||||||
|
return bytesToHex(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect wallet (handled by AppKit)
|
||||||
|
*/
|
||||||
|
async disconnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<void> {
|
||||||
|
// Clear stored delegation
|
||||||
|
this.keyDelegation.clearDelegation();
|
||||||
|
|
||||||
|
// Note: Actual disconnection is handled by AppKit's useDisconnect hook
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if delegation is valid
|
||||||
|
*/
|
||||||
|
isDelegationValid(): boolean {
|
||||||
|
return this.keyDelegation.isDelegationValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get delegation time remaining
|
||||||
|
*/
|
||||||
|
getDelegationTimeRemaining(): number {
|
||||||
|
return this.keyDelegation.getDelegationTimeRemaining();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the key delegation instance
|
||||||
|
*/
|
||||||
|
getKeyDelegation(): KeyDelegation {
|
||||||
|
return this.keyDelegation;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/lib/identity/wallets/index.ts
Normal file
2
src/lib/identity/wallets/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
|
||||||
|
export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService';
|
||||||
@ -1,34 +0,0 @@
|
|||||||
export enum WalletConnectionStatus {
|
|
||||||
Connected = 'connected',
|
|
||||||
Disconnected = 'disconnected',
|
|
||||||
NotDetected = 'not-detected',
|
|
||||||
Connecting = 'connecting'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BtcAccount {
|
|
||||||
address: string;
|
|
||||||
addressType: "p2tr" | "p2wpkh" | "p2sh" | "p2pkh";
|
|
||||||
publicKey: string;
|
|
||||||
purpose: "payment" | "ordinals";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PhantomBitcoinProvider {
|
|
||||||
isPhantom?: boolean;
|
|
||||||
signMessage?: (address: string, message: Uint8Array) => Promise<{ signature: Uint8Array }>;
|
|
||||||
connect?: () => Promise<{ publicKey: string }>;
|
|
||||||
disconnect?: () => Promise<void>;
|
|
||||||
on?: (event: string, callback: (arg: unknown) => void) => void;
|
|
||||||
off?: (event: string, callback: (arg: unknown) => void) => void;
|
|
||||||
publicKey?: string;
|
|
||||||
requestAccounts?: () => Promise<BtcAccount[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PhantomWallet {
|
|
||||||
bitcoin?: PhantomBitcoinProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
phantom?: PhantomWallet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user