Llamadas Cross-Contract
En los contratos de ink! es posible llamar a mensajes y constructores de otros contratos on-chain.
Existen varios enfoques para realizar estas llamadas Cross-Contract en ink!:
- Referencias de Contrato (
ContractRef
) - Builders (
CreateBuilder
yCallBuilder
)
Las Referencias de Contrato sólo pueden utilizarse para llamadas Cross-Contract a otros contratos de ink!. Los Builders pueden utilizarse para realizar llamadas Cross-Contract a cualquier contrato Wasm, como los escritos en ink!, Solang o ask!.
Referencias de Contrato
Las Referencias de Contrato se refieren a las structs generadas por el generador de código de ink! para las llamadas Cross-Contract.
Proporcionan a los desarrolladores una forma segura de interactuar con un contrato.
Una desventaja de su uso es que necesita importar el contrato al que desea llamar como una dependencia de su propio contrato.
Si quiere interactuar con un contrato que ya está on-chain, tendrás que utilizar el enfoque de los Builders
.
Tutorial para BasicContractRef
Recorreremos el ejemplo de basic_contract_ref
para demostrar cómo funcionan las llamadas Cross-Contract utilizando Referencias de Contratos.
El flujo general de trabajo será:
- Preparar
OtherContract
para ser importado en otros contratos - Importar
OtherContract
enBasicContractRef
- Cargar
OtherContract
on-chain - Instanciar
OtherContract
usandoBasicContractRef
- Llamar
OtherContract
usandoBasicContractRef
Preparación de OtherContract
Debemos asegurarnos de que la Referencia de Contrato generada por ink! para OtherContract
esté disponible para otras partes del código.
Para ello, re-exportaremos la Referencia de Contrato como se indica a continuación:
pub use self::other_contract::OtherContractRef;
Importando OtherContract
A continuación, tenemos que importar OtherContract
a nuestro contrato BasicContractRef
.
Primero, añadiremos las siguientes líneas a nuestro archivo Cargo.toml
:
# En `basic_contract_ref/Cargo.toml`
other_contract = { path = "other_contract", default-features = false, features = ["ink-as-dependency"] }
# -- snip --
[features]
default = ["std"]
std = [
"ink/std",
# -- snip --
"other_contract/std",
]
Hay dos cosas a tener en cuenta:
- Si no especificamos la característica
ink-as-dependency
terminaremos con errores de vinculación. - Si no habilitamos la característica
std
para construccionesstd
no podremos generar los metadatos de nuestro contrato.
Conectando BasicContractRef
En primer lugar, importaremos la Referencia de Contrato de OtherContract
, y declararemos que la referencia forma parte de nuestra estructura de almacenamiento.
// En `basic_contract_ref/lib.rs`
use other_contract::OtherContractRef;
#[ink(storage)]
pub struct BasicContractRef {
other_contract: OtherContractRef,
}
A continuación, añadiremos una forma de instanciar OtherContract
. Lo haremos desde el constructor de nuestro contrato.
// En `basic_contract_ref/lib.rs`
#[ink(constructor)]
pub fn new(other_contract_code_hash: Hash) -> Self {
let other_contract = OtherContractRef::new(true)
.code_hash(other_contract_code_hash)
.endowment(0)
.salt_bytes([0xDE, 0xAD, 0xBE, 0xEF])
.instantiate();
Self { other_contract }
}
Tenga en cuenta que para instanciar un contrato se necesitará acceder al code_hash
cargado on-chain. Se detallará esto más adelante.
Una vez que se obtenga una referencia instanciada a OtherContract
, se puede llamar a sus mensajes como si fueran métodos normales de Rust.
// En `basic_contract_ref/lib.rs`
#[ink(message)]
pub fn flip_and_get(&mut self) -> bool {
self.other_contract.flip();
self.other_contract.get()
}
Cargando OtherContract
Necesitaremos el substrate-contracts-node
ejecutándose en segundo plano para los siguientes pasos.
Se puede cargar OtherContract
usando cargo-contract
de la siguiente manera:
# En la carpeta `basic_contract_ref`
cargo contract build --manifest-path other_contract/Cargo.toml
cargo contract upload --manifest-path other_contract/Cargo.toml --suri //Alice
Si tenemos éxito, esto dará como resultado un code_hash
similar a:
Code hash "0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa"
Se puede utilizar este code code_hash
para instanciar nuestro contrato BasicContractRef
.
Instanciando OtherContract
a través de BasicContractRef
Primero tendremos que instanciar BasicContractRef
.
# En la carpeta `basic_contract_ref`
cargo contract build
cargo contract instantiate \
--constructor new \
--args 0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa \
--suri //Alice --salt $(date +%s)
Si tenemos éxito, esto dará como resultado una dirección de contrato para BasicContractRef
similar a:
Contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq
Llamando a OtherContract
a través de BasicContractRef
Por último, podemos llamar a los métodos de OtherContract
a través de BasicContractRef
de la siguiente manera:
cargo contract call --contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq \
--message flip_and_get --suri //Alice --dry-run
Lo que resultará en algo similar a:
Result Success!
Reverted false
Data Ok(true)
Builders
CreateBuilder
y
CallBuilder
ofrecen interfaces flexibles de bajo nivel para poder realizar llamadas entre contratos. El CreateBuilder
permite instanciar contratos ya cargados, y el
CallBuilder
permite llamar mensajes en contratos instanciados.
CreateBuilder
CreateBuilder
ofrece una forma sencilla de instanciar un contrato. Tenga en cuenta que necesitará que este contrato haya sido cargado previamente.
Para repasar la diferencia entre upload
e instantiate
dirigirse aquí.
Para instanciar un contrato necesitaremos una referencia a un contrato, al igual que en la sección anterior.
A continuación se muestra un ejemplo de cómo instanciar un contrato utilizando el CreateBuilder
. Vamos a:
- instanciar el contrato cargado con un
code_hash
de0x4242...
- sin límite de gas especificado (
0
significa ilimitado) - enviando
10
unidades de valor transferido a la instancia del contrato - instanciando con el constructor
new
- con los siguientes argumentos
- un
u8
con valor42
- un
bool
con valortrue
- un vector de 32
u8
con valor0x10
- un
- generar la dirección (
AccountId
) utilizando lossalt_bytes
especificados - y esperar que devuelva un valor de tipo
MyContractRef
use contract::MyContractRef;
let my_contract: MyContractRef = build_create::<MyContractRef>()
.code_hash(Hash::from([0x42; 32]))
.gas_limit(0)
.endowment(10)
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("new")))
.push_arg(42)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF])
.returns::<MyContractRef>()
.instantiate();
Dado que CreateBuilder::instantiate()
devuelve una referencia a un contrato, podemos utilizar esta referencia a un contrato para llamar a mensajes como en la
sección anterior.
CallBuilder
CallBuilder
te ofrece algunas maneras de llamar a mensajes desde otros contratos. Hay dos enfoques principales para esto: Call
s y DelegateCall
s. Cubriremos brevemente ambos aquí.
CallBuilder: Call
Al utilizar Call
s, el CallBuilder
requiere un contrato ya instanciado.
Vimos un ejemplo de cómo utilizar el CreateBuilder
para instanciar contratos en la
sección anterior.
A continuación, veremos un ejemplo de cómo llamar a un contrato utilizando el CallBuilder
. Vamos a:
- hacer una
Call
normal - a un contrato en la dirección
0x4242...
- sin especificar límite de gas (
0
significa ilimitado) - enviar
10
unidades de valor transferido a la instancia del contrato - llamar al mensaje
flip
- con los siguientes argumentos
- un
u8
con valor42
- un
bool
con valortrue
- un vector de 32
u8
con valor0x10
- un
- y esperar que devuelva un valor de tipo
bool
let my_return_value = build_call::<DefaultEnvironment>()
.call(AccountId::from([0x42; 32]))
.gas_limit(0)
.transferred_value(10)
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("flip")))
.push_arg(42u8)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.returns::<bool>()
.invoke();
Nota:
Los argumentos de los mensajes se codificarán en el orden en que se proporcionen al CallBuilder
.
Esto significa que deben coincidir con el orden (y tipo) en que aparecen en la firma de la función.
No podrá obtener información sobre esto en tiempo de compilación. Sólo descubrirá que la llamada ha fallado en tiempo de ejecución.
CallBuilder: DelegateCall
También puedes utilizar el CallBuilder
para crear llamadas utilizando la mecánica DelegateCall
.
Si necesitas un repaso de lo que son las llamadas a delegados,
consulta este artículo.
En el caso de DelegateCall
, no necesitamos un contrato ya instanciado. Sólo necesitamos el code_hash
de un contrato subido.
A continuación se muestra un ejemplo de cómo realizar una llamada delegada a un contrato utilizando el CallBuilder
. Vamos a:
- hacer una
DelegateCall
- a un contrato
code_hash
(¡no una dirección de contrato!) de0x4242...
- sin límite de gas especificado (
0
significa "automático") - enviar
10
unidades de valor transferido a la instancia del contrato - llamar al mensaje
flip
- con los siguientes argumentos
- un
u8
con valor42
- un
bool
con valortrue
- un vector de 32
u8
con valor0x10
- un
- y esperar que devuelva un
i32
let my_return_value = build_call::<DefaultEnvironment>()
.delegate(ink::primitives::Hash::from([0x42; 32]))
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("flip")))
.push_arg(42u8)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.returns::<i32>()
.invoke();
Gestión de errores del Builder
Tanto CreateBuilder
como el CallBuilder
ofrecen gestión de errores con los métodos try_instantiate()
y try_invoke()
respectivamente.
Estos permiten a los desarrolladores de contratos gestionar dos tipos de errores:
- Errores del entorno de ejecución subyacente (por ejemplo, el pallet
Contracts
) - Errores del lenguaje de programación (por ejemplo,
LangError
s)
Consulte la documentación de
try_instantiate
,
try_invoke
,
ink::env::Error
y
ink::LangError
para más detalles sobre el manejo adecuado de errores.
Dado que el CallBuilder
sólo requiere el AccountId
de un contrato y el selector
de un mensaje, podemos llamar a contratos Solidity que estén compilados utilizando el compilador Solang
y subidos a una chain que soporte pallet-contracts
.
Vea aquí un ejemplo de cómo hacerlo.
Por otro lado, las llamadas desde Solidity a ink! no están soportadas por Solang, pero hay planes para implementarlo en el futuro.