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.
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)
}
}
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;
}
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
, andbalance_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
}
If we fail to implement the inv_get
function, the Rust compiler would give an error such as
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)
}
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]
);
}
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 crate
s, we import the crate
s 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 thecrate
s 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 thestorage_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 put
ting 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
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]
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
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]" } } }'
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)
Hope you enjoyed this post!
If you want to support my work, you can send me some XLM at
GB2M73QXDA7NTXEAQ4T2EGBE7IYMZVUX4GZEOFQKRWJEMC2AVHRYL55V
Thanks for all your support!