Cross-blueprint Calls

A cross-blueprint call happens when a function/method invokes a function/method defined in a different blueprint. This allows a developer to create complex systems by composing various blueprints and components together.

Using a blueprint from the same package

You might decide to combine multiple blueprints in the same package. This allows you to easily deploy complex inter-blueprint functionnality to the ledger. In this section, you will learn how to call a bluebrint from another one living in the same package.

Let’s say you have two blueprints: CoffeeMachine and AlarmClock. If you want to be able to instantiate a CoffeeMachine component and call its methods from one of the AlarmClock’s method/function you would create three files:

src/lib.rs

// Import the blueprints that are part of the package
mod coffee_machine;
mod alarm_clock;

This lib.rs file is the starting point of all Scrypto packages. If you have only one blueprint in the package, you could write the logic directly in that file, like we saw previously. In our case, we will write the logic of the two blueprints in separate files. That’s why in lib.rs we are importing the two other files to include in our package (coffee_machine and alarm_clock) with the mod keyword.

src/coffee_machine.rs

use scrypto::prelude::*;

blueprint! {
    struct CoffeeMachine {}

    impl CoffeeMachine {
        pub fn new() -> CoffeeMachineComponent{ (1)
            Self{}.instantiate()
        }

        pub fn make_coffee(&self) {
            info!("Brewing coffee !");
        }
    }
}
1 Here we need to return CoffeeMachineComponent which is magic syntax that the blueprint! macro allows. Also notice that we do not call the globalize() method after instantiation. This component will only be accessible by our second blueprint that we are going to go through in the next section…​

This file includes the logic for the CoffeeMachine blueprint. This blueprint offers a function to instantiate a component with an empty state that offers a make_coffee() method, which we will call from the AlarmClock blueprint.

src/alarm_clock.rs

use scrypto::prelude::*;
use crate::coffee_machine::*; (1)

blueprint! {
    struct AlarmClock {
        // Store the coffee machine component
        coffee_machine: CoffeeMachineComponent
    }

    impl AlarmClock {
        pub fn new() -> ComponentAddress {
            Self{
                coffee_machine: CoffeeMachineComponent::new() (2)
            }.instantiate().globalize()
        }

        pub fn try_trigger(&mut self) {
            assert!(Runtime::current_epoch() % 100 == 0, "It's not time to brew yet !");
            self.coffee_machine.make_coffee(); (3)
        }
    }
}
1 Import the CoffeeMachine blueprint
2 Instantiate a CoffeeMachine component from the blueprint
3 Call methods on the component

First, this blueprint imports the CoffeeMachine Blueprint at the top of the file. Then, it instantiates a new CoffeeMachine component and stores it inside a newly instantiated AlarmClock component. Finally, in the try_trigger method, the CoffeeMachine’s make_coffee method is called.

Using a blueprint outside of your package

There are two main ways to use blueprints from external packages in your local package:

  1. Through the import! macro and the ABI.

  2. Through the external_blueprint! macro and the function signatures.

Each one of the ways comes with its advantages and disadvantages. So, there is no clear cut recommended way of using external blueprints in your package. However, the sections below touch on cases where each of the above should be used.

External Blueprint calls through the import! macro.

The import! macro is ideal when dealing with an external blueprint that uses custom types as its arguments or returns. An advantage of the import! macro and the ABI is that the structure of the custom types required by the external blueprint is included in the ABI and therefore the structs can be reconstructed by the macro and used by the developer.

Let’s say that someone else already wrote an implementation of a liquidity pool and you want to use it in a decentralized exchange (DEX) you are building.

You would first have to retrieve the ABI (Application Binary Interface) of the liquidity pool blueprint and specify it in your DEX’s code. The ABI specifies all the functions, methods and the type of each argument.

resim comes with a command to export the ABI of a particular blueprint:

resim export-abi <PACKAGE_ADDRESS> <BLUEPRINT_NAME>

After obtaining the blueprint’s ABI, you can call the import macro to use the blueprint in your code.

import! { r#"
{
  "package_address": "package_sim1q8td4r65grsk4t85zl7gsh9agukyxfsxrqqagjfsua0s2sur8v",
  "blueprint_name": "Hello",
  "abi": {
    "structure": {
      "type": "Struct",
      "name": "Hello",
      "fields": {
        "type": "Named",
        "named": [
          [
            "sample_vault",
            {
              "type": "Custom",
              "type_id": 179,
              "generics": []
            }
          ]
        ]
      }
    },
    "fns": [
      {
        "ident": "instantiate_hello",
        "mutability": null,
        "input": {
          "type": "Struct",
          "name": "Hello_instantiate_hello_Input",
          "fields": {
            "type": "Named",
            "named": []
          }
        },
        "output": {
          "type": "Custom",
          "type_id": 129,
          "generics": []
        },
        "export_name": "Hello_instantiate_hello"
      },
      {
        "ident": "free_token",
        "mutability": "Mutable",
        "input": {
          "type": "Struct",
          "name": "Hello_free_token_Input",
          "fields": {
            "type": "Named",
            "named": []
          }
        },
        "output": {
          "type": "Custom",
          "type_id": 177,
          "generics": []
        },
        "export_name": "Hello_free_token"
      }
    ]
  }
}
"# }

r#"<STRING_LITERAL>"# is a Rust syntax to define a string literal that may span multiple lines.

Once the blueprint’s ABI is imported, you can call any of its functions. For example, you can call its new function and instantiate a Hello component like this:

let hello: Hello = Hello::instantiate_hello().into();

Calling Hello::instantiate_hello() creates a new component and returns its address. If you want to reuse an existing component, you can retrieve the component with its address:

struct AMM {
  price: Decimal
}

impl AMM {
  pub fn new(oracle_address: ComponentAddress) -> ComponentAddress {
    let oracle: Oracle = oracle_address.into();
    Self {
      price: oracle.get_price()
    }.instantiate().globalize()
  }
}

As of v0.5 you cannot convert a String to a ComponentAddress directly. You have to take the address from the function/method arguments or from within the component state.

External Blueprint calls through the external_blueprint! macro.

Currently, the import! macro requires use of the ABI which is one long JSON string that is hard to read, understand, or interpret, which in turn, makes it more difficult to verify the correctness of code just by looking at it.

The external_blueprint! macro was introduced with v0.5.0 with the aim of avoiding some of the shortcomings of the import! macro, namely, this macro aims to provide a more readable way of importing external blueprints into a package. In addition to that, this macro can potentially have better enforcement of mutability requirement (when used correctly). This macro achieves better readability by not requiring the entire ABI of the external blueprint. Instead, all that it requires is:

  1. The blueprint’s package address and name.

  2. The function and method signatures.

With the above data provided, the macro generates all of the code required to call the external blueprint. The following is example usage of the macro:

external_blueprint! {
  {
      package: "package_sim1q8e4p0ut06dxeczmmhjlcs9mlkkhhml9ce58dlwxtugsfxedpm",
      blueprint: "Radiswap"
  },
  Radiswap {
    fn instantiate_pool(a_tokens: Bucket, b_tokens: Bucket, lp_initial_supply: Decimal, lp_symbol: String, lp_name: String, lp_url: String, fee: Decimal) -> (ComponentAddress, Bucket);
    fn add_liquidity(&mut self, a_tokens: Bucket, b_tokens: Bucket) -> (Bucket, Bucket);
    fn remove_liquidity(&mut self, lp_tokens: Bucket) -> (Bucket, Bucket);
    fn swap(&mut self, input_tokens: Bucket) -> Bucket;
    fn get_pair(&self) -> (ResourceAddress, ResourceAddress);
  }
}

The blueprint imported in the code above is far more complex than that imported in the previous section. Despite that, the code used to import it remains to be far more readable and understandable. Once the blueprint is imported, the interaction with the blueprint is identical to that described in the previous section.

If the external blueprint requires any custom structs or enums, then you do one of the following:

  1. Copy their definitions verbatim from the source code for the blueprint, and referenced inside the macro (along with its derives).

  2. Resort to using the import! macro since the ABI contains the structure of all of the required structs and enums and can generate them automatically.

Consider a situation where an external blueprint that we wish to import requires a custom enum called DepositResult. The following code shows how this situation can be dealt with when using the external_blueprint! macro:

#[derive(TypeId, Encode, Decode, Describe)]
enum DepositResult { (1)
    Success,
    Failure
}

external_blueprint! {
    {
        package: "package_sim1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsnznk7n",
        blueprint: "CustomAccount"
    },
    CustomAccount {
        fn instantiate_global(account_name: &str) -> ComponentAddress;
        fn deposit(&mut self, b: Bucket) -> DepositResult; (1)
        fn deposit_no_return(&mut self, b: Bucket);
        fn read_balance(&self) -> Decimal;
    }
}
1 Since the DepositResult enum is required by this blueprint, it needs to be defined outside of the external_blueprint! macro, and then used in the function or method signatures. You can obtain the definition of the enum from the source code of the external blueprint.

Calling without Blueprint ABI

In cases where you want to call a certain method on a component but you don’t know its ABI beforehand, you can also dynamically call a function or method using Scrypto APIs.

To call a function on a blueprint, construct a Package and then make a call:

let package_address: PackageAddress = "010a5bddf74447d15eea67356e696cc26aaf4994957af414bcbb51".parse().unwrap();
let package: &Package = borrow_package!(package_address);
let return_value = package.call::<ReturnType>("Hello", "instantiate_hello", args![arg1, arg2, arg3]);

To call a method, construct a Component and then make a call:

let component_address: ComponentAddress = "02ab611bef107a02c6c57785bf04793ccc1aa18d39226a7378a9f9".parse().unwrap();
let component: &Component = borrow_component!(component_address);
let return_value = component.call::<ReturnType>("free_token", args![arg1, arg2]);

If no return data is expected, use () for ReturnType.