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 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() -> ComponentAddress {
            Self{}.instantiate().globalize()
        }

        pub fn make_coffee(&self) {
            info!("Brewing coffee !");
        }
    }
}

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::CoffeeMachine; (1)

blueprint! {
    struct AlarmClock {
        // Store the component address in the state
        coffee_machine_address: ComponentAddress
    }

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

        pub fn try_trigger(&mut self) {
            assert!(Runtime::current_epoch() % 100 == 0, "It's not time to brew yet !");
            let coffee_machine: CoffeeMachine = self.coffee_machine_address.into();
            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 component at the top of the file. Then, it instantiates a new CoffeeMachine component and stores its address 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

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": "010e8bad2f3a0e497e18fe3420662859d78a27fda5449d02df42aa",
  "blueprint_name": "Hello",
  "functions": [
    {
      "name": "instantiate_hello",
      "inputs": [],
      "output": {
        "type": "Custom",
        "name": "ComponentAddress",
        "generics": []
      }
    }
  ],
  "methods": [
    {
      "name": "free_token",
      "mutability": "Mutable",
      "inputs": [],
      "output": {
        "type": "Custom",
        "name": "Bucket",
        "generics": []
      }
    }
  ]
}]
}
"# }

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:

let address: ComponentAddress = "022cf5de8153aaf56ee81c032fb06c7fde0a1dc2389040d651dfc2".parse().unwrap();
let hello: Hello = address.into();
let tokens: Bucket = hello.free_token();

To increase the reusability of your blueprint, instead of hard coding the address of a component or blueprint, you could accept it as a parameter of your functions or methods:

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()
  }
}

Doing it this way allows anyone to instantiate a new AMM that will work with any instantiated Oracle components.

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.