Saltar al contenido principal

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

BlockchainLambdaAddress FormatDerivation
EthereumCreateWalletEVM0x742d35... (42 chars)secp256k1
BSCCreateWalletEVM0x742d35... (42 chars)secp256k1
PolygonCreateWalletEVM0x742d35... (42 chars)secp256k1
BitcoinCreateWalletBtcbc1q... (segwit)BIP44 m/44'/0'/0'/0/0
SolanaCreateWalletSol5tZqE... (base58, 32-44 chars)ed25519
TronCreateWalletTrxTYa3Bv... (34 chars)secp256k1
TONCreateWalletTonEQD... (48 chars)ed25519
XRPCreateWalletXrplrN7n7o... (25-35 chars)secp256k1
DogecoinCreateWalletDogD... (34 chars)BIP44
LitecoinCreateWalletLtcltc1... (segwit)BIP44
PolkadotCreateWalletPolkadot1... (47-48 chars)sr25519
CardanoCreateWalletCadanoaddr1... (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ónLímiteVentanaRazón
Crear wallet5 wallets1 horaPrevenir spam
Importar wallet3 wallets1 horaPrevenir abuso
Enviar transacción10 tx1 horaSeguridad
Consultar balance60 requests1 minutoProteger RPC
Exportar clave3 exports24 horasMáxima seguridad
Ver historial30 requests1 minutoPrevenir 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

ProblemaCausa ProbableSolución
Wallet no se creaLambda timeoutVerificar CloudWatch logs de Lambda
Balance no actualizaRPC caídoVerificar health del RPC node
Transacción pendiente 1h+Gas fee muy bajoAumentar gas price y reenviar
No puede enviarPIN incorrecto 3 vecesEsperar 15 min o resetear PIN
Balance descuadradoMonitor no detectó txForzar resync del monitor