Cairo no tiene los modificadores “internal” y “pure” (ni ningún otro modificador, en realidad) como los tiene Solidity.
Recuerda que marcar un bloque impl con #[abi(embed_v0)] le indica a Cairo que incluya sus funciones en el ABI (Application Binary Interface) del contrato, haciéndolas invocables desde fuera del contrato. Las funciones en este bloque impl deben estar definidas en un trait que este implementa (similar a las interfaces en Solidity); esto asegura que quienes llaman externamente sepan exactamente qué funciones están disponibles y cómo invocarlas.
Pero ¿qué pasa con las funciones que no deberían ser invocables externamente? Cairo es capaz de restringir lo que las funciones pueden y no pueden hacer, así como la visibilidad de sus funciones, tal como lo hace Solidity.
En este artículo, mostraremos cómo lograr el equivalente a las funciones internas, privadas y puras en Cairo.
Demostración de una función interna
Nuestra primera demostración es una función de vista interna, una que puede leer el estado del contrato pero que no es invocable desde fuera del contrato.
Para comenzar con la demostración, crea una carpeta vacía llamada internal_demo y ejecuta scarb init dentro de ella para inicializar un nuevo proyecto de Cairo.
A continuación, añade una función get_balance_2x() dentro de src/lib.cairo, como se muestra a continuación:
// IHelloStarknet INTERFACE
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// ... existing functions (increase_balance, get_balance) ...
// NEWLY ADDED FUNCTION
// Note: This function will throw error
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
Obtenemos un error de compilación porque get_balance_2x no forma parte de IHelloStarknet.

Como se indicó anteriormente, al implementar un trait en Cairo, el bloque impl solo puede contener las funciones definidas en ese trait. Un contrato puede tener múltiples bloques impl, y las funciones que no forman parte del trait deben definirse en bloques impl separados. Esto difiere de Solidity, donde los contratos pueden añadir libremente funciones más allá de las que están en la interfaz que implementan.
Sin embargo, específicamente no queremos incluir get_balance_2x en el trait IHelloStarknet porque eso haría que la función fuera pública.
La solución al error de compilación causado por incluir get_balance_2x dentro del bloque HelloStarknetImpl (sin añadirla al trait) es:
- poner
get_balance_2xen un bloqueimplseparado - hacer que ese bloque
impluse untraitseparado.
Añade el siguiente código dentro del módulo del contrato HelloStarknet, después de la implementación de HelloStarknetImpl:
// NEWLY ADDED //
#[generate_trait]
impl InternalFunction of IInternal {
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
El contrato completo se muestra a continuación con el bloque impl InternalFunction recién añadido resaltado en rojo:

- El nombre
InternalFunctiones completamente arbitrario; puede ser cualquier nombre que tenga sentido para el contrato. - Dado que cada bloque
implnecesita untraitasociado, lo nombramosIInternal(también arbitrario). - No necesitamos crear explícitamente el
traitpara elimplinterno. El compilador lo genera automáticamente con el atributo#[generate_trait].
Ahora, si intentamos acceder a get_balance_2x desde los tests (tests/test_contract.cairo),
#[test]
fn test_balance_2x() {
let contract_address = deploy_contract("HelloStarknet");
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
dispatcher.increase_balance(42);
let balance_after = dispatcher.get_balance_2x();
assert(balance_after == 42, 'Invalid balance');
}
el test no compilará, ya que esa función no es visible públicamente:

Para probar que la función interna funciona como se espera, añadiremos otra función, extern_wrap_get_balance_2x, que será pública, y luego accederemos a nuestra función interna a través de la variable self como se muestra a continuación.
No olvides que también necesitamos añadir esta función a la interfaz (como se ve en el recuadro rojo a continuación) ya que queremos que sea accesible desde fuera del contrato:

La función extern_wrap_balance_2x (recuadro azul) llama a la función interna (recuadro verde) que devuelve el doble del balance actual. Aquí está el código completo:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
/// Retrieve 2x the balance
fn extern_wrap_get_balance_2x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn extern_wrap_get_balance_2x(self: @ContractState) -> felt252 {
self.get_balance_2x()
}
}
#[generate_trait]
impl InternalFunction of IInternal {
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
Añade el siguiente test en tests/test_contract.cairo debajo de los tests existentes:
#[test]
fn test_balance_2x() {
// Deploy the HelloStarknet contract
// Note: deploy_contract is a helper function from the test setup
let contract_address = deploy_contract("HelloStarknet");
// Create a dispatcher to interact with the contract
let dispatcher = IHelloStarknetDispatcher { contract_address };
// check initial balance is 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// Increase balance by 1
dispatcher.increase_balance(1);
// Call the wrapper function that uses our internal function
// Should return 1 * 2 = 2
let balance_after_2x = dispatcher.extern_wrap_get_balance_2x();
assert(balance_after_2x == 2, 'Invalid balance');
}
Ejecuta el test con scarb test test_balance_2x; deberías ver que pasa exitosamente.
En resumen: las funciones internas en Cairo se crean definiéndolas en un bloque impl separado sin #[abi(embed_v0)] y utilizando #[generate_trait] para autogenerar el trait que el bloque impl implementa. Esto mantiene las funciones invocables dentro de tu contrato pero ocultas para quienes llaman externamente.
Funciones de vista privadas y funciones puras en Cairo
En Solidity, la diferencia entre una función “private” e “internal” es que los contratos hijos pueden ver una función “internal”, pero una función “private” solo puede ser vista por el contrato que contiene la función.
Cairo no tiene herencia, por lo que debemos tener cuidado cuando nos referimos a una función “privada” en Cairo.
Sin embargo, surge una pregunta natural: ¿es posible “modularizar” la visibilidad de las funciones? Por ejemplo, en Solidity, supongamos que tenemos la siguiente configuración:
contract A {
function private_magic_number() private returns (uint256) {
return 6;
}
function internal_mul_by_magic_number(uint256 x) internal returns (uint256) {
return x * private_magic_number()
}
}
contract B is A {
function external_fun() external returns (uint256) {
return internal_mul_by_magic_number();
}
}
El contrato B puede “ver” la función internal_mul_by_magic_number() porque hereda de A; B no puede ver private_magic_number().
Sin embargo, external_fun() en B usa private_magic_number() “tras bambalinas” cuando llama a internal_mul_by_magic_number().
Creemos una estructura idéntica en Cairo para mostrar cómo una función puede estar fuera del alcance para otras partes del código, de forma similar a como funciona una función privada en Solidity.
Uso de módulos anidados para funciones privadas
Hasta ahora, solo hemos visto mod (“módulo”) como un “contenedor” para las funciones del contrato. Sin embargo, Cairo nos permite usar módulos anidados para una mayor modularización. Podemos usar este patrón para lograr una funcionalidad similar a las funciones privadas en Solidity.
A continuación se muestra la estructura predeterminada del contrato cuando generas un nuevo proyecto Scarb (snfoundry), pero con un mod interno que contiene las funciones internal_mul_by_magic_number() y private_magic_number().
Este módulo interno se declara al final del contrato, por lo que puedes desplazarte directamente allí para ver los cambios clave:
/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn get_balance_6x(self: @ContractState) -> felt252 {
rare_library::internal_mul_by_magic_number(self.balance.read())
}
}
// ~~~~~~~~~~~~~~~~~~~~~
// ~ MOD INSERTED HERE ~
// ~~~~~~~~~~~~~~~~~~~~~
mod rare_library {
pub fn internal_mul_by_magic_number(a: felt252) -> felt252 {
a * private_magic_number()
}
fn private_magic_number() -> felt252 {
6
}
}
}
Nota que ninguna de las funciones, internal_mul_by_magic_number ni private_magic_number acceden al estado a través de @self ContractState, y por lo tanto, desde la perspectiva de Solidity, se consideran funciones puras.
También nota que internal_mul_by_magic_number() está marcada como pub pero private_magic_number() no tiene pub. Esto significa que las funciones dentro de rare_library pueden llamar a private_magic_number(), pero las funciones fuera del módulo no pueden. Dado que internal_mul_by_magic_number está marcada con pub, puede ser llamada fuera del mod.
Ejercicio: Intenta llamar a private_magic_number() desde la función get_balance(). Deberías obtener un error de compilación confirmando que la función es inaccesible fuera de su módulo.
Debido a que private_magic_number() no puede ser llamada por nada fuera del módulo rare_library, podemos considerarla una función privada.
Mover el mod a un archivo separado
Los bloques mod en línea funcionan bien para módulos pequeños, pero pueden saturar tu archivo de contrato a medida que el módulo crece. Cuando necesitas múltiples módulos, cada uno con sus propias funciones, mantener todo en el archivo de contrato principal hace que sea más difícil localizar lógica específica.
Vamos a refactorizar nuestro código para mover el módulo rare_library a un archivo separado. Esto mantiene el archivo de contrato enfocado en la lógica del contrato mientras aísla las implementaciones del módulo de la biblioteca. Continuaremos trabajando con el proyecto internal_demo de la sección anterior.
Crear un archivo de módulo separado
Dentro del directorio src/, crea un nuevo archivo llamado rare_lib.cairo y añade las siguientes funciones:
pub fn internal_mul_by_magic_number(a: felt252) -> felt252 {
a * private_magic_number()
}
fn private_magic_number() -> felt252 {
6
}
Nota que dado que estamos en un archivo separado, ya no hay necesidad de envolver las funciones con mod; el archivo en sí actúa como el módulo.
Actualizar src/lib.cairo
Ahora necesitamos actualizar src/lib.cairo para usar nuestro nuevo módulo externo. Realiza los siguientes cambios:
- Declara el módulo en la parte superior de
lib.cairo
mod rare_lib;
- Importa la función “interna” que queremos usar:
use crate::rare_lib::{internal_mul_by_magic_number};
- Añade una nueva función
get_balance_6x()a la implementación:
fn get_balance_6x(self: @ContractState) -> felt252 {
internal_mul_by_magic_number(self.balance.read())
}
- Añade la función al
traitde la interfaz (de lo contrario no compilará):
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252; // ADD THIS LINE
}
- Elimina el
moden línearare_libraryque teníamos en la sección anterior (ya que lo hemos movido a su propio archivo).
Así es como debería verse el archivo src/lib.cairo:
mod rare_lib;
/// Interface representing 'HelloContract'.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use crate::rare_lib::{internal_mul_by_magic_number};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn get_balance_6x(self: @ContractState) -> felt252 {
internal_mul_by_magic_number(self.balance.read())
}
}
}
Los cambios están resaltados a continuación:

Ahora, añade el siguiente caso de prueba al archivo de tests para ver que get_balance_6x está multiplicando el balance por el número mágico, aunque las funciones estén en un archivo separado:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use internal_demo::IHelloStarknetDispatcher;
use internal_demo::IHelloStarknetDispatcherTrait;
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
contract_address
}
#[test]
fn test_increase_balance() {
let contract_address = deploy_contract("HelloStarknet");
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
dispatcher.increase_balance(42);
let balance_after = dispatcher.get_balance();
assert(balance_after == 42, 'Invalid balance');
}
// NEWLY ADDED //
#[test]
fn test_balance_x6() {
// Deploy the HelloStarknet contract
let contract_address = deploy_contract("HelloStarknet");
// Create a dispatcher to interact with the contract
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Verify initial balance is 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// Increase balance by 1
dispatcher.increase_balance(1);
// Call get_balance_6x which uses the internal function
// Should return 1 * 6 = 6 (multiplied by the magic number)
let balance_after_6x = dispatcher.get_balance_6x();
assert(balance_after_6x == 6, 'Invalid balance');
}
Ejecuta el test:
scarb test test_balance_6x
Deberías ver que pasa exitosamente, confirmando que nuestra estructura modular refactorizada funciona correctamente.
Conclusión
En este artículo creamos:
- Una función de vista interna:
get_balance_2x()(puede leer el estado del contrato) - Una función pura interna:
internal_mul_by_magic_number()(no puede acceder al estado) - Una función pura privada:
private_magic_number()(no puede acceder al estado)
Las funciones son puras cuando no toman self: @ContractState como parámetro, lo que significa que no pueden leer ni escribir en el almacenamiento del contrato.
Nota: No creamos una función privada que pueda ver el estado. Si bien técnicamente es posible pasando self: @ContractState a funciones en módulos anidados, no es un patrón común. En la práctica, las funciones que visualizan el estado generalmente se mantienen como funciones internas (en bloques impl separados) en lugar de funciones privadas (en módulos anidados), ya que las funciones internas ya proporcionan una encapsulación suficiente para la mayoría de los casos de uso.
Resumen
- Para crear funciones internas, define un bloque
implseparado (sin#[abi(embed_v0)]) y añade el atributo#[generate_trait]. Esto genera untraitautomáticamente, manteniendo estas funciones internas al contrato. - Para crear una función pura (una que no puede acceder al estado), declara un
moddentro del contrato. Luego crea unapub fndentro delmodinterno. Esta función será accesible para elmodexterior, pero no para nada más. - Un
modpuede colocarse en otro archivo e importarse. Solo las funcionespubserán visibles desde el exterior.
Este artículo es parte de una serie de tutoriales sobre Cairo Programming on Starknet