Saltar al contenido principal

Desplegador

El ejemplo del desplegador demuestra cómo desplegar contratos utilizando un contrato.

Aquí desplegamos un contrato en nombre de cualquier dirección e inicializarlo atómicamente.

Abrir en Gitpod

información

En este ejemplo hay dos contratos que se compilan por separado, y las pruebas despliegan uno con el otro.

Ejecutar el Ejemplo

Primero pasa por el proceso de Configuración para configurar tu entorno de desarrollo, luego clona la etiqueta v21.6.0 del repositorio soroban-examples:

git clone -b v21.6.0 https://github.com/stellar/soroban-examples

O salta la configuración del entorno de desarrollo y abre este ejemplo en Gitpod.

Para ejecutar las pruebas del ejemplo, navega hasta el directorio deployer/deployer, y utiliza cargo test.

cd deployer/deployer
cargo test

Deberías ver la salida:

running 1 test
test test::test ... ok

Código

deployer/deployer/src/lib.rs
#[contract]
pub struct Deployer;

#[contractimpl]
impl Deployer {
/// Deploy the contract Wasm and after deployment invoke the init function
/// of the contract with the given arguments.
///
/// This has to be authorized by `deployer` (unless the `Deployer` instance
/// itself is used as deployer). This way the whole operation is atomic
/// and it's not possible to frontrun the contract initialization.
///
/// Returns the contract address and result of the init function.
pub fn deploy(
env: Env,
deployer: Address,
wasm_hash: BytesN<32>,
salt: BytesN<32>,
init_fn: Symbol,
init_args: Vec<Val>,
) -> (Address, Val) {
// Skip authorization if deployer is the current contract.
if deployer != env.current_contract_address() {
deployer.require_auth();
}

// Deploy the contract using the uploaded Wasm with given hash.
let deployed_address = env
.deployer()
.with_address(deployer, salt)
.deploy(wasm_hash);

// Invoke the init function with the given arguments.
let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args);
// Return the contract ID of the deployed contract and the result of
// invoking the init result.
(deployed_address, res)
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v21.6.0/deployer

Cómo Funciona

Los contratos pueden desplegar otros contratos utilizando el método deployer() del SDK.

La dirección del contrato del contrato desplegado es determinista y se deriva de la dirección del desplegador. El despliegue también debe ser autorizado por el desplegador.

Abre el archivo deployer/deployer/src/lib.rs para seguir adelante.

Carga de Wasm del Contrato

Antes de desplegar las nuevas instancias de contrato, el código Wasm necesita ser cargado en la cadena. Luego se puede usar para desplegar un número arbitrario de instancias de contrato. La carga debería ocurrir típicamente fuera del contrato desplegador, ya que necesita suceder solo una vez. Sin embargo, es posible utilizar la función env.deployer().upload_contract_wasm() para cargar Wasm desde un contrato también.

Consulta las pruebas para un ejemplo de carga del código del contrato de forma programática. Para la instalación en cadena real, consulta el tutorial de despliegue general.

Autorización

información

Esta sección puede ser omitida para contratos de fábrica que desplieguen otro contrato desde su propia dirección (deployer == env.current_contract_address()).

información

Para una introducción a la autorización de Soroban, consulta el tutorial de autenticación.

Comenzamos verificando la autorización del desplegador, a menos que sea el contrato actual (en cuyo caso la autorización se implica).

if deployer != env.current_contract_address() {
deployer.require_auth();
}

Mientras deployer().with_address() también realiza la autorización, queremos asegurarnos de que deployer también ha autorizado toda la operación, ya que además del despliegue también realiza la inicialización atómica del contrato. Si no requiriéramos autorización del desplegador aquí, entonces sería posible adelantar la operación de despliegue realizada por deployer e inicializarlo de manera diferente, rompiendo así la promesa de inicialización atómica.

Consulta más detalles sobre las cargas de autorización reales en pruebas.

deployer()

La función SDK deployer() viene con algunas utilidades relacionadas con el despliegue. Aquí usamos el tipo de desplegador más genérico, with_address(deployer_address, salt).

let deployed_address = env
.deployer()
.with_address(deployer, salt)
.deploy(wasm_hash);

with_address() acepta la dirección del deployer y sal. Ambos se utilizan para derivar la dirección del contrato desplegado de manera determinista. No es posible volver a desplegar un contrato ya existente.

La función deploy() realiza el despliegue real utilizando el wasm_hash proporcionado. La implementación del nuevo contrato se define por el archivo Wasm cargado bajo wasm_hash.

consejo

Solo el wasm_hash en sí se almacena por ID de contrato, lo que ahorra espacio en el ledger y tarifas.

Cuando solo se despliega el contrato en nombre del contrato actual, es decir, cuando la dirección del deployer es siempre env.current_contract_address(), es posible utilizar la función deployer().with_current_contract(salt) para brevedad.

Inicialización

El contrato puede ser llamado inmediatamente después del despliegue, lo cual es útil para la inicialización.

let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args);

invoke_contract puede llamar cualquier función de contrato definida con cualquier argumento. Pasamos la función real a llamar y los argumentos de las entradas de deploy. El resultado puede ser cualquier valor, dependiendo del valor de retorno de init_fn.

Si la inicialización falla, entonces toda la llamada a deploy falla y, por lo tanto, el contrato no se desplegará. Este comportamiento es requerido también para la garantía de inicialización atómica.

El contrato devuelve la dirección del contrato desplegado y el resultado de ejecutar la función de inicialización.

 (deployed_address, res)

Pruebas

Abre el archivo deployer/deployer/src/test.rs para seguir adelante.

Importa el Wasm del contrato de prueba a desplegar.

// The contract that will be deployed by the deployer contract.
mod contract {
soroban_sdk::contractimport!(
file =
"../contract/target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm"
);
}

Ese contrato contiene el siguiente código que exporta dos funciones: la función de inicialización que toma un valor y una función de obtención para el valor inicializado almacenado.

deployer/contract/src/lib.rs
#[contract]
pub struct Contract;

const KEY: Symbol = symbol_short!("value");

#[contractimpl]
impl Contract {
pub fn init(env: Env, value: u32) {
env.storage().instance().set(&KEY, &value);
}
pub fn value(env: Env) -> u32 {
env.storage().instance().get(&KEY).unwrap()
}
}

Este contrato de prueba será utilizado al probar el desplegador. El contrato desplegador desplegará el contrato de prueba e invocará su función init.

Hay dos pruebas: despliegue desde el contrato actual sin autorización y despliegue desde una dirección arbitraria con autorización. Además de la autorización, estas pruebas son muy similares.

Desplegador del contrato actual

En la primera prueba desplegamos el contrato desde la instancia del contrato Desplegador.

#[test]
fn test_deploy_from_contract() {
let env = Env::default();
let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer));

// Upload the Wasm to be deployed from the deployer contract.
// This can also be called from within a contract if needed.
let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM);

// Deploy contract using deployer, and include an init function to call.
let salt = BytesN::from_array(&env, &[0; 32]);
let init_fn = symbol_short!("init");
let init_fn_args: Vec<Val> = (5u32,).into_val(&env);
let (contract_id, init_result) = deployer_client.deploy(
&deployer_client.address,
&wasm_hash,
&salt,
&init_fn,
&init_fn_args,
);

assert!(init_result.is_void());
// No authorizations needed - the contract acts as a factory.
assert_eq!(env.auths(), vec![]);

// Invoke contract to check that it is initialized.
let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);
}

En cualquier prueba, lo primero que siempre se requiere es un Env, que es el entorno Soroban en el que se ejecutará el contrato.

let env = Env::default();

Registra el contrato desplegador con el entorno y crea un cliente para él.

let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer));

Carga el código del contrato de prueba que hemos importado anteriormente a través de contractimport! y obtiene el hash del código Wasm subido.

let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM);

El cliente se usa para invocar la función deploy. El contrato desplegará el contrato de prueba utilizando el hash de su código Wasm, llamará a la función init, y pasará un único argumento 5u32. El valor de retorno esperado de la función init es solo void (es decir, ningún valor).

let salt = BytesN::from_array(&env, &[0; 32]);
let init_fn = symbol_short!("init");
let init_fn_args: Vec<Val> = (5u32,).into_val(&env);
let (contract_id, init_result) = deployer_client.deploy(
&deployer_client.address,
&wasm_hash,
&salt,
&init_fn,
&init_fn_args,
);

La prueba verifica que el contrato de prueba fue desplegado utilizando su cliente para invocarlo y obtener de regreso el valor establecido durante la inicialización.

let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);

Desplegador externo

La segunda prueba es muy similar a la primera.

#[test]
fn test_deploy_from_address() {
let env = Env::default();
let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer));

// Upload the Wasm to be deployed from the deployer contract.
// This can also be called from within a contract if needed.
let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM);

// Define a deployer address that needs to authorize the deployment.
let deployer = Address::random(&env);

// Deploy contract using deployer, and include an init function to call.
let salt = BytesN::from_array(&env, &[0; 32]);
let init_fn = symbol_short!("init");
let init_fn_args: Vec<Val> = (5u32,).into_val(&env);
env.mock_all_auths();
let (contract_id, init_result) =
deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args);

assert!(init_result.is_void());

let expected_auth = AuthorizedInvocation {
// Top-level authorized function is `deploy` with all the arguments.
function: AuthorizedFunction::Contract((
deployer_client.address,
symbol_short!("deploy"),
(
deployer.clone(),
wasm_hash.clone(),
salt,
init_fn,
init_fn_args,
)
.into_val(&env),
)),
// From `deploy` function the 'create contract' host function has to be
// authorized.
sub_invocations: vec![AuthorizedInvocation {
function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs {
contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
address: deployer.clone().try_into().unwrap(),
salt: Uint256([0; 32]),
}),
executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))),
}),
sub_invocations: vec![],
}],
};
assert_eq!(env.auths(), vec![(deployer, expected_auth)]);

// Invoke contract to check that it is initialized.
let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);
}

La principal diferencia es que el contrato se despliega en nombre de la dirección arbitraria.

// Define a deployer address that needs to authorize the deployment.
let deployer = Address::random(&env);

Antes de invocar el contrato, necesitamos habilitar la autorización simulada para obtener la carga de autorización registrada que podemos verificar.

env.mock_all_auths();
let (contract_id, init_result) =
deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args);

El árbol de autorización esperado para el deployer se ve como sigue.

let expected_auth = AuthorizedInvocation {
// Top-level authorized function is `deploy` with all the arguments.
function: AuthorizedFunction::Contract((
deployer_client.address,
symbol_short!("deploy"),
(
deployer.clone(),
wasm_hash.clone(),
salt,
init_fn,
init_fn_args,
)
.into_val(&env),
)),
// From `deploy` function the 'create contract' host function has to be
// authorized.
sub_invocations: vec![AuthorizedInvocation {
function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs {
contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
address: deployer.clone().try_into().unwrap(),
salt: Uint256([0; 32]),
}),
executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))),
}),
sub_invocations: vec![],
}],
};

A nivel superior tenemos la función deploy en sí con todos los argumentos que hemos pasado a ella. Desde la función deploy, la función de host 'crear contrato' tiene que ser autorizada. Esta es la carga de autorización que tiene que ser autorizada por cualquier desplegador en cualquier contexto. Contiene la dirección del desplegador, sal y ejecutable.

Este árbol de autorización prueba que el despliegue y la inicialización son autorizados atómicamente: el despliegue real ocurre dentro del contexto de deploy y toda la sal, ejecutable, y los argumentos de inicialización son autorizados juntos (es decir, hay una firma que autoriza esta combinación exacta).

Luego nos aseguramos de que el desplegador ha autorizado el árbol esperado y que el valor esperado ha sido almacenado.

assert_eq!(env.auths(), vec![(deployer, expected_auth)]);

let client = contract::Client::new(&env, &contract_id);
let sum = client.value();
assert_eq!(sum, 5);

Construir los Contratos

Para construir el contrato en un archivo .wasm, utiliza el comando stellar contract build. Construye tanto el contrato desplegador como el contrato de prueba.

stellar contract build

Ambos archivos .wasm deberían encontrarse en los directorios target de ambos contratos después de construir ambos contratos:

target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm
target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm

Ejecutar el Contrato

Si tienes stellar-cli instalado, puedes invocar la función del contrato para desplegar el contrato de prueba.

Antes de desplegar el contrato de prueba con el desplegador, instala el Wasm del contrato de prueba usando el comando install. El comando install imprimirá el hash derivado del archivo Wasm (no es solo el hash del archivo Wasm en sí) que debería ser utilizado por el desplegador.

stellar contract install --wasm contract/target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm

El comando imprime el hash en formato hexadecimal. Debería verse algo así como 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15.

También necesitamos desplegar el contrato Desplegador:

stellar contract deploy --wasm deployer/target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm --id 1

Esto devolverá la dirección del desplegador: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM.

Luego el contrato desplegador puede ser invocado con el valor hash de Wasm anterior.

stellar contract invoke --id 1 -- deploy \
--deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM
--salt 123 \
--wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 \
--init_fn init \
--init_args '[{"u32":5}]'

Y luego invoca el contrato de prueba desplegado utilizando el identificador devuelto del comando anterior.

stellar contract invoke \
--id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d \
-- \
value

La siguiente salida debería ocurrir utilizando el código anterior.

5