Soroban Series 1: Building a Payment Streaming App
Tags: soroban, stellar, rust, blockchain
Published: 2023-08-22 21:05:46

Streaming payments are a convenient way for users to pay a beneficiary over a period of time while ensuring that at every point in time only the value accrued can be withdrawn by the beneficiary.

In this article I will be building a decentralized application on the Stellar Network with Soroban. 

Here is the link to the complete source code: [Github](https://github.com/TosinShada/tide-soroban-contract).

## The Backend
The first step to creating a Soroban smart contract is to setup the environment as described [here](https://soroban.stellar.org/docs/getting-started/setup). 
Once the environment is set up, you should use cargo to start a new project. You can also clone an existing repository like this one and use it as a starter project.

**Nb: Given the fact that Soroban is still actively being developed with no stable release a lot of the patterns in this article might not be supported fully in the nearest future.**

### - Using the Latest Soroban CLI Version
In order to use the latest release of Soroban CLI with all the new features available we will add the below code to the config.toml file in the .cargo directory. 

``` toml
[alias] # command aliases
install_soroban = "install --git https://github.com/stellar/soroban-tools --rev 9b75c00fd19c0baf379c84668d90bfe444734173 --root ./target soroban-cli --debug"
```

The '9b75c00fd19c0baf379c84668d90bfe444734173' is the git commit id of the latest release of Soroban CLI in the Github repository.
After making this modification, you can follow the steps outlined [here](https://github.com/stellar/soroban-example-dapp) to install the necessary dependencies.

### - Building the Streaming Contract
The Payment Streaming contract will be centred around the Stream Struct that will hold important information about a payment stream

``` rust
#[derive(Clone, Debug)]
#[contracttype]
pub struct Stream {
    pub id: u32,
    pub sender: Address,
    pub recipient: Address,
    pub start_time: u64,
    pub stop_time: u64,
    pub deposit: i128,
    pub rate_per_second: u64,
    pub remaining_balance: i128,
    pub token_address: Address,
    pub token_symbol: String,
    pub token_decimals: u32,
}
```
We will then create the contract struct and implement it 
``` rust
#[contract]
struct StreamToken;

#[contractimpl]
#[allow(clippy::needless_pass_by_value)]
impl StreamToken {
    // Implement the contract
}
```
Before implementing the contract, we will have to create DataKeys that will be used to store out states.

``` rust 
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Token,
    NextStreamId,
    Streams(u32),
    UserStreams(Address),
}
```
The token is the default token address supported by the contract, the Streams key is a mapping of the Streams to the Stream Id while the UserStreams key is a mapping of a user's address to a collection of streams.

Every smart contract in Soroban must be initialized with the `pub fn initialize(e: Env)` initialization function. 

``` rust
pub fn initialize(e: Env, token: Address, start_id: u32) {
    assert!(
        !e.storage().instance().has(&DataKey::NextStreamId),
        "already initialized"
    );
    e.storage()
        .instance()
        .set(&DataKey::NextStreamId, &start_id);
    e.storage().instance().set(&DataKey::Token, &token);
    e.storage().instance().bump(INSTANCE_BUMP_AMOUNT);
}
```
In the above code, we are checking whether a stream Id exist which will signify that the contract has already been initialized. If not, we are going to set the token to be used in this streaming contract and the start id.
To better understand how states are stored, you can check out these Soroban articles. 

- [Persisting Data in Soroban](https://soroban.stellar.org/docs/fundamentals-and-concepts/persisting-data)
- [State Expiration](https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration)

We will then implement some get functions to get the stream by Id and the stream by User Address

``` rust 
fn get_stream(e: &Env, stream_id: &u32) -> Stream {
    e.storage()
        .persistent()
        .get::<_, Stream>(&DataKey::Streams(stream_id.clone()))
        .expect("invalid stream id")
}

fn get_streams_by_user(e: &Env, who: &Address) -> Vec<Stream> {
    e.storage()
        .persistent()
        .get::<_, Vec<Stream>>(&DataKey::UserStreams(who.clone()))
        .unwrap_or(Vec::new(&e))
}
```
These functions will be exposed in the contract implementation as shown below
``` rust
pub fn get_stream(e: Env, stream_id: u32) -> Stream {
        get_stream(&e, &stream_id)
    }

    pub fn get_streams_by_user(e: Env, caller: Address) -> Vec<Stream> {
        get_streams_by_user(&e, &caller)
    }
```

One of the core ability of the streaming contract is the way the balance of a user is dynamically generated and changes at each point within the streaming interval. 

``` rust
fn get_balance_of(e: &Env, caller: &Address, stream: &Stream) -> i128 {
    let start_time = stream.start_time;
    let stop_time = stream.stop_time;
    let current_ledger_time = get_ledger_timestamp(&e);
    let delta = get_delta_of(&start_time, &stop_time, &current_ledger_time);

    let mut local_balance = LocalBalance {
        recipient_balance: 0,
        withdrawal_amount: 0,
        sender_balance: 0,
    };
    local_balance.recipient_balance = (delta * stream.rate_per_second) as i128;

    // If the stream `balance` does not equal `deposit`, it means there have been withdrawals.
    // We have to subtract the total amount withdrawn from the amount of money that has been streamed until now.
    if stream.deposit > stream.remaining_balance {
        local_balance.withdrawal_amount = stream.deposit - stream.remaining_balance;
        check_nonnegative_amount(local_balance.withdrawal_amount);
        local_balance.recipient_balance =
            local_balance.recipient_balance - local_balance.withdrawal_amount;
        check_nonnegative_amount(local_balance.recipient_balance);
    }

    if caller.clone() == stream.recipient {
        local_balance.recipient_balance
    } else if caller.clone() == stream.sender {
        local_balance.sender_balance = stream.remaining_balance - local_balance.recipient_balance;
        check_nonnegative_amount(local_balance.sender_balance);
        local_balance.sender_balance
    } else {
        0
    }
}
```
This function will also be called in the contract implementation
``` rust
    /// Returns the amount of tokens that have already been released to the recipient.
    /// Panics if the id does not point to a valid stream.
    /// @param stream_id The id of the stream
    /// @param who The address of the caller
    /// @return The amount of tokens that have already been released
    pub fn balance_of(e: Env, stream_id: u32, caller: Address) -> i128 {
        let stream = get_stream(&e, &stream_id);

        get_balance_of(&e, &caller, &stream)
    }
```

To create the stream, we are going to make a bunch of assertions and panic if any of those assertions fails, then we are going to create a mutable struct to hold the duration and rate per second to be calculated from the provided start and end time.
There will also be a cross contract call to the token contract to get the token symbol and decimal. To learn more about cross contract calls in Soroban, you can check out [this article](https://soroban.stellar.org/docs/basic-tutorials/cross-contract-call).

Once all the required information have been gotten, we will then create the Stream struct and persist it in the Soroban storage.

We also have the capability to withdraw from the stream for both the sender and receiver as implemented below

``` rust
pub fn withdraw_from_stream(e: Env, recipient: Address, stream_id: u32, amount: i128) {
        recipient.require_auth();
        assert!(amount > 0, "amount is zero or negative");
        assert!(
            recipient != e.current_contract_address(),
            "stream to the contract itself"
        );

        let mut stream = get_stream(&e, &stream_id);
        require_sender_or_recipient(&stream, &recipient);

        let balance = get_balance_of(&e, &recipient, &stream);
        assert!(balance >= amount, "amount exceeds the available balance");

        stream.remaining_balance = stream.remaining_balance - amount;

        if stream.remaining_balance == 0 {
            remove_stream(&e, &stream_id);
        } else {
            set_stream(&e, &stream_id, &stream);
        }

        transfer(&e, &recipient, &amount);

        events::withdraw_from_stream(&e, recipient, stream_id, amount);
    }
```

The above code will allow the receiver to withdraw up to the amount that has been streamed and the sender to withdraw the balance remaining.

To cancel the stream, we have implemented a cancel_stream function that can only be called by either the sender or receiver

``` rust
    pub fn cancel_stream(e: Env, caller: Address, stream_id: u32) {
        caller.require_auth();
        let stream = get_stream(&e, &stream_id);
        require_sender_or_recipient(&stream, &caller);

        let sender_balance = get_balance_of(&e, &stream.sender, &stream);
        let recipient_balance = get_balance_of(&e, &stream.recipient, &stream);

        remove_stream(&e, &stream_id);

        if recipient_balance > 0 {
            transfer(&e, &stream.recipient, &recipient_balance);
        }
        if sender_balance > 0 {
            transfer(&e, &stream.sender, &sender_balance);
        }

        events::cancel_stream(
            &e,
            stream_id,
            stream.sender,
            stream.recipient,
            sender_balance,
            recipient_balance,
        );
    }
```

We will also implement some tests functions to confirm that our code is working as it should.

## The Frontend
The frontend was put together with [NextJS](https://nextjs.org) and [Shadcn](https://ui.shadcn.com). While the main focus of the application was the backend, lets look at the hook that was used to show how someone can call the create stream function as well as get the streams mapped to his wallet account.

## Wrapping Up
Soroban is a new smart contract platform that is built on a well tested blockchain infrastructure ([Stellar](https://stellar.org)). While the ecosystem is still new, some interesting projects are already springing up and I personally will be looking forward to a better developer experience especially with regards to querying on-chain data and listening to events emitted from the smart contracts.