🗣️Chamadas entre Contratos (Cross-Contract Calls)

Nos contratos ink!, é possível chamar mensagens e construtores de outros contratos na cadeia.

Existem algumas abordagens para realizar essas chamadas entre contratos no ink!:

  1. Referências de contratos (ou seja, ContractRef).

  2. Builders (ou seja, CreateBuilder e CallBuilder).

As referências de contratos só podem ser usadas para chamadas entre contratos (cross-contract) ink! a outros contratos ink!. Os builders podem ser usados para fazer chamadas entre contratos (cross-contract) para qualquer contrato Wasm, como os escritos em ink!, Solang ou ask!.

Referências de Contrato

As referências de contrato se referem a estruturas que são geradas pela geração de código ink! com o propósito de chamadas entre contratos.

Elas fornecem aos desenvolvedores uma maneira segura de interagir com um contrato.

Uma desvantagem de usá-las é que você precisa importar o contrato que deseja chamar como uma dependência do seu próprio contrato.

Se você quiser interagir com um contrato que já está na cadeia, você precisará usar a abordagem dos Builders em vez disso.

BasicContractRef Passo a passo

Vamos seguir o exemplo básico basic_contract_ref para demonstrar como as chamadas entre contratos usando referências de contrato funcionam.

O fluxo geral será:

  1. Prepare OtherContract para ser importado por outros contratos.

  2. Importe OtherContract para BasicContractRef.

  3. Faça o upload de OtherContract na on-chain.

  4. Instancie OtherContract usando BasicContractRef.

  5. Chame OtherContract usando BasicContractRef.

Preparação OtherContract

Precisamos garantir que a referência de contrato gerada pelo ink! para OtherContract esteja disponível para outras partes do código.

Fazemos isso reexportando a referência de contrato da seguinte forma:

pub use self::other_contract::OtherContractRef;

Importação OtherContract

Em seguida, precisamos importar OtherContract para nosso contrato BasicContractRef.

Primeiro, adicionamos as seguintes linhas ao nosso arquivo Cargo.toml:

# In `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",
]

Duas coisas a serem observadas aqui:

  1. Se não especificarmos a funcionalidade ink-as-dependency teremos erros de ligação.

  2. Se não habilitarmos a funcionalidade std para compilações std, não poderemos gerar os metadados do nosso contrato.

Conexão BasicContractRef

Primeiro, iremos importar a referência de contrato de OtherContracte declarar a referência como parte da nossa estrutura de armazenamento.

// In `basic_contract_ref/lib.rs`

use other_contract::OtherContractRef;

#[ink(storage)]
pub struct BasicContractRef {
    other_contract: OtherContractRef,
}

Em seguida, precisamos adicionar uma maneira de instanciar OtherContract. Fazemos isso a partir do construtor do nosso contrato.

// In `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 }
}

Observe que, para instanciar um contrato, precisamos ter acesso ao contrato carregado na cadeia code_hash. Voltaremos a isso mais tarde.

Uma vez que temos uma referência instanciada para OtherContract, podemos chamar suas mensagens como se fossem métodos normais do Rust!

// In `basic_contract_ref/lib.rs`

#[ink(message)]
pub fn flip_and_get(&mut self) -> bool {
    self.other_contract.flip();
    self.other_contract.get()
}

Carregando OtherContract

Você precisará do substrate-contracts-node sendo executado em segundo plano para os próximos passos.

Podemos carregar OtherContract usando o cargo-contract da seguinte forma:

# In the `basic_contract_ref` directory
cargo contract build --manifest-path other_contract/Cargo.toml
cargo contract upload --manifest-path other_contract/Cargo.toml --suri //Alice

Se bem-sucedido, isso irá produzir um code_hash semelhante a:

Code hash "0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa"

Podemos então usar este code_hash para instanciar nosso contrato BasicContractRef.

Instanciando OtherContract por meio BasicContractRef

Primeiro, precisaremos instanciar BasicContractRef.

# In the `basic_contract_ref` directory
cargo contract build
cargo contract instantiate \
    --constructor new \
    --args 0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa \
    --suri //Alice --salt $(date +%s)

Se bem-sucedido, isso irá produzir um endereço de contrato paraBasicContractRef semelhante a:

Contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq

Chamando com OtherContract por meio de BasicContractRef

Finalmente, podemos chamar os métodos de OtherContract por meio de BasicContractRef da seguinte maneira:

cargo contract call --contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq \
    --message flip_and_get --suri //Alice --dry-run

O que resultará em algo como:

Result Success!
Reverted false
Data Ok(true)

Builders

Os BuildersCreateBuilder e CallBuilder oferecem interfaces de baixo nível e flexíveis para realizar chamadas entre contratos. O CreateBuilder permite instanciar contratos já carregados, e o CallBuilder permite chamar mensagens em contratos instanciados.

CreateBuilder

O CreateBuilder oferece uma maneira fácil de instanciar um contrato. Note que você ainda precisará ter carregado esse contrato previamente.

NOTA: Para uma revisão sobre a diferença entre upload e instantiate, consulte aqui.

Para instanciar um contrato, você precisa de uma referência ao seu contrato, assim como na seção anterior.

Abaixo está um exemplo de como instanciar um contrato usando o CreateBuilder. Faremos o seguinte:

  • para instanciar o contrato carregado com um code_hash de 0x4242...

  • sem limite de gás especificado (0 significa ilimitado)

  • enviando 10 unidades de valor transferido para a instância do contrato

  • instanciando com o new construtor

  • com os seguintes argumentos

    • um u8 com valor 42

    • um bool com valor true

    • um array de 32 u8 com valor 0x10

  • gerar o endereço (AccountId) usando os salt_bytes especificados

  • e esperamos que retorne um valor do 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();

Como o método CreateBuilder::instantiate() retorna uma referência ao contrato, podemos usar essa referência para chamar mensagens da mesma forma que na seção anterior.

CallBuilder

O CallBuilder oferece algumas maneiras de chamar mensagens de outros contratos. Existem duas abordagens principais para isso:

Chamadas Calls e DelegateCalls. Vamos abordar brevemente ambas aqui.

CallBuilder: Call

Ao usar Chamadas Calls o CallBuilder requer um contrato já instanciado.

Vimos um exemplo de como usar oCreateBuilder para instanciar contratos na seção anterior.

Abaixo está um exemplo de como chamar um contrato usando o CallBuilder. Faremos o seguinte:

  • fazer uma Call regular

  • para um contrato no endereço 0x4242...

  • sem limite de gás especificado (0 significa ilimitado)

  • enviando 10 unidades de valor transferido para a instância do contrato

  • chamando a mensagem flip

  • com os seguintes argumentos

    • um u8 com valor 42

    • um bool com valor true

    • um array de 32 u8 com valor 0x10

  • e esperamos que retorne um valor do 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();

Observação: Os argumentos da mensagem serão codificados na ordem em que são fornecidos ao CallBuilder. Isso significa que eles devem corresponder à ordem (e tipo) em que aparecem na assinatura da função.

Você não será capaz de obter nenhum feedback sobre isso em tempo de compilação. Você só descobrirá que sua chamada falhou em tempo de execução!

CallBuilder: Delegate Call

Você também pode usar o CallBuilder para criar chamadas usando a mecânica DelegateCall. Se você precisa de um lembrete sobre o que são as chamadas de delegação, consulte este artigo.

No caso das DelegateCalls, não exigimos um contrato já instanciado. Apenas precisamos do code_hash de um contrato enviado.

Abaixo está um exemplo de como fazer uma chamada de delegação a um contrato usando o CallBuilder. Faremos o seguinte:

  • fazer uma chamada de delegação DelegateCall

  • para um contrato com um code_hash (não endereço do contrato!) de 0x4242...

  • sem limite de gás especificado (0 significa "automático")

  • enviando 10 unidades de valor transferido para a instância do contrato

  • chamando a mensagem flip

  • com os seguintes argumentos

    • um u8 com valor 42

    • um bool com valor true

    • um array de 32 u8 com valor 0x10

  • e esperamos que retorne um 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();

Builder Error Handling

O CreateBuilder e o CallBuilder oferecem recursos de tratamento de erros com os métodos try_instantiate() e try_invoke(), respectivamente.

Isso permite que os desenvolvedores de contratos lidem com dois tipos de erros:

  1. Erros do ambiente de execução subjacente (por exemplo, o pallet Contracts)

  2. Erros da linguagem de programação (por exemplo, LangErrors)

Consulte a documentação para try_instantiate, try_invoke, ink::env::Error e ink::LangError para obter mais detalhes sobre o tratamento adequado de erros.

DICA: Como o CallBuilder requer apenas o AccountId do contrato e o seletor de mensagem, podemos chamar contratos Solidity compilados usando o compilador Solang e implantados em uma cadeia que suporta o pallet-contracts. Veja aqui um exemplo de como fazer isso.

No entanto, o inverso, chamadas de Solidity para ink!, não é suportado pelo Solang, mas há planos para implementar isso no futuro.

Last updated

Was this helpful?