Soroban Quest - Asset Interop
Tags: soroban, stellar, tutorial
Published: 2022-12-04 06:00:00

So, we have tackled Custom Types on the [last post](https://dev.to/altug/soroban-quest-custom-types-5d54) on Soroban and learned a lot. Now, on this last quest, we will use our knowledge from all of the previous quests to dive into Asset Interop!

## Understanding `README.md`

For the last time, let's check out the `README.md` file.

![README](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfd14mbnfp80814y1pol.png)

It is quite the quest! We will use Stellar's "Classic" Assets, Soroban Tokens, and even import some Lumens from Stellar network to Soroban network!

## Setting Up Our Quest Account

At this point, you should be able to start playing pretty easy! If you need a reminder, we should use `sq play 6`, and fund our account to start playing the 6<sup>th</sup> and last quest.

## Token Interface

On Soroban, tokens should implement the Token Interface if they want to interoperate between other contracts and protocols. Having a common interface for contracts help developers to support all types of tokens implementing the same interface without knowing their implementations. 

The Token Interface is defined as

```rust
// The metadata used to initialize token (doesn't apply to contracts representing 
// 'classic' Stellar assets).
pub struct TokenMetadata {
    pub name: Bytes,
    pub symbol: Bytes,
    pub decimals: u32,
}

// Initializes a 'smart-only' token by setting its admin and metadata.
// Tokens that represent 'classic' Stellar assets don't need to call this, as
// their metadata is inherited from the existing assets.
fn init(env: Env, admin: Identifier, metadata: TokenMetadata);

// Functions that apply on for tokens representing classic assets.

// Moves the `amount` from classic asset balance to the token balance of `id`
// user.
// `id` must be a classic Stellar account (i.e. an account invoker or signature 
// signed by an account).
fn import(env: Env, id: Signature, nonce: BigInt, amount: i64);

// Moves the `amount` from token balance to the classic asset balance of `id`
// user.
// `id` must be a classic Stellar account (i.e. an account invoker or signature 
// signed by an account).
fn export(env: Env, id: Signature, nonce: BigInt, amount: i64);

// Admin interface -- these functions are privileged

// If "admin" is the administrator, burn "amount" from "from"
fn burn(e: Env, admin: Signature, nonce: BigInt, from: Identifier, amount: BigInt);

// If "admin" is the administrator, mint "amount" to "to"
fn mint(e: Env, admin: Signature, nonce: BigInt, to: Identifier, amount: BigInt);

// If "admin" is the administrator, set the administrator to "id"
fn set_admin(e: Env, admin: Signature, nonce: BigInt, new_admin: Identifier);

// If "admin" is the administrator, freeze "id"
fn freeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier);

// If "admin" is the administrator, unfreeze "id"
fn unfreeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier);

// Token Interface

// Get the allowance for "spender" to transfer from "from"
fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt;

// Set the allowance to "amount" for "spender" to transfer from "from"
fn approve(e: Env, from: Signature, nonce: BigInt, spender: Identifier, amount: BigInt);

// Get the balance of "id"
fn balance(e: Env, id: Identifier) -> BigInt;

// Transfer "amount" from "from" to "to"
fn xfer(e: Env, from: Signature, nonce: BigInt, to: Identifier, amount: BigInt);

// Transfer "amount" from "from" to "to", consuming the allowance of "spender"
fn xfer_from(
    e: Env,
    spender: Signature,
    nonce: BigInt,
    from: Identifier,
    to: Identifier,
    amount: BigInt,
);

// Returns true if "id" is frozen
fn is_frozen(e: Env, id: Identifier) -> bool;

// Returns the current nonce for "id"
fn nonce(e: Env, id: Identifier) -> BigInt;

// Descriptive Interface

// Get the number of decimals used to represent amounts of this token
fn decimals(e: Env) -> u32;

// Get the name for this token
fn name(e: Env) -> Bytes;

// Get the symbol for this token
fn symbol(e: Env) -> Bytes;
```

on the [Soroban SDK](https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-token-spec/src/lib.rs). It uses some cryptographic tools and some helpful interfaces from the `soroban-auth` crate, which is a crate that contain libraries for user authentication on the Soroban network.

The token interface is quite large but it is not hard to understand, so I urge you to check it out yourself and see if you can make sense of it. We will inspect the parts we will use on the token interface in quite the detail when we need them.

## Examining the Contract

Once again, let's check out the `lib.rs` file. The source code for this contract is quite large, and pretty complex. Let's check out some important parts of the source file.

```rust
use soroban_auth::{Identifier, Signature};
```

On this contract, we are dealing with user authentication, so we need the `soroban_auth` crate to add this functionality to our contract. This crate helps us get more details about the input arguments of some functions that the Token Interface use.

```rust
mod token {
    soroban_sdk::contractimport!(file = "./soroban_token_spec.wasm");
}
```

This `mod` statement imports the `soroban_token_spec.wasm` file to make the compiler infer the types and structs from the Token Interface. This is useful as we did not have to copy and paste the source code of the token interface of the contract to a new file and import it.

### Allowance Trait

We have a `trait` in our code, which defines the `AllowanceTrait` interface. The contracts implementing the `AllowanceTrait` should implement two functions, `init` and `withdraw`, to successfully compile.

```rust
pub trait AllowanceTrait {
    fn init(
        e: Env,
        child: AccountId,
        token_id: BytesN<32>,
        amount: BigInt,
        step: u64,
    ) -> Result<(), Error>;

    fn withdraw(e: Env) -> Result<(), Error>;
}
```

The `init` function has four external arguments. The `child` argument sets the "Child" account that can withdraw some money from the "Parent" account. `token_id` arguments is the ID of the token that the Child account can withdraw, `amount` is how much of the said token that Child account withdraw for a year, and `step` is how frequent the child account can withdraw, in seconds.

The `withdraw` function does not have an external argument and returns success or a custom `Error` if the the withdrawal fails.

We can see that our `AllowanceContract` implements the `AllowanceTrait` trait, so it should implement those two functions in addition to whatever it might implement. Let's check out the implementations of the `init` and `withdraw` functions so that we have a good understanding about what each of them do.

### `init` Function

The `init` function is defined as

```rust
fn init(
        e: Env,
        child: AccountId,
        token_id: BytesN<32>,
        amount: BigInt,
        step: u64,
    ) -> Result<(), Error> {
        let token_key = DataKey::TokenId;
        if e.data().has(token_key.clone()) {
            return Err(Error::ContractAlreadyInitialized);
        }

        if step == 0 {
            return Err(Error::InvalidArguments);
        }

        if (&amount * step) / SECONDS_IN_YEAR == 0 {
            return Err(Error::InvalidArguments);
        }

        e.data().set(token_key, token_id);
        e.data()
            .set(DataKey::Parent, to_account(e.invoker()).unwrap());
        e.data().set(DataKey::Child, child);
        e.data().set(DataKey::Amount, amount);
        e.data().set(DataKey::Step, step);

        Ok(())
    }
```

The contract has to be initialized only once. The first thing that the `init` function does is checking this case. If the environment already has a `TokenId` defined, which can be only done on the `init` function, it means that the contract has already been initialized, and the `init` function returns an `Error`.

After a sanity check, the contract validates the numerical arguments. It first checks if the `step` value is not 0, as division by 0 on calculations would give an error, and you can't *really* withdraw every 0 seconds. After that, it checks if `(&amount * step) / SECONDS_IN_YEAR` is not zero. Since we are supplying an amount that is for a whole year, we can calculate how much the child account can withdraw at a time using the calculation given.

Then, the function sets up some data on the environment for later use. Lastly, the contract sets up the time of the initial withdraw to a value in the past, so that the Child can withdraw at any time starting now.

> #### Ledger Data
>
> We have utilized the contract data quite much, but how about some network-wide values that do not depend on any contract? We can read the values from the Stellar Ledger using the `ledger` method of the environment.
>
> The Ledger has information about the `protocol_version`, `sequence`, `timestamp` of the closing time, and the `network_passphrase` available for all contracts to read. This information is useful as we can create a contract that *unlocks* at a future time, which is implemented using the `timestamp` value of the Ledger. Check out the [docs on soroban_sdk](https://docs.rs/soroban-sdk/latest/soroban_sdk/ledger/struct.Ledger.html) for more information.

### `withdraw` Function

The withdraw function is implemented as

```rust
fn withdraw(e: Env) -> Result<(), Error> {
        let key = DataKey::TokenId;
        if !e.data().has(key.clone()) {
            return Err(Error::ContractNotInitialized);
        }

        let token_id: BytesN<32> = e.data().get(key).unwrap().unwrap();
        let client = token::Client::new(&e, token_id);

        match e.invoker() {
            Address::Account(id) => id,
            _ => return Err(Error::InvalidInvoker),
        };

        let step: u64 = e.data().get(DataKey::Step).unwrap().unwrap();
        let iterations = SECONDS_IN_YEAR / step;
        let amount: BigInt = e.data().get(DataKey::Amount).unwrap().unwrap();
        let withdraw_amount = amount / iterations;

        let latest: u64 = e.data().get(DataKey::Latest).unwrap().unwrap();
        if latest + step > e.ledger().timestamp() {
            return Err(Error::ChildAlreadyWithdrawn);
        }

        client.xfer_from(
            &Signature::Invoker,
            &BigInt::zero(&e),
            &Identifier::Account(e.data().get(DataKey::Parent).unwrap().unwrap()),
            &Identifier::Account(child),
            &withdraw_amount,
        );

        let new_latest = latest + step;
        e.data().set(DataKey::Latest, new_latest);

        Ok(())
    }
```

The `withdraw` function also starts with a sanity check, this time it requires the contract to be initialized, as it needs to know the parameters on what will it work on.

After the check, it creates a `client` which, if you remember, is an interface for the function to interact with another contract. This time, the interface is any contract that implements the Token Interface.

After that, it checks the caller is an EOA and not another contract. This is not required, but used for our purposes.

Next, it does some calculations like how much to withdraw at a time, who is the Child account, and such using the stored data on the environment. It also checks if the latest withdraw is before the allowed interval, and returns an error otherwise.

Finally, it sends the required amount to the Child account using the `xfer_from` function of the token contract. This function transfers tokens from the Parent account to the Child account. To accomplish this task, the Parent account should allow the use of its tokens by this contract. We will accomplish that task later on the quest.

## Examining the Test

The `test.rs` file has a test using the USD Coin contract to create an allowance for the Child account, approving the contract and so on. Please inspect the test functions to see if you can make sense of them. One thing to note here is the use of

```rust
env.ledger().set(LedgerInfo {
        timestamp: 1669726145,
        protocol_version: 1,
        sequence_number: 10,
        network_passphrase: Default::default(),
        base_reserve: 10,
    });
```

function to set the state of the Soroban Ledger for testing purposes. Since the functions depend on the timestamps of the Ledger, we need a way of testing instead of waiting the required amount of time!

Again, we can use `cargo test` to test the validity of the tests.

## Python Scripts

You will see a folder named `py-scripts` on the quest folder that contains some Python scripts. These scripts are supplied by [overcat](https://github.com/overcat), and are ways to interact with the Soroban network using the Stellar Python SDK. We won't be using these scripts, but feel free to check them out if you are interested with interacting with Soroban using Python.

## Solving the Quest

So, our tasks for the quest are

- Deploy the `AllowanceContract` as the `Parent_Account`
- Invoke the `init` function of the `AllowanceContract`
- `import` some XLM into the `Parent_Account` from their "Classic" Stellar account
- `approve` the `AllowanceContract` to make proxy transfers from the `Parent_Account` to the `Child_Account`
- Invoke the `withdraw` function of the `AllowanceContract` with either the `Child_Account` or `Parent_Account`
- `export` some XLM from the `Child_Account` to their "Classic" Stellar account

Let's start with them.

### Deploying the Contract

As usual, we can build and deploy the contract using

```bash
cargo build --target wasm32-unknown-unknown --release
soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_asset_interop_contract.wasm
```

![Contract Deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1angs0wvxpznxq9yk0ut.png)

Note your contract ID somewhere, as we will need it for the next steps of the quest.

## Invoking the `init` Function

At this point, we are used to the custom types that we have to supply to the `soroban` CLI, so invoking the `init` function shouldn't be that hard. The `init` function has the arguments of type `AccountId`, `BytesN<32>`, `BigInt`, and `u64`.

We have worked with the all those types except `BigInt`, which is not any different than supplying a `u64` variable. The only difference is that the `BigInt` type can be a quite large number, even larger than the `u64` variable.

We need to create a Child account before invoking the `init` function, as we need to supply the value as an argument.

The `stellar_sdk` on Python has all the functionality we need to create an account and view its public key in hexadecimal. Note that for this Quest, we need some additional Soroban compatibility on `stellar_sdk`, so we need to install a specific version of the `stellar_sdk`. We might already have a version of `stellar_sdk`, so use the following command to change your version of the `stellar_sdk` to a Soroban enabled one.

```bash
pip install git+https://github.com/StellarCN/py-stellar-base.git@soroban --upgrade --force-reinstall
```

To create the Child account and get its public key in hexadecimal, we will use

```python
from stellar_sdk import Keypair

child = Keypair.random()

print(f"Public Key: {child.public_key}")
print(f"Private Key: {child.secret}")
print(f"Public Key in Hex: {child.raw_public_key().hex()}")
```

Note these values somewhere to not forget them as we will need those later.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qdkx0crj354hi1ne4xhg.png)

We also need to find the contract ID of the native Stellar token on Soroban network. Since the contract IDs of imported tokens are *deterministic*, we can use the same calculation to determine the address. We will use

```python
import hashlib
from stellar_sdk import Asset, xdr

asset = Asset.native()

data = xdr.HashIDPreimage(
	xdr.EnvelopeType.ENVELOPE_TYPE_CONTRACT_ID_FROM_ASSET,
    from_asset=asset.to_xdr_object(),
)

print(f"Native Token ID: {hashlib.sha256(data.to_xdr_bytes()).hexdigest()}")
```

Note the XLM token's contract ID, which should be `71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7`, somewhere since we will use it later.

![XLM Contract ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jzodnb5hdukygdgu9a7.png)

Now that we have all the information we need, Let's invoke the `init` function of our contract.

```bash
soroban invoke \
--id [your contract id] \
--fn init \
--arg '{"object":{"accountId":{"publicKeyTypeEd25519":"[public key of your child account in hexadecimal]"}}}' \
--arg [the XLM token contract address] \
--arg [the allowance that you would like to give in stroops] \
--arg [the withdrawal rate in seconds]
```

We should get a success reply if our arguments are correct. Note that the allowance is in `stroop`s, and a `stroop` is a 10 millionth of a Lumen.

![Invoking Init Function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ynqpki0007f8wyhqd2bf.png)

I allowed the Child account to withdraw 500 Lumens yearly, once every week (60 * 60 * 24 * 7 = 604800).

### `import`ing XLM to Parent Account

Now, we have to import some "Classic" Lumens from the Stellar network to Soroban network, so that the Child account can withdraw them. We will use the `import` function of the token contract, as it implements the Token Interface that we discussed above. As a reminder, the definition of the `import` function is

```rust
fn import(env: Env, id: Signature, nonce: BigInt, amount: i64);
```

We have a new `Signature` type that we need to construct as an argument. The `Signature` type is a *proof* indicating the signer is giving permission to the function invoker the required permissions. Since we will import for ourselves, we can use the "Invoker" argument to tell the contract that we are doing it for ourselves. Since we are the invoker of this function, the contract knows that we possess the control of the account's secret key, and allows us to accomplish whatever we would like to do.

The `nonce` parameter is used for signature validation. Since we are not supplying a signature, we can just pass 0.

> #### Signature `nonce`s
>
> Let's say we have approved a third party to take 100 tokens from us by giving them a signature indicating the approval. What keeps them from just using the signature again and again to take 100s of tokens from us? This is called a "Signature Replay Attack", and should be migitated with making signatures "one use only".
>
> Adding a `nonce` parameter to the message we are signing, assuming the contract has the correct implementation, will prevent Signature Replay Attacks, as now the contract will check if the supplied nonce is one higher than the previous.

So, we can call the `import` function on the XLM Token Contract using

```bash
soroban invoke \
--id 71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7 \
--fn import \
--arg '{"object":{"vec":[{"symbol":"Invoker"}]}}' \
--arg 0 \
--arg [amount in stroops]
```

Note that the amount is again in `stroop`s, meaning that we have to add seven 0s to convert them to Lumens.

![Importing Lumens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2pkd08iglycx6i07rv1e.png)

We can go a step further and check out the amount of Lumens we have using the `balance` function on the XLM Token Contract. The definition of the `balance` function is

```rust
fn balance(e: Env, id: Identifier) -> BigInt;
```

The `Identifier` type is a type that indicates if the ID argument is from an EOA or a Contract. Since we are an EOA with a Public and Private key pair, we should use the `Account` identifier on our argument. If you forgot how to calculate your account ID in hexadecimal using Python, here's a reminder:

```python
from stellar_sdk import Keypair

print(Keypair.from_public_key("[your public key]").raw_public_key().hex())
```

Let's invoke the `balance` function of the XLM Token Contract using

```bash
soroban invoke \
--id 71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7 \
--fn balance \
--arg '{"object":{"vec":[{"symbol":"Account"},{"object":{"accountId":{"publicKeyTypeEd25519":"[your public key in hexadecimal]"}}}]}}'
```

We will get a reply indicating our balance in `stroop`s.

![Parent Account's Balance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rm8xkeirqbukyqb6z921.png)

### `approve`ing the `AllowanceContract`

We now should permit the `AllowanceContract` to use our Lumens. Note that we should not permit the Child account instead, as they should only be able to `withdraw` using our contract.

The `approve` function's definition is

```rust
fn approve(e: Env, from: Signature, nonce: BigInt, spender: Identifier, amount: BigInt);
```

This time the `Identifier` will be of type contract, which will have the contract ID of our deployed contract as an input. Since we already know how to construct the other arguments, we can call the `approve` function using

```bash
soroban invoke \
--id 71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7 \
--fn approve \
--arg '{"object":{"vec":[{"symbol":"Invoker"}]}}' \
--arg 0 \
--arg '{"object":{"vec":[{"symbol":"Contract"},{"object":{"bytes":"[your contract id]"}}]}}' \
--arg [amount to approve]
```

If you approve less than a `withdraw` function would take at a time, it will fail. We can `approve` the same amount of tokens we have allowed.

![Approval](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89oakxw40p8uu5v2dw9n.png)

### `withdraw`ing to the Child Account

After setting up the contract and giving out permissions, we now can invoke the `withdraw` function in our contract using either the child account or our account. Note that anyone can call the `withdraw` function, but the funds will only go to the Child account. For a difference, let's invoke the contract from the Child account. We just need to supply the Child account's Secret Key to `soroban` CLI to invoke it from the Child account.

```bash
soroban invoke \
--id [your contract id] \
--fn withdraw \
--secret-key [child account's secret key]
```

We should see a success denoting the withdrawal has succeeded.

![Withdraw Function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78j4dvzbj8szkyoshqwk.png)

Now, let's check out the balance of the child account to see if the withdrawal actually worked.

```bash
soroban invoke \
--id 71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7 \
--fn balance \
--arg '{"object":{"vec":[{"symbol":"Account"},{"object":{"accountId":{"publicKeyTypeEd25519":"[child account's public key in hexadecimal]"}}}]}}'
```

We should see a non-zero value for the Child account's balance! This value is also in `stroop`s and will change with the arguments you have supplied to the `init` function.

![Child Account's Balance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0gne8pv5vaoui8hscw4x.png)

### `export`ing Lumens Back

Since the Child account has some Lumens now, we can `export` them back to the Stellar network using the `export` command of the XLM Token Contract. The `export` function is defined as

```rust
fn export(env: Env, id: Signature, nonce: BigInt, amount: i64);
```

Similar to the `import` function, we will build these arguments as JSON objects, and invoke the `export` function from the Child account.

```bash
soroban invoke \
--id 71c670db8b9d9dd3fa17d83bd98e4a9814f926121972774bd419fa402fe01dc7 \
--fn export \
--arg '{"object":{"vec":[{"symbol":"Invoker"}]}}' \
--arg 0 \
--arg 100000 \
--secret-key [child account's secret key]
```

Note that we can only export a maximum of the amount of Lumens that we have.

![Exporting Lumens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mwzzvlzpep0xnnclglor.png)

Success! Use `sq check 6` to claim your reward 🎉!

## Finishing Up

This was the last quest of this series, and definitely was the hardest one to finish. You should be proud of yourself! We have accomplished many feats using the Soroban network, and learned a lot about blockchain design, cryptography primitives, and programming in general.

I hope that you have enjoyed these posts, and learned some new information. The Stellar Quest series is always an incredible place to learn, so keep going on! Solve the Learn Quests, the Side Quests, and read the documentation.

I hope to see you again for the next quest. See you around!