Introducción
En 2024, Solana surgió a la prominencia, convirtiéndose en la cuarta blockchain pública por Valor Total Bloqueado (TVL), captando el interés de inversores y desarrolladores.
BlockSec ha curado la serie "Solana Simplificado", que incluye artículos sobre los conceptos básicos de Solana, tutoriales para escribir contratos inteligentes de Solana y guías para analizar transacciones de Solana. El objetivo es ayudar a los lectores a comprender el ecosistema de Solana y dominar las habilidades esenciales para desarrollar proyectos y realizar transacciones en Solana.
En el primer artículo de esta serie, profundizamos en los conceptos clave de la red Solana, incluyendo su mecanismo de funcionamiento, modelo de cuentas y transacciones, sentando una base sólida para escribir contratos de Solana correctos y de alto rendimiento.
En este artículo, le guiaremos a través de la escritura de un programa de Solana (es decir, un contrato inteligente de Solana) para publicar y mostrar artículos. Esto ayudará a consolidar los conceptos del primer artículo e introducir algunas características de Solana que aún no hemos discutido. El programa relevante y el código de prueba han sido publicados en GitHub.
Configurar el Entorno
Nota: Los siguientes comandos solo cubren el sistema Ubuntu. Algunos comandos pueden no funcionar en sistemas Windows y macOS.
👉 Puede usar comandos alternativos o consultar Configurar el entorno de desarrollo local e instalar la CLI de Solana para resolver esto.
Compilaremos e implementaremos programas de Solana en el entorno local. Antes de adentrarnos en el desarrollo de programas de Solana, necesitamos instalar algunos comandos y dependencias.
Rust
Los programas de Solana están escritos predominantemente en el lenguaje de programación Rust, por lo que necesitamos ejecutar el siguiente comando para instalar Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
node.js & TypeScript
El script de prueba está escrito en TypeScript, por lo que necesitamos instalar Node.js y la cadena de herramientas de TypeScript:
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node
Solana CLI
A continuación, necesitamos instalar la herramienta Solana CLI, que proporciona comandos para tareas como crear billeteras e implementar contratos:
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
Ejecute el siguiente comando para aplicar los cambios al terminal actual:
source ~/.profile
Puede ejecutar solana --version para verificar que la CLI de Solana se ha instalado correctamente:
$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)
Billetera Local
A continuación, ejecute el siguiente comando para generar una billetera de sistema de archivos local:
solana-keygen new
Por defecto, este comando crea un archivo de clave privada en ~/.config/solana/id.json, que usaremos en el script de prueba más adelante. Puede usar el comando solana address para ver la dirección generada por él:
$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy
Devnet
El programa se implementará en devnet, por lo que necesitamos ejecutar el siguiente comando para cambiar la CLI de Solana a devnet:
solana config set --url https://api.devnet.solana.com
Tokens SOL
Dado que implementar contratos y enviar transacciones de prueba no es gratuito, necesitamos solicitar algunos tokens SOL. Puede ejecutar solana airdrop 2 o solicitar tokens directamente desde el faucet web público.
Escribir el Programa
El programa que vamos a introducir permite a los usuarios publicar artículos y listar todos los artículos publicados actualmente. Maneja tres tipos de instrucciones:
- instrucción init: Inicializa el programa creando una cuenta de datos para almacenar el índice máximo actual de artículos.
- instrucción post: Almacena un artículo publicado en una nueva cuenta de datos.
- instrucción list: Imprime todos los artículos publicados en el registro.
A continuación se muestra la estructura del programa:
$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
├── instructions
│ ├── init.rs
│ ├── list.rs
│ ├── mod.rs
│ └── post.rs
├── lib.rs
└── processor.rs
Cargo.toml especifica las bibliotecas externas para el proyecto:
[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"
Nota: La versión de solana-program debe coincidir con la versión de la CLI de Solana instalada (puede verificarlo usando
solana --version). De lo contrario, puede encontrar errores durante la compilación.
La función de entrada del contrato está definida en processor.rs. El archivo comienza importando las dependencias requeridas (omitiremos partes similares en otros archivos Rust):

A continuación viene la definición de la función de entrada del contrato:

En la línea 10 del programa, la macro entrypoint! de la biblioteca de Solana especifica que la función de entrada es process_instruction. Esta función acepta tres parámetros:
- program_id: La dirección implementada del programa actual.
- accounts: Todas las cuentas involucradas en la instrucción.
- instruction_data: El arreglo de bytes utilizado para procesar instrucciones.
process_instruction extrae el primer byte de instruction_data para identificar el tipo de instrucción y llama a la función correspondiente para manejarlo. A continuación, veremos cómo se implementan estas tres funciones.
init
Cada una de las tres funciones está definida en un archivo con el mismo nombre bajo el directorio program/src/instructions. Comencemos con init.rs.

Dado que la instrucción init no requiere un arreglo de bytes adicional, solo acepta dos parámetros: program_id y accounts.
Entre las líneas 12 y 15, el programa extrae la información de cuenta requerida secuencialmente, luego llama a la función find_program_address para calcular la dirección de la cuenta de datos utilizada para almacenar el índice máximo actual de artículos, index_pda_key. Luego el programa verifica que index_pda_key sea igual a la dirección de index_pda.
Aquí, la dirección de index_pda es una dirección especial llamada PDAs (Direcciones Derivadas de Programa). A diferencia de las direcciones de cuentas "billetera" generadas a partir de claves públicas, las PDAs se derivan de un arreglo de bytes opcional, un byte llamado "bump" y la dirección de un programa. El arreglo de bytes y la dirección del programa son proporcionados explícitamente por el llamador, mientras que el bump es generado y devuelto automáticamente por la función find_program_address. El bump garantiza que las direcciones creadas de esta manera no puedan colisionar con las direcciones generadas por clave pública.

Después de confirmar que la dirección de index_pda es la esperada, usamos la instrucción create_account proporcionada por el SystemProgram para crear index_pda. En la línea 22, el programa crea una variable de tipo IndexAccountData, que registra el índice máximo actual de artículos (inicializado en 0). Como se muestra en la figura a continuación, este tipo implementa los traits BorshSerialize y BorshDeserialize, lo que le permite ser serializado y deserializado en formato Borsh:

Las líneas 23 a 24 calculan el espacio requerido para almacenar los datos de la cuenta y el alquiler mínimo para crear la cuenta.
Las líneas 27 a 37 crean una instrucción create_account del SystemProgram y la invocan usando el método invoke_signed. La acción de invocar una instrucción a otro programa dentro de un programa se denomina CPI (Invocación entre Programas). En Solana, puede usar dos funciones para realizar CPI: invoke e invoke_signed.
La función invoke procesa directamente la instrucción dada, y su firma de función es la siguiente:
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>
La única diferencia entre invoke_signed e invoke es que invoke_signed proporciona una forma de firmar para cuentas PDA. Las cuentas PDA no tienen claves públicas-privadas y por lo tanto no pueden proporcionar firmas directamente. invoke_signed resuelve este problema. Esta función acepta un parámetro adicional llamado signers_seeds. Su firma de función es la siguiente.
Similar a cómo funciona la función find_program_address, invoke_signed calcula un conjunto de direcciones PDA basadas en los signers_seeds proporcionados y el program_id del llamador. Si account_infos contiene estas direcciones PDA, serán marcadas como firmantes, como si hubieran proporcionado firmas.
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>
Aquí, dado que la instrucción create_account requiere que la cuenta que se está creando sea un firmante, debemos usar invoke_signed para proporcionar una firma para index_pda.

Al final del programa, el contrato llama al método serialize en index_data para serializar los datos y escribirlos en el campo data de index_pda.
post_article

A continuación veamos la implementación de la instrucción post. A diferencia de la instrucción init, la instrucción post acepta un artículo publicado, por lo que tiene un parámetro adicional llamado instruction_data_inner, que almacena los datos serializados del artículo.
Como de costumbre, el programa comienza extrayendo la información de cuenta requerida por la instrucción. Dado que generamos una cuenta PDA separada para cada artículo, el programa acepta una cuenta adicional, new_article_pda, en la línea 23.
Similar a la instrucción init, las líneas 27 a 30 usan find_program_address para verificar que la dirección del index_pda pasado sea correcta. Luego deserializamos los datos en su campo data para leer el índice máximo actual asignado.
Las líneas 33 a 40 también verifican la dirección de new_article_pda. El programa usa la cadena "ARTICLE_PDA" y el índice actual para generar una nueva dirección. El índice se incrementa con cada carga de artículo, lo que garantiza que la dirección generada sea única.

A continuación, el programa deserializa instruction_data_inner en datos de artículo, que son de tipo PostArticleData:

Esta estructura tiene solo dos campos: title y content. En las líneas 45 a 49, el programa realiza una verificación para asegurarse de que el artículo no sea demasiado grande para estar contenido en una sola cuenta.

Los pasos siguientes también son similares a la instrucción init: El programa primero calcula el espacio requerido y el alquiler para la cuenta, luego invoca la instrucción create_account del SystemProgram para crear la cuenta PDA para almacenar el artículo. Finalmente, el artículo publicado se serializa en el campo data de new_article_pda, y el índice máximo actual se incrementa en uno y se serializa en el campo data de index_pda.
list_articles

Finalmente, veamos la implementación de la instrucción list. Dado que esta instrucción necesita listar todos los artículos, se usa un vector en la línea 15 para representar todas las cuentas PDA que almacenan los artículos, y la dirección de la cuenta index_pda también es validada.

Después de eso, el programa verifica que el tamaño del vector sea igual al índice máximo actual y comprueba la corrección de la dirección de cada cuenta.

Al final del programa, itera sobre cada cuenta y usa la macro msg! de solana-program para mostrar el contenido del artículo deserializado uno por uno.
Prueba de Transacciones
Para probar nuestro contrato, usamos un script TypeScript ubicado en client/main.ts en el repositorio.

En la parte superior del script, importamos todas las bibliotecas necesarias y definimos tres constantes globales. KEYPAIR_PATH indica la ruta al archivo de clave privada generado usando solana-keygen new, PROGRAM_ID es la dirección del programa implementado que hemos escrito en la última sección, y POST_ARTICLE_SCHEMA es el objeto utilizado para la serialización de artículos.


En el cuerpo principal del script, primero llama a la función loadKeyFromFile para analizar el archivo de clave privada y obtener un objeto Keypair como pagador de las tarifas de transacción. Luego, usa el método findProgramAddressSync proporcionado por @solana/web3.js para calcular la dirección de indexPda. Este método usa el mismo algoritmo que el contrato Rust, asegurando que compute la misma dirección con los mismos parámetros. El objeto connection especifica que usaremos devnet para las pruebas.

A continuación, enviamos la primera transacción de inicialización. Creamos una transacción que contiene una sola instrucción. El campo data de esta instrucción contiene solo un byte "0", lo que indica que esta es una invocación a la instrucción init. La transacción se envía y confirma usando sendAndConfirmTransaction.

La segunda transacción invoca la instrucción post para publicar algunos artículos. Desde las líneas 56 a 63, el script serializa dos artículos y calcula las direcciones PDA correspondientes. Luego se agregan dos instrucciones post a una sola transacción para crear estos artículos.

La transacción final invoca la instrucción list, y luego los registros se imprimen después de que la transacción es confirmada.
Para probar el programa con el script, reemplace KEYPAIR_PATH con la ruta a su clave privada (si no es la ruta predeterminada). A continuación, compile e implemente el contrato inteligente Rust ejecutando los siguientes comandos:
cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so
Luego, copie la dirección implementada y péguela en el campo PROGRAM_ID. En el directorio raíz del proyecto, ejecute npm install para instalar las dependencias, y luego ejecute npm start para ejecutar el script.
Después de eso, podemos usar Solscan para ver los detalles de ejecución de las transacciones de prueba.

Veamos directamente la segunda transacción para publicar artículos. Contiene dos instrucciones, cada una con el primer byte del Instruction Data Raw siendo 0x01. Además, cada instrucción incluye una instrucción interna, que es una invocación a la instrucción CreateAccount del System Program.

De manera similar, podemos inspeccionar la tercera transacción para listar todos los artículos. En la sección Program Logs, podemos encontrar que los índices, títulos y contenidos de los dos artículos son impresos.
Conclusión
En este artículo, primero explicamos cómo configurar el entorno de programación y ejecución de Solana localmente. Luego, detallamos la lógica de implementación de un contrato de Solana. Finalmente, probamos las funcionalidades del contrato usando un script TypeScript. Con este tutorial paso a paso, creemos que ha aprendido cómo escribir un contrato inteligente simple de Solana 🥳.
En el próximo artículo, proporcionaremos una guía detallada sobre cómo ver y analizar transacciones de Solana usando Phalcon Explorer (Phalcon Explorer ahora soporta Solana). ¡Estén atentos!



