DEV Community

Cover image for Soroban Quest - Cross Contract
Altuğ Bakan
Altuğ Bakan

Posted on • Edited on

Soroban Quest - Cross Contract

We have tackled the Reverse Engineer quest on the last post, and are ready for the next quest, Cross Contract.

Understanding README.md

As always, let's check out the README.md file on the quest folder.

README

On this quest, we are tasked with deploying the CrossContractCall contract, and use it to interact with the DataStore contract we worked on the Auth Store quest.

Setting Up Our Quest Account

You should be pretty familiar with this concept by now. Use sq login if you haven't, and sq play 4 to start this quest.

Examining the Contract

Let's check out the contract's source code on lib.rs to learn more about this quest.

mod storage_contract {
    soroban_sdk::contractimport!(
        file = "../../target/wasm32-unknown-unknown/release/soroban_auth_store_contract.wasm"
    );
}

pub trait StorageCallTrait {
    fn inv_get(env: Env, store_id: BytesN<32>, owner: AccountId) -> Bytes;
}

pub struct CrossContractCallContract;

#[contractimpl]
impl StorageCallTrait for CrossContractCallContract {
    fn inv_get(env: Env, store_id: BytesN<32>, owner: AccountId) -> Bytes {
        let storage_client = storage_contract::Client::new(&env, store_id);
        storage_client.get(&owner)
    }
}
Enter fullscreen mode Exit fullscreen mode

There are some new concepts on the source code. Let's examine them one by one.

Contract Import

Previously, we used the mod keyword to include the test.rs file, or the error.rs contract into our code. These files had their source code ready for importing, and we used them for developing our contract.

Using soroban_sdk::contractimport! macro is quite different, as we are now importing a precompiled contract. The soroban_sdk can use the imported contract's types, interface, and the client in the imported source code this way.

Contract Traits

We can define traits for contracts using the trait keyword. This can be thought as an abstract class, meaning that it has the interface but not the source code of its subclasses. On the source code of the CrossContractCallContract, we have

pub trait StorageCallTrait {
    fn inv_get(env: Env, store_id: BytesN<32>, owner: AccountId) -> Bytes;
}
Enter fullscreen mode Exit fullscreen mode

which means that every contract that implements the StorageCallTrait should implement a function, inv_get, which has the parameters env, store_id, owner, and returns a Bytes result.

Why Do We Need Traits?

Since interoperability is a basic feature of smart contract blockchains, there are some standards that people use for easier interoperability. Let's say we have a token trait, which implements the send, recv_from, and balance_of traits. If we have such a standard, we can develop a token marketplace for all tokens that use this standard. Actually, most of the tokens you see everywhere implements the ERC-20 Token Standard created by Ethereum developers!

If we didn't have such standards exist, the exchange developers would have to support every implementation that exist on a smart contract blockchain, which would be harmful for anyone's time, and would raise various security concerns.

After creating a trait, we can import it to our contract using

impl StorageCallTrait for CrossContractCallContract {
    // code implementing inv_get here
}
Enter fullscreen mode Exit fullscreen mode

If we fail to implement the inv_get function, the Rust compiler would give an error such as

Trait Error

Very handy for developing contracts!

Overall View

After getting to know about the new concepts on the contract, we can start checking out what the contract does.

There is only one function on the contract implementation, which is inv_get

fn inv_get(env: Env, store_id: BytesN<32>, owner: AccountId) -> Bytes {
        let storage_client = storage_contract::Client::new(&env, store_id);
        storage_client.get(&owner)
    }
Enter fullscreen mode Exit fullscreen mode

It simply uses the imported StorageContract's get function to return the data that exists on that contract for an address.

Examining the Test

The test.rs file has one test function, get_cross_call.

use crate::{storage_contract, CrossContractCallContract, CrossContractCallContractClient};

#[test]
fn get_cross_call() {
    let env = Env::default();
    let storage_contract_id = env.register_contract_wasm(None, storage_contract::WASM);
    let storage_contract_client = storage_contract::Client::new(&env, storage_contract_id.clone());

    let cross_call_contract_id = env.register_contract(None, CrossContractCallContract);
    let cross_call_contract_client =
        CrossContractCallContractClient::new(&env, cross_call_contract_id);

    let u1 = env.accounts().generate();
    env.set_source_account(&u1);
    storage_contract_client.put(&bytes![&env, 0x48656c6c6f20536f726f62616e21]);

    assert_eq!(
        cross_call_contract_client.inv_get(&storage_contract_id, &u1),
        bytes![&env, 0x48656c6c6f20536f726f62616e21]
    );
}
Enter fullscreen mode Exit fullscreen mode

We have an interesting line here, use crate::{storage_contract, CrossContractCallContract, CrossContractCallContractClient};, which imports the types from the lib.rs file. Since Rust modules are called crates, we import the crates from the contract implementation to use them in tests. Or, we could just use use super::* as previous tests did.

Using Specific Classes vs All Classes

Using use super::* instead of importing the crates used one by one might look easier, but later could cause some serious headache when the code grows in complexity. If we import everything from the parent implementation, we would have to use the names, types, and anything implemented on that contract only. For example, we could have problems if we tried to import another implementation of the storage_contract with additional functionality for testing.

Since we have examined lots of tests until now, I think you now understand what this test does. It first deploys the storage_contract and the cross_call_contract into the testing sandbox we use, then, creates a test user to put a value into the storage_contract. After putting a value, we can now assert that our cross_call_contract returns the same result that was put into the storage_contract.

Solving the Quest

Since we are tasked with deploying the DataStoreContract again, let's use the commands we used on the Auth Store quest to deploy them using current quest's account.

cargo build --target wasm32-unknown-unknown --release
soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_auth_store_contract.wasm
Enter fullscreen mode Exit fullscreen mode

After we deploy the DataStoreContract, let's invoke the put function to put some data into the contract.

soroban invoke \
--id [the ID of your DataStoreContract] \
--fn put \
--arg [hexadecimal value longer than 10 bytes]
Enter fullscreen mode Exit fullscreen mode

Deploy Data Store and Put Data

The first part of the quest is done! For the second part, we should first deploy the CrossContractCallContract to Soroban Futurenet.

soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_call_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Cross Contract Call Contract Deployment

Then, we need to find what the required function arguments are. The first argument we need to supply is store_id, which is of type BytesN<32>. This corresponds to the DataStoreContract's ID, which consists of 32 bytes, represented in hexadecimal values. The second argument is owner, of type AccountId. Recall that we used the AccountId type when solving the second quest using the get function. The idea is the same, we should create the JSON representation of the complex type AccountId. We can use the same Python script to find out our raw public key in hexadecimal format.

soroban invoke \
--id [the ID of your CrossContractCallContract] \
--fn inv_get \
--arg [the ID of your DataStoreContract] \
--arg '{"object": { "accountId": { "publicKeyTypeEd25519": "[your raw public key in hexadecimal format]" } } }'
Enter fullscreen mode Exit fullscreen mode

Successful Inv Get Call

Success! That was quite hard, and you should be proud of yourself. This quest combined almost all of the concepts of previous quests, so your success is well deserved! Use sq check 4 to claim your reward 🎉!

Top comments (1)

Collapse
 
altug profile image
Altuğ Bakan • Edited

Hope you enjoyed this post!

If you want to support my work, you can send me some XLM at

GB2M73QXDA7NTXEAQ4T2EGBE7IYMZVUX4GZEOFQKRWJEMC2AVHRYL55V
Stellar Wallet

Thanks for all your support!