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.
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
#[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
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()
).
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
.
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.
#[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.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke --id 1 -- deploy \
--deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM
--salt 123 \
--wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 \
--init_fn init \
--init_args '[{"u32":5}]'
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.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d \
-- \
value
stellar contract invoke `
--id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d `
-- `
value
La siguiente salida debería ocurrir utilizando el código anterior.
5