La última vez completamos el sistema de control de riesgos de la bolsa, en esta ocasión conectamos la wallet de la bolsa a la cadena Solana. El modelo de cuentas, el almacenamiento de logs y el mecanismo de confirmación de Solana son muy diferentes de las cadenas basadas en Ethereum. Si se sigue usando el esquema de Ethereum, es muy fácil cometer errores. A continuación, repasamos la lógica general para entender Solana.
Comprendiendo la singularidad de Solana
Modelo de cuentas de Solana
Solana utiliza un modelo donde programa y datos están separados; los programas son compartidos, mientras que los datos del programa se almacenan en cuentas PDA (Program Derived Address) de forma independiente. Debido a que los programas son compartidos, se necesita Token Mint para distinguir diferentes tokens. La cuenta Token Mint almacena metadatos globales del token, como permiso de acuñación (mint_authority), suministro total (supply), decimales (decimals), etc.
Cada token tiene una dirección única de cuenta Mint que lo identifica, por ejemplo, USDC en la red principal de Solana tiene la dirección de Mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.
En Solana existen dos programas de Token: SPL Token y SPL Token-2022. Cada SPL Token tiene una ATA (Associated Token Account) independiente para guardar el saldo del usuario. Cuando se realiza una transferencia de tokens, en realidad se llama a su programa respectivo para transferir entre cuentas ATA.
Restricciones en los logs de Solana
En Ethereum, se obtiene información de transferencias analizando los logs históricos. En Solana, los logs de ejecución no se conservan permanentemente por defecto, no pertenecen al estado del libro mayor (no hay un filtro Bloom para logs), y pueden ser truncados durante la ejecución.
Por lo tanto, no podemos hacer conciliación de depósitos solo escaneando logs, sino que debemos usar getBlock o getSignaturesForAddress para analizar instrucciones.
Confirmación y reorganización en Solana
El tiempo de bloque en Solana es de aproximadamente 400ms. Con 32 confirmaciones (unos 12 segundos), se alcanza el estado finalizado (finalized). Si la precisión en tiempo real no es crítica, basta confiar en los bloques finalizados.
Para mayor precisión, hay que considerar posibles reorganizaciones de bloques, aunque son raras. La diferencia es que el consenso de Solana no depende del parentBlockHash para formar la cadena, por lo que no se puede detectar bifurcaciones comparando parentBlockHash y blockHash en la base de datos como en Ethereum. ¿Cómo detectar si un bloque ha sido reorganizado?
Al escanear localmente, debemos registrar el blockhash de cada slot. Si el blockhash de un mismo slot cambia, indica que hubo un rollback.
Entendiendo las diferencias de Solana, ahora podemos comenzar con las modificaciones en la base de datos:
Diseño de tablas en la base de datos
Dado que Solana tiene dos tipos de tokens, en la tabla tokens añadiremos un campo token_type para distinguir entre SPL Token y SPL Token-2022.
Aunque las direcciones de Solana difieren de Ethereum, ambas pueden derivarse usando BIP32 y BIP44, solo que con diferentes rutas de derivación. Por ello, podemos mantener la tabla wallets existente, pero para soportar el mapeo ATA y el seguimiento de bloques en Solana, añadiremos estas tres tablas:
| Nombre de la tabla | Campos clave | Descripción |
|---|---|---|
| solana_slots | slot, block_hash, status, parent_slot | Información redundante de slots, para detectar bifurcaciones y gestionar reversiones |
| solana_transactions | tx_hash, slot, to_addr, token_mint, amount, type | Detalles de depósitos y retiros, tx_hash único para seguimiento de doble firma |
| solana_token_accounts | wallet_id, wallet_address, token_mint, ata_address | Registro del mapeo ATA-usuario, para que el módulo de escaneo pueda consultar por ata_address |
Detalles:
Para más detalles, consultar db_gateway/database.md
Procesamiento de depósitos de usuarios
Para gestionar depósitos, se deben escanear continuamente los datos en la cadena Solana, con dos métodos principales:
Método 1: Escanear firmas del address. Se llama a getSignaturesForAddress con la dirección (que puede ser la ATA generada para el usuario o el programID). Se pasa como parámetros before, until, limit para obtener firmas incrementales. Luego, se obtiene la transacción con getTransaction usando la firma.
Este método funciona bien con pocos usuarios o cuentas, pero si hay muchas, es mejor usar el método de escaneo de bloques, que es el que implementamos aquí.
Método 2: Escanear bloques. Se obtiene el slot más reciente, se llama a getBlock para obtener detalles completos, incluyendo transacciones y cuentas, filtrando por instrucciones y cuentas relevantes.
Nota: Debido al alto volumen de transacciones y TPS en Solana, en producción puede que el análisis y filtrado no puedan seguir el ritmo de los bloques. En ese caso, se recomienda usar colas de mensajes (Kafka, RabbitMQ) para filtrar las transferencias de tokens y detectar potenciales eventos de depósito, enviándolos a los consumidores para su procesamiento y almacenamiento. Para acelerar el filtrado, algunos datos calientes se almacenan en Redis. Si hay muchos usuarios, se puede dividir por ATA para mejorar la eficiencia con múltiples consumidores.
Otra opción es usar servicios de indexadores de terceros, que ofrecen Webhook, monitoreo de cuentas y filtrado avanzado, soportando cargas de datos elevadas.
Proceso de escaneo de bloques
Utilizamos el método 2, con código en los módulos scan/solana-scan (blockScanner.ts y txParser.ts). El flujo principal:
1. Sincronización inicial y recuperación histórica (performInitialSync)
2. Escaneo en tiempo real (scanNewSlots)
3. Análisis de bloques (txParser.parseBlock)
4. Análisis de instrucciones (txParser.parseInstruction)
Gestión de reversiones:
El programa obtiene continuamente el slot finalizado (finalizedSlot). Cuando un slot es menor o igual, se marca como finalizado. Para los que aún están en confirmed, se comprueba si el blockhash ha cambiado para detectar reversiones.
Ejemplo de código clave:
// blockScanner.ts - Escaneo de un slot
async scanSingleSlot(slot: number) {
const block = await solanaClient.getBlock(slot);
if (!block) {
await insertSlot({ slot, status: 'skipped' });
return;
}
const finalizedSlot = await getCachedFinalizedSlot();
const status = slot <= finalizedSlot ? 'finalized' : 'confirmed';
await processBlock(slot, block, status);
}
// txParser.ts - Parseo de instrucciones
for (const tx of block.transactions) {
if (tx.meta?.err) continue; // Saltar transacciones fallidas
const instructions = [
...tx.transaction.message.instructions,
...(tx.meta.innerInstructions ?? []).flatMap(i => i.instructions)
];
for (const ix of instructions) {
// Transferencia SOL
if (ix.programId === SYSTEM_PROGRAM_ID && ix.parsed?.type === 'transfer') {
if (monitoredAddresses.has(ix.parsed.info.destination)) {
// Procesar depósito
}
}
// Transferencia SPL Token
if (ix.programId === TOKEN_PROGRAM_ID || ix.programId === TOKEN_2022_PROGRAM_ID) {
if (ix.parsed?.type === 'transfer' || ix.parsed?.type === 'transferChecked') {
const ataAddress = ix.parsed.info.destination;
const walletAddress = ataToWalletMap.get(ataAddress);
if (walletAddress && monitoredAddresses.has(walletAddress)) {
// Procesar depósito
}
}
}
}
}
Al detectar depósitos, se mantiene la seguridad con DB Gateway y firma doble del control de riesgos, y se registra en la tabla de fondos.
Retiro
El proceso de retiro en Solana es similar al de EVM, pero con diferencias en la construcción de transacciones:
Proceso de retiro
El flujo general:
![diagrama de flujo de retiro]
El código clave para firmar y enviar la transacción:
// Construcción de instrucciones
const instruction = getTransferSolInstruction({
source: hotWalletSigner,
destination: solanaAddressTo,
amount: BigInt(amount)
});
// Para token
const instruction = getTransferInstruction({
source: sourceAta,
destination: destAta,
authority: hotWalletSigner,
amount: BigInt(amount)
});
// Construcción y firma del mensaje
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(hotWalletSigner, tx),
tx => setTransactionMessageLifetime({ blockhash, lastValidBlockHeight }),
tx => appendTransactionMessageInstruction(instruction, tx)
);
// Firmar
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
// Codificación para envío
const signedTransaction = getBase64EncodedWireTransaction(signedTx);
Luego, se envía usando @solana/web3.js:
const solanaRpc = chainConfigManager.getSolanaRpc();
const txSignature = await solanaRpc.sendTransaction(signedTransaction, ...);
Los archivos relevantes son:
Optimización pendiente:
Resumen
Integrar Solana en la bolsa no requiere cambios en la arquitectura general, solo adaptar su modelo de cuentas, estructura de transacciones y mecanismo de confirmación.
Para depósitos, mantener un mapeo ATA-wallet, monitorizar cambios en blockhash para detectar reorganizaciones, y actualizar dinámicamente el estado del bloque (confirmed → finalized).
Para retiros, obtener el último recentBlockhash en tiempo real, distinguir entre tokens SPL y Token-2022, y construir transacciones específicas para cada caso.
Artículos relacionados
Solana mantiene un soporte clave mientras la subida de Bitcoin impulsa el mercado de criptomonedas
Solana mantiene el soporte clave mientras el rango se ajusta por debajo de $90
Implementación de firmas de seguridad cuántica en la red de pruebas de Solana; la velocidad de procesamiento de la red disminuye aproximadamente un 90%
3 Altcoins prometedores para comprar antes de que se construya el impulso — SOL, DOGE y BCH
Circle acuña más de $10B en USDC en Solana en un mes