5.1 Composability

On blockchains, single programs living on their own are not usually the norm. In fact, part of what's interesting about developing on a blockchain is that, in theory, anyone can build their own applications leveraging other's tokens, state, features... Things get more interesting when multiple programs work together. This is what we usually call program composability.

Importing a program

On Aleo a transition from a program can call a transition from another program. Here is how it works in practice.

Let's have a first program example, arithmetic.aleo, with a transition that computes the square value of an input.

leo new arithmetic
cd arithmetic

And let's update arithmentic/src/main.leo with the following code:

program arithmetic.aleo {
    transition square_u64(a: u64) -> u64 {
        return a*a;
    }
}

Now let's create another program, quadratic residue, that computes the quadratic residue of a number mod another one, using our first transition.

cd ..
leo new quadratic
cd quadratic

Now we need first to add the other program to our list of external dependencies:

leo add arithmetic --local ../arithmetic

local argument here is the path to the local Leo folder of the project. If you want to use a program deployed on chain already, use instead:

Our source file for that program, quadratic/src/main.leo will include the following content:

First, notice the line on top, outside of the program code block.

Then, inside of the residue transition, notice the external call:

As you can see, external transition calls have almost the same syntax as calling an internal function. The difference is the function identifier includes the imported program ID then the functon name, separated by a slash.

Signer vs Caller

Within a transition you can get the address of the direct caller of the transition, wether it's a program address or a user account address.

For instance, for the following chain of calls: User → Program A → Program B

  • self.caller within program B would be program A's address.

  • self.signer within program B would be user's account address.

Here is an example. The function direct_calls_only defined below could only be called directly from a user would fail otherwise, if called from a program importing it.

External Records

Let's update our imported program so it includes a counter record, to count the amount of times it has been called from quadratic.aleo already:

Here's how quadratic.aleo can now provide those external records

Notice that before every record name, the imported program ID is provided before the slash.

Imported structs

When importing a program that defines struct data types, make sure you redefine those structs in the importing program as this could lead to errors. Contrary to records, you don't add the program ID prior to the imported struct name.

Here's an example of a program defining and using a struct:

And here's how the importing program can use those structs:

Call an Async Transition

Async transition calls are slightly different than the non-async calls that we just went discovered. The reason for this is that there are both the on-chain and off-chain executed code. For each of those, we can decide independently if the caller program's code must be run before/after the imported code.

To understand this let's explore the syntax through an example first. Our arithmetic program will now store the counter of times its been called using a mapping.

Let's see how to call it from the quadratic.aleo program:

As you can notice, the future returned by the async external call is passed as an argument to the on-chain executed function.

All async external calls future must be passed to an async function and awaited this way. They cannot be returned directly. If there are multiple async external calls, each must be passed and awaited.

All the function instructions, mapping reads, writes etc... are sequentially executed in the same order as their future are awaited.

Notice that the futures can be awaited in a different orders than the off-chain execution are made. The following body for the finalize_calls_2transitions function above would be valid as well:

Reading external mappings

While a mapping cannot be updated directly by another program, it is possible to read its value. Here's an example:

You can use the contains and get_or_use mapping functions this way as well.

Last updated