Wallet Service
Gestion du portefeuille multi-devises : soldes, transactions, transferts et opérations crypto.
Overview
Le Wallet Service gère l'ensemble des opérations financières de l'utilisateur, incluant les soldes fiat et crypto, l'historique des transactions et les transferts.
Service Central
Balance
Soldes multi-devises
History
Historique complet
Transfer
Envoi & réception
Crypto
Buy/Sell/Swap
Pourquoi un Wallet Multi-Devises ?
Dans un contexte financier de plus en plus diversifié, les utilisateurs jonglent entre euros, cryptomonnaies et points de fidélité. YaniPay répond à ce besoin avec un portefeuille unifié qui simplifie la gestion financière quotidienne.
Défis résolus par le Wallet Service
- Fragmentation des actifs : Plus besoin de 5 apps différentes pour gérer euros, Bitcoin, YANI et points fidélité
- Conversion instantanée : Échangez entre devises en un clic avec des taux compétitifs
- Optimistic Updates: L'interface reflète immédiatement les actions utilisateur, même avant confirmation serveur
- Conformité SEPA : Virements européens en J+1 intégrés nativement
Performance avec React Query
staleTime de 30 secondes pour le solde total. Cela réduit les appels API tout en gardant les données fraîches. Pour les opérations critiques comme les transferts, les optimistic updates offrent une UX instantanée.Cas d'Utilisation
Découvrez comment différents profils utilisent le Wallet Service au quotidien.
Sophie
Freelance Designer
Consulte son solde multi-devises
Sophie reçoit des paiements en EUR et USDC de clients internationaux. Elle utilise le Wallet Service pour visualiser tous ses soldes en temps réel et effectuer des conversions.
Thomas
Investisseur Crypto
Achète et stake du YANI
Thomas utilise le service crypto pour acheter du YANICoin et profiter du staking avec 12% APY. Il suit la performance de son portefeuille via l'historique des transactions.
Marie
Commerçante
Gère les paiements de sa boutique
Marie utilise les transferts pour envoyer le chiffre d'affaires sur son compte bancaire. Le Wallet Service calcule automatiquement les frais et délais.
Lucas
Étudiant
Envoie de l'argent à ses amis
Lucas utilise la fonction transfert P2P pour rembourser ses amis instantanément. Les optimistic updates affichent immédiatement le nouveau solde.
Architecture
Vue d'ensemble de l'architecture du Wallet Service et ses interactions avec l'écosystème YaniPay.
Flux de données
Flux de Transfert P2P
Voici le flux complet d'un transfert entre utilisateurs, incluant les optimistic updates pour une expérience fluide.
Sécurité des Transferts
Balance Management
Récupération et gestion des soldes par devise.
import { apiClient } from './api/client';
export interface WalletBalance {
currency: string;
available: number;
pending: number;
reserved: number;
total: number;
}
export interface TotalBalance {
totalInEur: number;
totalInUsd: number;
change24h: number;
changePercent24h: number;
wallets: WalletBalance[];
}
export interface Wallet {
id: string;
currency: string;
name: string;
balance: number;
pendingBalance: number;
type: 'fiat' | 'crypto';
address?: string; // For crypto wallets
network?: string;
icon?: string;
color?: string;
valueInEur: number;
}
export const walletService = {
// Get all wallets
async getWallets(): Promise<Wallet[]> {
const { data } = await apiClient.get<Wallet[]>('/wallets');
return data;
},
// Get specific wallet
async getWallet(currency: string): Promise<Wallet> {
const { data } = await apiClient.get<Wallet>(`/wallets/${currency}`);
return data;
},
// Get main balance (EUR)
async getBalance(): Promise<WalletBalance> {
const { data } = await apiClient.get<WalletBalance>('/wallets/EUR/balance');
return data;
},
// Get total portfolio balance
async getTotalBalance(): Promise<TotalBalance> {
const { data } = await apiClient.get<TotalBalance>('/wallets/total');
return data;
},
// Get balance for specific currency
async getCurrencyBalance(currency: string): Promise<WalletBalance> {
const { data } = await apiClient.get<WalletBalance>(
`/wallets/${currency}/balance`
);
return data;
},
// Create new wallet (for crypto)
async createWallet(currency: string): Promise<Wallet> {
const { data } = await apiClient.post<Wallet>('/wallets', { currency });
return data;
},
// Get deposit address for crypto
async getDepositAddress(currency: string, network?: string): Promise<{
address: string;
network: string;
memo?: string;
}> {
const { data } = await apiClient.get(`/wallets/${currency}/deposit-address`, {
params: { network },
});
return data;
},
};Transaction History
Historique des transactions avec pagination et filtres.
export type TransactionType =
| 'SEND'
| 'RECEIVE'
| 'PAYMENT'
| 'REFUND'
| 'DEPOSIT'
| 'WITHDRAWAL'
| 'EXCHANGE'
| 'CARD_PAYMENT'
| 'FEE';
export type TransactionStatus =
| 'PENDING'
| 'PROCESSING'
| 'COMPLETED'
| 'FAILED'
| 'CANCELLED'
| 'REFUNDED';
export interface Transaction {
id: string;
type: TransactionType;
status: TransactionStatus;
amount: number;
currency: string;
amountInEur: number;
fee?: number;
description?: string;
reference?: string;
counterparty?: {
id?: string;
name: string;
avatar?: string;
};
merchant?: {
name: string;
category: string;
logo?: string;
};
metadata?: Record<string, unknown>;
createdAt: string;
completedAt?: string;
}
export interface TransactionFilters {
type?: TransactionType[];
status?: TransactionStatus[];
currency?: string;
startDate?: string;
endDate?: string;
minAmount?: number;
maxAmount?: number;
search?: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
total: number;
page: number;
limit: number;
hasMore: boolean;
};
}
export const transactionService = {
// Get transactions with pagination
async getTransactions(
filters?: TransactionFilters,
page = 1,
limit = 20
): Promise<PaginatedResponse<Transaction>> {
const { data } = await apiClient.get('/transactions', {
params: { ...filters, page, limit },
});
return data;
},
// Get recent transactions
async getRecentTransactions(limit = 5): Promise<Transaction[]> {
const { data } = await apiClient.get('/transactions/recent', {
params: { limit },
});
return data;
},
// Get single transaction
async getTransaction(id: string): Promise<Transaction> {
const { data } = await apiClient.get<Transaction>(`/transactions/${id}`);
return data;
},
// Get transactions for specific wallet
async getWalletTransactions(
currency: string,
page = 1,
limit = 20
): Promise<PaginatedResponse<Transaction>> {
const { data } = await apiClient.get(`/wallets/${currency}/transactions`, {
params: { page, limit },
});
return data;
},
// Get transaction statistics
async getStatistics(period: 'week' | 'month' | 'year'): Promise<{
totalSent: number;
totalReceived: number;
totalSpent: number;
byCategory: Record<string, number>;
byDay: Array<{ date: string; amount: number }>;
}> {
const { data } = await apiClient.get('/transactions/statistics', {
params: { period },
});
return data;
},
// Export transactions
async exportTransactions(
filters: TransactionFilters,
format: 'csv' | 'pdf'
): Promise<Blob> {
const { data } = await apiClient.get('/transactions/export', {
params: { ...filters, format },
responseType: 'blob',
});
return data;
},
};Transfers
Service de transferts entre utilisateurs et vers l'extérieur.
export interface SendMoneyDTO {
recipientId?: string;
recipientEmail?: string;
recipientPhone?: string;
amount: number;
currency: string;
note?: string;
reference?: string;
}
export interface SendMoneyResult {
transactionId: string;
status: TransactionStatus;
amount: number;
currency: string;
fee: number;
recipient: {
id: string;
name: string;
};
createdAt: string;
}
export interface WithdrawDTO {
amount: number;
currency: string;
bankAccountId: string;
reference?: string;
}
export interface BankAccount {
id: string;
bankName: string;
iban: string;
bic: string;
accountHolder: string;
isDefault: boolean;
}
export const transferService = {
// Send money to another user
async sendMoney(data: SendMoneyDTO): Promise<SendMoneyResult> {
const response = await apiClient.post<SendMoneyResult>('/transfers/send', data);
return response.data;
},
// Request money from another user
async requestMoney(data: {
recipientId: string;
amount: number;
currency: string;
note?: string;
expiresAt?: string;
}): Promise<{ requestId: string }> {
const response = await apiClient.post('/transfers/request', data);
return response.data;
},
// Get pending payment requests
async getPaymentRequests(type: 'sent' | 'received'): Promise<Array<{
id: string;
amount: number;
currency: string;
requester: { id: string; name: string };
note?: string;
status: 'PENDING' | 'PAID' | 'DECLINED' | 'EXPIRED';
expiresAt: string;
createdAt: string;
}>> {
const { data } = await apiClient.get('/transfers/requests', {
params: { type },
});
return data;
},
// Pay a payment request
async payRequest(requestId: string): Promise<SendMoneyResult> {
const { data } = await apiClient.post(`/transfers/requests/${requestId}/pay`);
return data;
},
// Decline a payment request
async declineRequest(requestId: string): Promise<void> {
await apiClient.post(`/transfers/requests/${requestId}/decline`);
},
// Get bank accounts
async getBankAccounts(): Promise<BankAccount[]> {
const { data } = await apiClient.get<BankAccount[]>('/bank-accounts');
return data;
},
// Add bank account
async addBankAccount(data: {
iban: string;
bic: string;
accountHolder: string;
isDefault?: boolean;
}): Promise<BankAccount> {
const response = await apiClient.post<BankAccount>('/bank-accounts', data);
return response.data;
},
// Withdraw to bank account
async withdraw(data: WithdrawDTO): Promise<{
transactionId: string;
estimatedArrival: string;
}> {
const response = await apiClient.post('/transfers/withdraw', data);
return response.data;
},
// Get transfer fee preview
async getFeePreview(data: {
type: 'send' | 'withdraw';
amount: number;
currency: string;
recipientId?: string;
bankAccountId?: string;
}): Promise<{
fee: number;
totalAmount: number;
exchangeRate?: number;
}> {
const { data: result } = await apiClient.post('/transfers/fee-preview', data);
return result;
},
};Crypto Operations
Achat, vente et échange de cryptomonnaies.
export interface CryptoPrice {
currency: string;
priceEur: number;
priceUsd: number;
change24h: number;
changePercent24h: number;
high24h: number;
low24h: number;
volume24h: number;
marketCap: number;
lastUpdated: string;
}
export interface SwapQuote {
id: string;
fromCurrency: string;
toCurrency: string;
fromAmount: number;
toAmount: number;
exchangeRate: number;
fee: number;
expiresAt: string;
}
export interface BuySellOrder {
id: string;
type: 'buy' | 'sell';
currency: string;
amount: number;
price: number;
total: number;
fee: number;
status: 'PENDING' | 'COMPLETED' | 'CANCELLED';
createdAt: string;
}
export const cryptoService = {
// Get crypto prices
async getPrices(currencies?: string[]): Promise<CryptoPrice[]> {
const { data } = await apiClient.get('/crypto/prices', {
params: { currencies: currencies?.join(',') },
});
return data;
},
// Get single price
async getPrice(currency: string): Promise<CryptoPrice> {
const { data } = await apiClient.get<CryptoPrice>(`/crypto/prices/${currency}`);
return data;
},
// Get price history
async getPriceHistory(
currency: string,
interval: '1h' | '24h' | '7d' | '30d' | '1y'
): Promise<Array<{ timestamp: string; price: number }>> {
const { data } = await apiClient.get(`/crypto/prices/${currency}/history`, {
params: { interval },
});
return data;
},
// Buy crypto
async buy(data: {
currency: string;
amountEur?: number;
amountCrypto?: number;
}): Promise<BuySellOrder> {
const response = await apiClient.post<BuySellOrder>('/crypto/buy', data);
return response.data;
},
// Sell crypto
async sell(data: {
currency: string;
amount: number;
}): Promise<BuySellOrder> {
const response = await apiClient.post<BuySellOrder>('/crypto/sell', data);
return response.data;
},
// Get swap quote
async getSwapQuote(data: {
fromCurrency: string;
toCurrency: string;
amount: number;
}): Promise<SwapQuote> {
const { data: quote } = await apiClient.post('/crypto/swap/quote', data);
return quote;
},
// Execute swap
async executeSwap(quoteId: string): Promise<{
transactionId: string;
fromAmount: number;
toAmount: number;
}> {
const { data } = await apiClient.post(`/crypto/swap/execute/${quoteId}`);
return data;
},
// Withdraw crypto
async withdrawCrypto(data: {
currency: string;
amount: number;
address: string;
network: string;
memo?: string;
}): Promise<{
transactionId: string;
txHash?: string;
estimatedTime: number;
}> {
const { data: result } = await apiClient.post('/crypto/withdraw', data);
return result;
},
// Get supported cryptocurrencies
async getSupportedCurrencies(): Promise<Array<{
symbol: string;
name: string;
networks: string[];
minWithdraw: number;
withdrawFee: number;
}>> {
const { data } = await apiClient.get('/crypto/currencies');
return data;
},
};React Query Hooks
Hooks React Query pour le wallet avec optimistic updates.
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { walletService, Wallet, TotalBalance } from '@/services/walletService';
import { transactionService, Transaction, TransactionFilters } from '@/services/transactionService';
import { transferService, SendMoneyDTO, SendMoneyResult } from '@/services/transferService';
// Query keys
export const walletKeys = {
all: ['wallet'] as const,
wallets: () => [...walletKeys.all, 'list'] as const,
wallet: (currency: string) => [...walletKeys.all, 'detail', currency] as const,
balance: () => [...walletKeys.all, 'balance'] as const,
totalBalance: () => [...walletKeys.all, 'total'] as const,
transactions: (filters?: TransactionFilters) =>
[...walletKeys.all, 'transactions', filters] as const,
};
// Wallet hooks
export function useWallets() {
return useQuery({
queryKey: walletKeys.wallets(),
queryFn: walletService.getWallets,
staleTime: 30 * 1000, // 30 seconds
});
}
export function useWallet(currency: string) {
return useQuery({
queryKey: walletKeys.wallet(currency),
queryFn: () => walletService.getWallet(currency),
enabled: !!currency,
});
}
export function useBalance() {
return useQuery({
queryKey: walletKeys.balance(),
queryFn: walletService.getBalance,
staleTime: 10 * 1000, // 10 seconds
refetchInterval: 60 * 1000, // Refetch every minute
});
}
export function useTotalBalance() {
return useQuery({
queryKey: walletKeys.totalBalance(),
queryFn: walletService.getTotalBalance,
staleTime: 30 * 1000,
});
}
// Transaction hooks with infinite query
export function useInfiniteTransactions(filters?: TransactionFilters) {
return useInfiniteQuery({
queryKey: walletKeys.transactions(filters),
queryFn: ({ pageParam = 1 }) =>
transactionService.getTransactions(filters, pageParam, 20),
getNextPageParam: (lastPage) =>
lastPage.pagination.hasMore ? lastPage.pagination.page + 1 : undefined,
initialPageParam: 1,
});
}
export function useRecentTransactions(limit = 5) {
return useQuery({
queryKey: [...walletKeys.transactions(), 'recent', limit] as const,
queryFn: () => transactionService.getRecentTransactions(limit),
staleTime: 30 * 1000,
});
}
// Send money mutation with optimistic update
export function useSendMoney() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: SendMoneyDTO) => transferService.sendMoney(data),
onMutate: async (data) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: walletKeys.balance() });
// Snapshot previous value
const previousBalance = queryClient.getQueryData(walletKeys.balance());
// Optimistically update balance
queryClient.setQueryData(walletKeys.balance(), (old: any) => ({
...old,
available: old.available - data.amount,
pending: old.pending + data.amount,
}));
return { previousBalance };
},
onError: (err, data, context) => {
// Rollback on error
if (context?.previousBalance) {
queryClient.setQueryData(walletKeys.balance(), context.previousBalance);
}
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: walletKeys.balance() });
queryClient.invalidateQueries({ queryKey: walletKeys.transactions() });
},
});
}Bonnes Pratiques
Références
Ressources officielles pour approfondir les concepts utilisés dans le Wallet Service.