Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/frontend/app/contexts/WalletContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client';

import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { WalletType, WalletConnection, WalletServiceFactory } from '../services/wallet';

interface WalletContextType {
wallet: WalletConnection | null;
isConnecting: boolean;
error: string | null;
connect: (walletType: WalletType) => Promise<void>;
disconnect: () => void;
signTransaction: (xdr: string) => Promise<string>;
getAvailableWallets: () => Promise<WalletType[]>;
}

const WalletContext = createContext<WalletContextType | undefined>(undefined);

interface WalletProviderProps {
children: ReactNode;
}

export const WalletProvider: React.FC<WalletProviderProps> = ({ children }) => {
const [wallet, setWallet] = useState<WalletConnection | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);

// Load wallet from localStorage on mount
useEffect(() => {
const savedWallet = localStorage.getItem('vaultix_wallet');
if (savedWallet) {
try {
const parsedWallet = JSON.parse(savedWallet);
setWallet(parsedWallet);
} catch {
localStorage.removeItem('vaultix_wallet');
}
}
}, []);

const connect = async (walletType: WalletType) => {
setIsConnecting(true);
setError(null);

try {
const service = WalletServiceFactory.getService(walletType);
const publicKey = await service.connect();

let network = 'testnet'; // Default
if (walletType === WalletType.FREIGHTER) {
network = await service.getNetwork?.() || 'testnet';
} else if (walletType === WalletType.ALBEDO) {
network = process.env.NEXT_PUBLIC_STELLAR_NETWORK || 'testnet';
}

const walletConnection: WalletConnection = {
publicKey,
walletType,
network,
};

setWallet(walletConnection);
localStorage.setItem('vaultix_wallet', JSON.stringify(walletConnection));
} catch (err: any) {
setError(err.message || 'Failed to connect wallet');
throw err;
} finally {
setIsConnecting(false);
}
};

const disconnect = () => {
setWallet(null);
localStorage.removeItem('vaultix_wallet');
setError(null);
};

const signTransaction = async (xdr: string): Promise<string> => {
if (!wallet) {
throw new Error('No wallet connected');
}

const service = WalletServiceFactory.getService(wallet.walletType);
return await service.signTransaction(xdr);
};

const getAvailableWallets = async (): Promise<WalletType[]> => {
return await WalletServiceFactory.getAvailableWallets();
};

return (
<WalletContext.Provider
value={{
wallet,
isConnecting,
error,
connect,
disconnect,
signTransaction,
getAvailableWallets,
}}
>
{children}
</WalletContext.Provider>
);
};

export const useWallet = () => {
const context = useContext(WalletContext);
if (context === undefined) {
throw new Error('useWallet must be used within a WalletProvider');
}
return context;
};
19 changes: 19 additions & 0 deletions apps/frontend/app/hooks/useWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useWallet as useWalletContext } from "../contexts/WalletContext";



export const useWalletConnection = () => {
const { wallet, isConnecting, error, connect, disconnect } = useWalletContext();

return {
isConnected: !!wallet,
wallet,
isConnecting,
error,
connect,
disconnect,
publicKey: wallet?.publicKey,
walletType: wallet?.walletType,
network: wallet?.network,
};
};
58 changes: 58 additions & 0 deletions apps/frontend/app/services/wallet/albedo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Import albedo correctly


export interface AlbedoIntentResult {
signed_envelope_xdr?: string;
pubkey?: string;
signature?: string;
}

export class AlbedoService {
private static instance: AlbedoService;

private constructor() {}

public static getInstance(): AlbedoService {
if (!AlbedoService.instance) {
AlbedoService.instance = new AlbedoService();
}
return AlbedoService.instance;
}

async connect(): Promise<string> {
try {
const result = await this.publicKey()
return result;
} catch (error: any) {
throw new Error(`Failed to connect with Albedo: ${error.message}`);
}
}

async signTransaction(xdr: string): Promise<string> {
try {
const result = await "await albedo.signTransaction({xdr, network: this.getNetworkParam()});"

if (!result) {
throw new Error('No signed envelope returned from Albedo');
}

return result;
} catch (error: any) {
throw new Error(`Failed to sign transaction with Albedo: ${error.message}`);
}
}

private getNetworkParam(): string {
const network = process.env.NEXT_PUBLIC_STELLAR_NETWORK || 'testnet';
return network === 'mainnet' ? 'public' : 'testnet';
}

private async publicKey(): Promise<string> {
try {
const result = "await albedo.publicKey({});"
return result;
} catch (error: any) {
throw new Error(`Failed to get public key from Albedo: ${error.message}`);
}
}
}
91 changes: 91 additions & 0 deletions apps/frontend/app/services/wallet/freighter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Update the import - use the correct way to access freighter
declare global {
interface Window {
freighter?: any;
}
}

export interface FreighterWallet {
isConnected: () => Promise<boolean>;
getPublicKey: () => Promise<string>;
signTransaction: (xdr: string, opts?: any) => Promise<string>;
getNetwork: () => Promise<string>;
}

export class FreighterService {
private static instance: FreighterService;

private constructor() {}

public static getInstance(): FreighterService {
if (!FreighterService.instance) {
FreighterService.instance = new FreighterService();
}
return FreighterService.instance;
}

private async getFreighter(): Promise<any> {
// Check if freighter is available
if (typeof window === 'undefined') {
throw new Error('Window is not defined');
}

// Freighter injects itself into the window object
if (!window.freighter) {
throw new Error('Freighter wallet is not installed');
}

return window.freighter;
}

async isInstalled(): Promise<boolean> {
try {
if (typeof window === 'undefined') return false;
return !!window.freighter;
} catch (error) {
return false;
}
}

async connect(): Promise<string> {
try {
const freighter = await this.getFreighter();

// Enable freighter
await freighter.enable();

// Get public key
const publicKey = await freighter.getPublicKey();

return publicKey;
} catch (error: any) {
throw new Error(`Failed to connect to Freighter: ${error.message}`);
}
}

async getNetwork(): Promise<string> {
try {
const freighter = await this.getFreighter();
const network = await freighter.getNetwork();
return network.toLowerCase(); // Convert to lowercase for consistency
} catch (error) {
throw new Error('Failed to get network from Freighter');
}
}

async signTransaction(xdr: string): Promise<string> {
try {
const freighter = await this.getFreighter();
const network = await this.getNetwork();

const signedXdr = await freighter.signTransaction(xdr, {
network,
accountToSign: await freighter.getPublicKey(),
});

return signedXdr;
} catch (error: any) {
throw new Error(`Failed to sign transaction: ${error.message}`);
}
}
}
53 changes: 53 additions & 0 deletions apps/frontend/app/services/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FreighterService } from './freighter';
import { AlbedoService } from './albedo';

export enum WalletType {
FREIGHTER = 'freighter',
ALBEDO = 'albedo',
}

export interface WalletConnection {
publicKey: string;
walletType: WalletType;
network: string;
}

export interface IWalletService {
connect(): Promise<string>;
signTransaction(xdr: string): Promise<string>;
getNetwork?(): Promise<string>;
isInstalled?(): Promise<boolean>;
disconnect?(): Promise<void>;
}

export class WalletServiceFactory {
static getService(walletType: WalletType): IWalletService {
switch (walletType) {
case WalletType.FREIGHTER:
return FreighterService.getInstance();
case WalletType.ALBEDO:
return AlbedoService.getInstance();
default:
throw new Error(`Unsupported wallet type: ${walletType}`);
}
}

static async getAvailableWallets(): Promise<WalletType[]> {
const availableWallets: WalletType[] = [];

// Check for Freighter
const freighterService = FreighterService.getInstance();
try {
if (await freighterService.isInstalled?.()) {
availableWallets.push(WalletType.FREIGHTER);
}
} catch {
// Freighter not available
}

// Albedo is always available as it's a web-based wallet
availableWallets.push(WalletType.ALBEDO);

return availableWallets;
}
}
Loading