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 arithmeticAnd 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 quadraticNow we need first to add the other program to our list of external dependencies:
leo add arithmetic --local ../arithmeticlocal 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.
On Aleo there is no such thing as "call by address", as you would do in Solidity, at the moment this course is being written. This means it is not possible to depend dynamically on any arbitrary program implementing a certain interface, then providing that program address as one of the argument of the caller function.
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.callerwithin program B would be program A's address.self.signerwithin 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.
You can use a program ID directly in Leo to get its address, for instance for getting the address of my_program.aleo:
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.
Having a external record as an output of a program does not mean it will necessarily consume that record. The record will only be consumed if it is used as an input of a transition of the initial program it was defined in.
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