Este tutorial introducirá el mecanismo mediante el cual los programas Anchor de Solana pueden transferir SOL como parte de la transacción.
A diferencia de Ethereum, donde las billeteras especifican msg.value como parte de la transacción y “empujan” el ETH al contrato, los programas de Solana “extraen” el SOL de la billetera.
Como tal, no existen las funciones “payable” o “msg.value”.
A continuación hemos creado un nuevo proyecto de Anchor llamado sol_splitter y hemos puesto el código en Rust para transferir SOL del remitente a un destinatario.
Por supuesto, sería más eficiente si el remitente simplemente enviara el SOL directamente en lugar de hacerlo a través de un programa, pero queremos ilustrar cómo se hace:
use anchor_lang::prelude::*;
use anchor_lang::system_program;
declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");
#[program]
pub mod sol_splitter {
use super::*;
pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
}
);
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
#[derive(Accounts)]
pub struct SendSol<'info> {
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient: UncheckedAccount<'info>,
system_program: Program<'info, System>,
#[account(mut)]
signer: Signer<'info>,
}
Hay muchas cosas que explicar aquí.
Introducción a la CPI: Invocación Cruzada de Programas (Cross Program Invocation)
En Ethereum, la transferencia de ETH se hace simplemente especificando un valor en el campo msg.value. En Solana, un programa integrado llamado system program transfiere SOL de una cuenta a otra. Es por eso que seguía apareciendo cuando inicializábamos cuentas y teníamos que pagar una tarifa para inicializarlas.
Puedes pensar en el system program más o menos como un precompilado en Ethereum. Imagina que se comporta como un token ERC-20 integrado en el protocolo que se utiliza como moneda nativa. Y tiene una función pública llamada transfer.
Context para transacciones CPI
Cada vez que se llama a una función de un programa de Solana, se debe proporcionar un Context. Ese Context contiene todas las cuentas con las que interactuará el programa.
Llamar al system program no es diferente. El system program necesita un Context que contenga las cuentas from y to. El amount que se transfiere se pasa como un argumento “regular” — no es parte del Context (ya que “amount” no es una cuenta, es solo un valor).
Ahora podemos explicar el fragmento de código a continuación:

Estamos construyendo un nuevo CpiContext que contiene el programa que vamos a llamar como primer argumento (cuadro verde), y las cuentas que se incluirán como parte de esa transacción (cuadro amarillo). El argumento amount no se proporciona aquí porque amount no es una cuenta.
Ahora que tenemos nuestro cpi_context construido, podemos hacer una invocación cruzada de programas al system program (cuadro naranja) mientras especificamos la cantidad (amount).
Esto devuelve un tipo Result<()>, al igual que lo hacen las funciones públicas en nuestros programas de Anchor.
No ignores los valores de retorno de las invocaciones cruzadas de programas.
Para verificar si la invocación cruzada de programas tuvo éxito, solo necesitamos comprobar que el valor devuelto sea un Ok. Rust hace que esto sea sencillo con el método is_ok():
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
Solo el firmante (signer) puede ser “from”
Si llamas al system program y from es una cuenta que no es un Signer, entonces el system program rechazará la llamada. Sin una firma, el system program no puede saber si autorizaste la llamada o no.
Código en Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SolSplitter } from "../target/types/sol_splitter";
describe("sol_splitter", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.SolSplitter as Program<SolSplitter>;
async function printAccountBalance(account) {
const balance = await anchor.getProvider().connection.getBalance(account);
console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
}
it("Transmit SOL", async () => {
// generate a new wallet
const recipient = anchor.web3.Keypair.generate();
await printAccountBalance(recipient.publicKey);
// send the account 1 SOL via the program
let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
await program.methods.sendSol(amount)
.accounts({recipient: recipient.publicKey})
.rpc();
await printAccountBalance(recipient.publicKey);
});
});
Algunos puntos a tener en cuenta:
- Hemos creado una función auxiliar
printAccountBalancepara mostrar el saldo antes y después - Generamos la billetera del destinatario utilizando
anchor.web3.Keypair.generate() - Transferimos un SOL a la nueva cuenta
Cuando ejecutamos el código, el resultado esperado es el siguiente. Las declaraciones de impresión muestran el saldo antes y después de la dirección del destinatario:

Ejercicio: Construye un programa de Solana que divida equitativamente el SOL entrante entre dos destinatarios. No podrás lograr esto a través de los argumentos de la función, las cuentas deben estar en el struct Context.
Construyendo un divisor de pagos: usando un número arbitrario de cuentas con remaining_accounts.
Podemos ver que sería bastante incómodo tener que especificar un struct Context de esta manera si quisiéramos dividir SOL entre varias cuentas:
#[derive(Accounts)]
pub struct SendSol<'info> {
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient1: UncheckedAccount<'info>,
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient2: UncheckedAccount<'info>,
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient3: UncheckedAccount<'info>,
// ...
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipientn: UncheckedAccount<'info>,
system_program: Program<'info, System>,
#[account(mut)]
signer: Signer<'info>,
}
Para resolver esto, Anchor añade un campo remaining_accounts a los structs Context.
El siguiente código ilustra cómo usar esa característica:
use anchor_lang::prelude::*;
use anchor_lang::system_program;
declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");
#[program]
pub mod sol_splitter {
use super::*;
// 'a, 'b, 'c are Rust lifetimes, ignore them for now
pub fn split_sol<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
amount: u64,
) -> Result<()> {
let amount_each_gets = amount / ctx.remaining_accounts.len() as u64;
let system_program = &ctx.accounts.system_program;
// note the keyword `remaining_accounts`
for recipient in ctx.remaining_accounts {
let cpi_accounts = system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: recipient.to_account_info(),
};
let cpi_program = system_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
let res = system_program::transfer(cpi_context, amount_each_gets);
if !res.is_ok() {
return err!(Errors::TransferFailed);
}
}
Ok(())
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
#[derive(Accounts)]
pub struct SplitSol<'info> {
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
Y aquí está el código en Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SolSplitter } from "../target/types/sol_splitter";
describe("sol_splitter", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.SolSplitter as Program<SolSplitter>;
async function printAccountBalance(account) {
const balance = await anchor.getProvider().connection.getBalance(account);
console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
}
it("Split SOL", async () => {
const recipient1 = anchor.web3.Keypair.generate();
const recipient2 = anchor.web3.Keypair.generate();
const recipient3 = anchor.web3.Keypair.generate();
await printAccountBalance(recipient1.publicKey);
await printAccountBalance(recipient2.publicKey);
await printAccountBalance(recipient3.publicKey);
const accountMeta1 = {pubkey: recipient1.publicKey, isWritable: true, isSigner: false};
const accountMeta2 = {pubkey: recipient2.publicKey, isWritable: true, isSigner: false};
const accountMeta3 = {pubkey: recipient3.publicKey, isWritable: true, isSigner: false};
let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
await program.methods.splitSol(amount)
.remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
.rpc();
await printAccountBalance(recipient1.publicKey);
await printAccountBalance(recipient2.publicKey);
await printAccountBalance(recipient3.publicKey);
});
});
Al ejecutar las pruebas se muestran los saldos antes y después:

Aquí hay algunos comentarios sobre el código en Rust:
Tiempos de vida (Lifetimes) en Rust
La declaración de la función de split_sol introduce una sintaxis algo extraña:
pub fn split_sol<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
amount: u64,
) -> Result<()>
Los 'a, 'b y 'c son tiempos de vida (lifetimes) de Rust. Los lifetimes en Rust son un tema complicado que preferimos evitar por ahora. Pero una explicación a alto nivel es que el código en Rust necesita garantías de que los recursos pasados al bucle for recipient in ctx.remaining_accounts existirán durante la totalidad del bucle.
ctx.remaining_accounts
El bucle itera con for recipient in ctx.remaining_accounts. La palabra clave remaining_accounts es el mecanismo de Anchor para pasar un número arbitrario de cuentas sin tener que crear un montón de claves en el struct Context.
En las pruebas de Typescript, podemos añadir remaining_accounts a la transacción de la siguiente manera:
await program.methods.splitSol(amount)
.remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
.rpc();
Aprende más con RareSkills
Consulta nuestro curso de Solana para el resto de los tutoriales de Solana.
Publicado originalmente el 2 de marzo de 2024