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:

leo add your_program_id.aleo --network testnet # Or mainnet depending on the network

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

import arithmetic.aleo;

program quadratic.aleo {
    transition residue(a: u64, n: u64) -> u64 {
        let squared: u64 = arithmetic.aleo/square_u64(a);
        return squared.mod(n);
    }
}

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

import arithmetic.aleo;

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

let squared: u64 = arithmetic.aleo/square_u64(a);

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.

    transition get_caller() -> address {
        let direct_caller: address = self.caller;
        return direct_caller;
    }

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.

    transition direct_calls_only() {
        let direct_caller: address = self.caller;
        let transaction_signer: address = self.signer;
        assert_eq(direct_caller, transaction_signer);
    }

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:

program arithemtic.aleo {
    record Counter {
        owner: address,
        amount: u64
    }
    
    transition create() -> Counter {
        let counter: Counter = Counter {
            owner: self.signer,
            amount: 0u64
        };
        return counter;
    }
    
    // Square function now includes a Counter record input/output.
    transition square_u64(a: u64, in_counter: Counter) -> (u64, Counter) {
        let out_counter: Counter = Counter {
            owner: in_counter.owner,
            amount: in_counter.amount + 1u64
        };
        return (a*a, out_counter);
    }
}

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

import arithmetic.aleo;

program quadratic.aleo {
    transition residue(
        a: u64, n: u64, in_counter: arithmetic.aleo/Counter
    ) -> (u64, arithmetic.aleo/Counter) {
        let (squared, out_counter): (
            u64, arithmetic.aleo/Counter
        ) = arithmetic.aleo/square_u64(a, arithmetic.aleo/Counter);
        let out_residue: u64 = squared.mod(n);
        return (out_residue, out_counter);
    }
}

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:

program arithemtic.aleo {
    struct Point {
        x: u64,
        y: u64
    }
    
    transition add_points(a: Point, b: Point) -> Point {
        let c: Point = Point {
            x: a.x + b.x,
            y: a.y + b.y
        };
        return c;
    }
}

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

import arithmetic.aleo;

program quadratic.aleo {
    // Redefine the struct with the same name and fields
    struct Point {
        x: u64,
        y: u64
    }

    // Use struct as a normal struct
    transition double_point(a: Point) -> Point {
        let doubled: Point = quadratic.aleo/add_points(a, a);
        return doubled;
    }
}

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.

program arithemtic.aleo {
    mapping counter: u8 => u64;
    
    // Square function now includes a Counter record input/output.
    async transition square_u64(a: u64) -> (u64, Future) {
        let square_u64_future: Future = finalize_square_u64();
        return (a*a, square_u64_future);
    }
    async function finalize_square_u64(){
        let amount: u64 = counter.get_or_use(0u8, 0u64);
        amount += 1u64;
        counter.set(0u8, amount);
    }
}

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

import arithmetic.aleo;

program quadratic.aleo {
    async transition residue(a: u64, n: u64) -> (u64, Future) {
        let (squared, square_u64_future): (
            u64, Future
        ) = arithmetic.aleo/square_u64(a, arithmetic.aleo/Counter);
        let out_residue: u64 = squared.mod(n);
        
        let residue_future: Future = finalize_residue(square_u64_future);
        return (out_residue, residue_future);
    }
    async function finalize_residue(square_u64_future: Future){
        // Do anything...
        square_u64_future.await(); // Await the future
        // Do anything else...
    }
}

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.

    async transition calls_2transitions() -> Future {
        let foo_future: Future = external.aleo/foo();
        let bar_future: Future = external.aleo/bar();
        let out_future: Future = finalize_calls_2transitions(foo_future, bar_future);
        return out_future;
    }
    async function finalize_calls_2transitions(future1: Future, future2: Future){
        future1.await();
        future2.await();
    }

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:

        future2.await();
        future1.await();

Reading external mappings

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

import arithmetic.aleo;

program quadratic.aleo {
    async transition read_external_mapping() -> (Future) {
        return finalize_read_external_mapping();
    }
    async function finalize_read_external_mapping(){
        let count: u64 = arithmetic.aleo/counter.get(0u8);
        assert_neq(count, 0u64);
    }
}

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

Last updated