Published on

Previewing The Mercury Serverless Functions: Extract Anything from the Ledger, Process The Data, and Query It.

Authors

On top of providing a way to build custom ingestion flows and write to the database for every time a new ledger closes (which even if it's not public yet is already used by some selected projects), we're also aiming to introduce in the Mercury ecosystem the so-called serverless functions.

These functions are executed on-demand and have access to the whole current state of the Stellar ledger and can, like regural Zpehyr functions, do anything they want with it (within reasonable bounds), and finally expose the results in an API.

The underlying difference between Zephyr ingesters and functions is the logic of the programs themselves and the way they are executed. One is run for every new ledger and won't return anything (it communicates by writing to the database) and the other is executed on demand through an API request ad is returning a user-defined result.

This enables for a lot of interesting use cases, from stuff like AMM aggregators to additional indexing capabilities for protocols.

All of this by just deploying a Mercury serverless function, no setup required! This aligns with Mercury's goal of providing efficient and advanced data retrieval for projects and users that prefer not to or don't have the resources to manage and maintain their own infrastructure, overall helping the Soroban and Stellar ecosystem apps to scale.

How It Works

Mercury serverless functions are WebAssembly binaries that get instantiated and executed by our very own Zephyr Virtual Machine.

Users will be able to build logic that retrieves data from the ledger itself (for example env.get_lp_by_pair(...)), process this data (for example compare prices with soroban prices or with offers) and then return a response with all the information the developer wants to share with the callers.

All of this runs in our servers and the VM execution enables for it to be safe and resource-constrained (though we're not on-chain here, so limits are obviously much higher).

Preview

We have started working towards implementing all the functionalities which the VM needs in order to safely allow for such functions on Mercury, and while there's still a lot of work to be done, we're sharing with the community the current functionality so that we can also start gathering feedback, feature requests, or ideas!

To make a favor to the non-coders, we'll start with the actual preview i.e the result of executing a Mercury Serverless function.

Final result

The goal here is to showcase how we can easily obtain holders with balance higher than 5.000 XLM on xycLoans's XLM pool.

And we want a respose with a structure like:

{
    "entries": ["Entries here"],
    "count": "Count of top holders here"
}

As you can see we've retrieved all the entries that have a balance higher than 5.000 XLM on that pool contract. This is of course not yet much, but gives an idea of that Mercury functions can do.

Now, onto the creation of the function.

The Function

The function's code is this one:

use rs_zephyr_sdk::{utils, EnvClient};
use serde::{Deserialize, Serialize};
use stellar_xdr::next::{LedgerEntry, LedgerEntryData, ScSymbol, ScVal};

#[derive(Deserialize, Serialize)]
pub struct Result {
    entries: Vec<LedgerEntry>,
    count: usize,
}

#[no_mangle]
pub extern "C" fn top_holders() {
    let env = EnvClient::empty();
    let contract_id = stellar_strkey::Contract::from_string(
        "CARDOVHUIQVBDUKEYKCS4YDFFM7VSAHIMKCZ57NZKS6CT7RBEZNRKKL5",
    )
    .unwrap()
    .0;

    let entries = env.read_contract_entries(contract_id).unwrap();

    let top_holders: Vec<LedgerEntry> = entries
        .iter()
        .filter_map(|entry| {
            if let (ScVal::Vec(Some(scvec)), LedgerEntryData::ContractData(data)) =
                (&entry.key, &entry.entry.data)
            {
                if let Some(val) = scvec.get(0) {
                    if val == &ScVal::Symbol(ScSymbol("Balance".try_into().unwrap())) {
                        if let ScVal::I128(parts) = &data.val {
                            if utils::parts_to_i128(parts) >= 50_000_000_000 {
                                return Some(entry.entry.clone());
                            }
                        }
                    }
                }
            }
            None
        })
        .collect();

    env.conclude(Result {
        count: top_holders.len(),
        entries: top_holders,
    });
}

Not yeat the SDK experience we're aiming for, but I think that it's agreeable that this is not too complex.

From here you can just build the program targeting a wasm build and deploy!


Thanks for reading! We've shared this preview mainly for gathering feedback so if you have any, don't hesitate to let us know.