Operaciones de Wallet
Este documento explica todos los flujos técnicos relacionados con la gestión de wallets en SwapBits.
Vista General de Operaciones
1. Crear Wallet (Multi-Chain)
Arquitectura de Creación
Datos por Blockchain
| Blockchain | Lambda | Address Format | Derivation |
|---|---|---|---|
| Ethereum | CreateWalletEVM | 0x742d35... (42 chars) | secp256k1 |
| BSC | CreateWalletEVM | 0x742d35... (42 chars) | secp256k1 |
| Polygon | CreateWalletEVM | 0x742d35... (42 chars) | secp256k1 |
| Bitcoin | CreateWalletBtc | bc1q... (segwit) | BIP44 m/44'/0'/0'/0/0 |
| Solana | CreateWalletSol | 5tZqE... (base58, 32-44 chars) | ed25519 |
| Tron | CreateWalletTrx | TYa3Bv... (34 chars) | secp256k1 |
| TON | CreateWalletTon | EQD... (48 chars) | ed25519 |
| XRP | CreateWalletXrpl | rN7n7o... (25-35 chars) | secp256k1 |
| Dogecoin | CreateWalletDog | D... (34 chars) | BIP44 |
| Litecoin | CreateWalletLtc | ltc1... (segwit) | BIP44 |
| Polkadot | CreateWalletPolkadot | 1... (47-48 chars) | sr25519 |
| Cardano | CreateWalletCadano | addr1... (103 chars) | BIP32-Ed25519 |
Wallet Guardada en MongoDB
{
_id: ObjectId("..."),
userId: ObjectId("user_123"),
address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
coin: "ETH",
network: "ethereum",
type: "EVM",
privateKeyEncrypted: "U2FsdGVkX1...", // AES-256 encrypted
publicKey: "0x04a8b...",
balance: 0,
balanceUSD: 0,
isActive: true,
isImported: false,
label: "Mi wallet ETH principal",
createdAt: ISODate("2025-10-20T10:00:00Z"),
updatedAt: ISODate("2025-10-20T10:00:00Z")
}
2. Importar Wallet Existente
Flujo de Importación
Validaciones de Importación
// 1. Validar formato de private key
if (type === 'EVM') {
// 64 caracteres hex sin 0x, o 66 con 0x
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new BadRequestException('Invalid EVM private key');
}
}
if (type === 'BTC') {
// WIF format (Wallet Import Format)
if (!/^[5KL][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(privateKey)) {
throw new BadRequestException('Invalid Bitcoin private key');
}
}
// 2. Derivar address y verificar que coincida
const derivedAddress = deriveAddressFromKey(privateKey, type);
// 3. Verificar que no esté ya importada
const existing = await this.walletsRepo.findOne({ address: derivedAddress });
if (existing) {
throw new ConflictException('Wallet already exists');
}
// 4. Verificar ownership (opcional): firmar mensaje
const signature = signMessage("SwapBits verification", privateKey);
if (!verifySignature(signature, derivedAddress)) {
throw new UnauthorizedException('Invalid signature');
}
3. Consultar Balance
Flujo de Actualización de Balance
Cálculo de Balance USD
// Pseudocódigo de cálculo
async calculateBalanceUSD(wallet: Wallet): number {
let totalUSD = 0;
// 1. Balance nativo
const nativePrice = await this.priceService.getPrice(wallet.coin);
totalUSD += wallet.balance * nativePrice;
// 2. Tokens (solo EVM)
if (wallet.type === 'EVM') {
const tokens = await this.getTokenBalances(wallet.address);
for (const token of tokens) {
const tokenPrice = await this.priceService.getPrice(token.symbol);
totalUSD += token.balance * tokenPrice;
}
}
return totalUSD;
}
4. Enviar Transacción
Flujo Completo de Envío
Validaciones Pre-Envío
// Validaciones críticas antes de enviar
async validateTransaction(dto: SendTransactionDto, user: User, wallet: Wallet) {
// 1. Balance suficiente (incluyendo fee)
const fee = await this.estimateFee(wallet.coin, dto.to, dto.amount);
if (wallet.balance < dto.amount + fee) {
throw new BadRequestException('Insufficient balance including fee');
}
// 2. Monto dentro de límites
const limits = this.getLimits(wallet.coin);
if (dto.amount < limits.min || dto.amount > limits.max) {
throw new BadRequestException('Amount out of bounds');
}
// 3. Rate limiting por usuario y por wallet
await this.rateLimit.check(`send:user:${user._id}`, 10, 3600000); // 10/hora
await this.rateLimit.check(`send:wallet:${wallet._id}`, 5, 3600000); // 5/hora
// 4. Verificar address destino válida
if (!this.isValidAddress(dto.to, wallet.type)) {
throw new BadRequestException('Invalid destination address');
}
// 5. No permitir envío a mismo usuario
const destinationWallet = await this.walletsRepo.findOne({ address: dto.to });
if (destinationWallet?.userId.equals(user._id)) {
throw new BadRequestException('Cannot send to your own wallet');
}
// 6. KYC requerido para montos grandes
const usdValue = dto.amount * await this.priceService.getPrice(wallet.coin);
if (usdValue > 1000 && user.kycStatus !== 'approved') {
throw new ForbiddenException('KYC required for transactions over $1000');
}
// 7. Verificar que wallet no esté bloqueada
if (wallet.isLocked) {
throw new ForbiddenException('Wallet is locked');
}
// 8. Anti-fraude: detectar patrones sospechosos
await this.fraudService.analyze(user, wallet, dto);
}
5. Recibir Fondos (Generación QR)
Flujo de Recepción
Formatos de URI por Blockchain
// EVM (Ethereum, BSC, Polygon)
ethereum:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb?value=1000000000000000000
// Bitcoin
bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh?amount=0.01
// Solana
solana:5tZqE9vXQgNMKPRf8R8xyv7kPQx9MXg8dB9ZGC6hAXJ?amount=1
// Tron
tron:TYa3BvJmKmqNQqL4nYoKQFJNJq9nqzQk8o?amount=100
// Bitcoin Cash
bitcoincash:qr2gp5x8hqqzxjpg0j0s7r8q7p5x8hqqzxjpg0j0s7r?amount=0.5
6. Historial de Transacciones
Consulta de Historial
Filtros Disponibles
// Query params soportados
interface TransactionFilters {
page: number; // Página actual
limit: number; // Items por página (max 100)
type?: 'sent' | 'received'; // Filtrar por tipo
status?: 'pending' | 'confirmed' | 'failed'; // Filtrar por estado
minAmount?: number; // Monto mínimo
maxAmount?: number; // Monto máximo
startDate?: Date; // Desde fecha
endDate?: Date; // Hasta fecha
search?: string; // Buscar en txHash o address
}
7. Exportar Clave Privada
Flujo de Exportación (Crítico)
Advertencias de Seguridad
// Advertencias mostradas al usuario ANTES de exportar
const warnings = [
"⚠️ Nunca compartas tu clave privada con nadie",
"⚠️ SwapBits NUNCA te pedirá tu clave privada",
"⚠️ Cualquiera con tu clave privada puede robar tus fondos",
"⚠️ Guárdala en un lugar seguro offline",
"⚠️ Esta acción quedará registrada en tu historial de seguridad"
];
// Rate limiting estricto
const exportLimit = {
maxAttempts: 3,
windowMs: 86400000, // 24 horas
message: "Por seguridad, solo puedes exportar 3 claves por día"
};
// Auditoría obligatoria
await this.auditLog.create({
userId: user._id,
action: 'EXPORT_PRIVATE_KEY',
walletId: wallet._id,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date()
});
8. Eliminar Wallet
Flujo de Eliminación
Validaciones de Eliminación
async deleteWallet(walletId: string, user: User) {
const wallet = await this.walletsRepo.findOne({ _id: walletId, userId: user._id });
// 1. Verificar balance cero
if (wallet.balance > 0) {
throw new BadRequestException('Cannot delete wallet with balance. Transfer funds first.');
}
// 2. Verificar no hay transacciones pendientes
const pendingTxs = await this.transactionsRepo.count({
walletId,
status: 'pending'
});
if (pendingTxs > 0) {
throw new BadRequestException('Cannot delete wallet with pending transactions');
}
// 3. Soft delete (mantener para auditoría)
await this.walletsRepo.update(walletId, {
isActive: false,
deletedAt: new Date()
});
// 4. Dejar de monitorear en blockchain monitor
await this.monitorService.unsubscribe(wallet.address);
return { message: 'Wallet deleted successfully' };
}
Rate Limiting por Operación
| Operación | Límite | Ventana | Razón |
|---|---|---|---|
| Crear wallet | 5 wallets | 1 hora | Prevenir spam |
| Importar wallet | 3 wallets | 1 hora | Prevenir abuso |
| Enviar transacción | 10 tx | 1 hora | Seguridad |
| Consultar balance | 60 requests | 1 minuto | Proteger RPC |
| Exportar clave | 3 exports | 24 horas | Máxima seguridad |
| Ver historial | 30 requests | 1 minuto | Prevenir scraping |
Estados de Wallet
interface WalletStates {
// Estado activo normal
active: {
isActive: true,
isLocked: false,
balance: number
};
// Wallet bloqueada temporalmente
locked: {
isActive: true,
isLocked: true,
lockReason: 'suspicious_activity' | 'user_request',
lockedUntil: Date
};
// Wallet eliminada (soft delete)
deleted: {
isActive: false,
deletedAt: Date,
deletedBy: ObjectId
};
// Wallet con balance pendiente
pending: {
isActive: true,
hasPendingTransactions: true,
lockedBalance: number // Balance bloqueado en tx pendientes
};
}
Seguridad de Claves Privadas
Encriptación en Reposo
// Proceso de encriptación (pseudocódigo)
function encryptPrivateKey(privateKey: string, userId: string): string {
// 1. Obtener master key desde AWS Secrets Manager
const masterKey = await secretsManager.getSecret('WALLET_MASTER_KEY');
// 2. Derivar key específica del usuario
const userSalt = crypto.createHash('sha256').update(userId).digest();
const userKey = crypto.pbkdf2Sync(masterKey, userSalt, 100000, 32, 'sha256');
// 3. Encriptar con AES-256-GCM
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', userKey, iv);
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// 4. Combinar IV + AuthTag + Encrypted
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}
Desencriptación (Solo en Lambda)
// Proceso de desencriptación (pseudocódigo)
// IMPORTANTE: Esto SOLO se ejecuta dentro de AWS Lambda
function decryptPrivateKey(encryptedKey: string, userId: string): string {
const [ivHex, authTagHex, encrypted] = encryptedKey.split(':');
const masterKey = await secretsManager.getSecret('WALLET_MASTER_KEY');
const userSalt = crypto.createHash('sha256').update(userId).digest();
const userKey = crypto.pbkdf2Sync(masterKey, userSalt, 100000, 32, 'sha256');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', userKey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Seguridad Crítica
Las claves privadas NUNCA deben:
- ❌ Guardarse sin encriptar
- ❌ Enviarse por logs
- ❌ Mostrarse en respuestas API (excepto export explícito)
- ❌ Guardarse en Redis o cache
- ❌ Desencriptarse en el backend (solo en Lambda)
Las claves privadas SIEMPRE deben:
- ✅ Encriptarse con AES-256-GCM antes de guardar
- ✅ Desencriptarse solo en el momento del envío (Lambda)
- ✅ Limpiarse de memoria inmediatamente después de usar
- ✅ Auditarse cada acceso/exportación
- ✅ Protegerse con PIN del usuario
Monitoreo de Wallets
Métricas Clave
// Métricas importantes para monitorear
interface WalletMetrics {
totalWallets: number; // Total de wallets activas
walletsCreatedToday: number; // Nuevas wallets hoy
totalBalanceUSD: number; // Balance total en USD
averageBalancePerWallet: number; // Promedio por wallet
transactionsLast24h: number; // Transacciones últimas 24h
pendingTransactions: number; // Transacciones pendientes
failedTransactionsToday: number; // Transacciones fallidas hoy
topBlockchains: Array<{ // Blockchains más usadas
blockchain: string;
count: number;
percentage: number;
}>;
}
Troubleshooting Común
| Problema | Causa Probable | Solución |
|---|---|---|
| Wallet no se crea | Lambda timeout | Verificar CloudWatch logs de Lambda |
| Balance no actualiza | RPC caído | Verificar health del RPC node |
| Transacción pendiente 1h+ | Gas fee muy bajo | Aumentar gas price y reenviar |
| No puede enviar | PIN incorrecto 3 veces | Esperar 15 min o resetear PIN |
| Balance descuadrado | Monitor no detectó tx | Forzar resync del monitor |