SanchezDario
Posted on February 12, 2022
Aclaraciones iniciales:
- El siguiente artículo está enfocado en el desarrollo de contratos inteligentes usando Rust.
- Si bien los ejemplos están desarrollados en NEAR, gran parte de la siguiente información se puede transpolar a cualquier otra blockhain donde se pueda desarrollar usando Rust.
- Los ejemplos son extraídos del siguiente repositorio: BlockJobs-Marketplace, donde se pueden observar varias cross-contract calls además de las utilizadas a continuación.
- En este artículo se muestra una cross-contract call desde un marketplace de servicios profesionales, al contrato de DAI, para permitir realizar los pagos de los servicios en DAI.
Introducción:
Existen diversos motivos para requerir hacer llamadas entre contratos, como solicitar y/o modificar información, utilizar código de un contrato ajeno sin copiarlo, transferir tokens, etc. y esto se puede hacer trabajando de una forma similar a como funcionan las llamadas a una API.
A continuación se muestra un ejemplo de esto, brindando una explicación contextual de como una cross-contract call funciona.
1- Contrato que llama
Para poder llamar una función externa lo primero que se debe hacer es indicar a nuestro contrato qué información requiere la función que será llamada, especificando parámetros con sus respectivos tipos de datos (en varios contratos se ve que se agrega &self
y el tipo de dato a retornar, pero esto se puede omitir). Para esto primeramente se debe importar near_sdk::ext_contract
e implementarlo como se muestra a continuación.
use near_sdk::ext_contract;
impl Contract {
…
}
#[ext_contract(ext_contract)]
trait ExtContract {
fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option<String>);
}
#[ext_contract(ext_self)]
pub trait ExtSelf {
fn on_buy_service(service_id: u64) -> Service;
}
- El 'ext_contract' entre paréntesis es el nombre que se le está dando y luego se utilizará para indicar qué trait se está llamando, ya que se puede establecer más de uno con el fin de tener un mejor orden.
- 'ExtContract' es una denominación que no se requiere posteriormente, es decir que es indiferente.
- Si la función que es llamada o no, es pública, es también indiferente, con colocar 'ft' es suficiente.
- El nombre de la función y los parámetros deben ser exactamente iguales a los del contrato que es llamado, ante cualquier error de tipeado recibiremos un error de "insufficient gas amount".
1.1 - Hacer el llamado
Dentro de la función correspondiente, el llamado se realiza de la siguiente forma:
ext_contract::ft_transfer(
self.contract_me.clone(),
service.metadata.price.into(),
None,
&token,
ONE_YOCTO,
GAS_FT_TRANSFER
).then(ext_self::on_buy_service(
service_id,
&env::current_account_id(),
NO_DEPOSIT,
BASE_GAS)
);
- Se está llamando a
ft_transfer
pasando primeramente los 3 parámetros correspondientes, luego la dirección del contrato (se debe pasar la dirección de DAI en este caso, la cual está asignada atoken
), luego la cantidad de NEAR adjunto para el caso de tratarse de una función de tipo payable y finalmente la cantidad de gas, tema que será ampliado más adelante. El orden de los parámetros debe ser estrictamente respetado, de lo contrario fallará y no siempre el compilador muestra error. - El depósito y el gas están en este caso asignados en constantes, pero puede también especificarse el valor numérico directamente.
- El método
then
puede omitirse, se utiliza en el caso de querer ejecutar una segunda función según lo que retorne la función que se está llamando, esto se muestra con mayor detalle a continuación.
1.2 - Call-back
Se puede crear una segunda función en el contrato que realiza la llamada, la cual a partir de lo que se retorna ejecute determinadas acciones. No es estrictamente necesario pero normalmente se le denomina con un on predecediendo al nombre la primer función, en el caso de ejemplo la función desde donde se llama inicialmente es buy_service
por lo que ahora la denominación será on_buy_service
y está dada por el siguiente código:
pub fn on_buy_service(&mut self, service_id: u64) -> Service {
match env::promise_result(0) {
PromiseResult::Successful(_data) => {
service.sold = true;
service.on_sale = false;
self.service_by_id.insert(&service_id, &service);
return service;
}
PromiseResult::Failed => env::panic(b"Callback faild"),
PromiseResult::NotReady => env::panic(b"Callback faild"),
};
}
- En el caso de haber un error en el llamado se ejecuta un error de tipo panic con el mensaje "Callback faild", en caso de haberse ejecutado correctamente la llamada se ejecuta el código que modifica el estado del contrato que llama inicialmente (esto podría hacerse directamente en el primer función prescindiendo del método
then
, pero se trata de una buena práctica. - En este caso no se está haciendo uso del valor que se retorna, por esto es que la parámentro
data
tiene un "_" antepuesto, evitando que se emita un warning, pero podría ser necesario dicho valor para la modificación de estado. - En el repositorio compartido inicialmente se pueden ver también ejemplos donde se omite esto.
- Si bien no se hace uso en este ejemplo, puede ser útil el retornar una promesa, para lo cual se debe importar
near_sdk::PromiseOrValue
, en este link se amplía este tema.
2 - Contrato que es llamado
El contrato que se llama puede tratarse tanto de uno propio como uno ajeno, tan solo es necesario conocer la dirección y el método a llamar con sus parámetros.
En el caso de ejemplo se está llamando a la función para transferir del contrato de DAI, por lo tanto si la transferencia se ejecuta correctamente, se ejecuta posteriormente on_buy_service
y modificado el estado según se le indique. Sin embargo el valor retornado podría tratarse de otros datos como un booleano por ejemplo y a partir de este modificar la información. En el código de ejemplo se puede observar esto yendo a la función validate_user
y validate_tokens
, las cuales se encuentran en el contrato mediator.rs
.
3 - Manejo del gas
El error más común es el de "Insufficient gas amount", el cual puede deberse a una insuficiente cantidad de gas establecido o adjuntado, pero también a un error que evite que la función llamada termine de ejecutarse y retornar un resultado. En el caso de suceder esto puede ser buena idea el emitir logs al principio y al final de cada función para ver hasta donde se realiza la ejecución y también el comentar el código donde se realiza cambios de estado para verificar si el problema está en la llamada o en el código interno de alguna de las funciones.
Si el error no se encuentra realizando lo antes mencionado , puede ser que se esté estableciendo una cantidad de gas mayor a la máxima soportada por Near que es de 300 Tgas, si en el ejemplo se hubiese pasado como parámetro 200 Tgas para ejecutar ft_transfer
y 120 Tgas para on_buy_service
, esos 300 Tgas nunca serían suficientes, o en el caso de trabajar en testnet, puede ser que no se esté adjuntando el gas suficiente. Para hacer pruebas puede ser recomendable especificar la cantidad máxima de gas agregando --gas 300000000000000
al final del llamado a la función. Luego el gas sobrante se devuelve a quien firma la transacción, pero si se prefiere este valor puede ajustarse viendo cuánto gas se consume, es recomendable indicar siempre una cantidad mayor, quizás el doble para asegurarse de que no sea insuficiente.
Aclaraciones finales
- Recuerde que es recomendable escribir test para las funciones y las llamadas entre contratos que se realicen.
- Para ver información pertinente al uso de gas y los valores retornados por las funciones puede utilizar Near Explorer.
- Para dudas se recomienda StackOverflow, o el canal de telegram para devs.
Espero esta información le haya sido de utilidad.
Posted on February 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.