Custom RPC-alike Endpoints

Learn how you can set from your program a RPC-alike endpoint for ledger entries retrieval and contract call simulation.

Through serverless functions, it is possible to set endpoints that have the same functionality you could find in a RPC provider. Those are contract call simulation and on-demand getLedger APIs.

No need to pay for an external RPC provider to simulate transactions in your project when already using Mercury for the indexing!

Contract Call Simulation

Simulating a contract call with Zephyr is quite straightforward: you just have to set up a serverless function, as seen before and as you can see in the example, and call the env.simulate_contract_call_to_tx() function. The function takes the following arguments:

  • the account that makes the contract call

  • the ledger sequence

  • the contract address

  • the function to call

  • the arguments to call the function with

To return the response we use env.conclude.

#[no_mangle]
pub extern "C" fn simulate() {
    let env = EnvClient::empty();
    let request: SimulationRequest = env.read_request_body();

    let response = match request {
        SimulationRequest::Send(SimulateSend { from, sequence, message }) => {
            let address = Address::from_string(&SString::from_str(&env.soroban(), &from));
            let message = Bytes::from_slice(&env.soroban(), message.as_bytes());
            env.simulate_contract_call_to_tx(
                from,
                sequence,
                CONTRACT_ADDRESS,
                Symbol::new(&env.soroban(), "send"),
                vec![
                    &env.soroban(),
                    address.into_val(env.soroban()),
                    message.into_val(env.soroban()),
                ],
            )
            
        },
        // other match arms...
    }
    env.conclude(response)
}

In this example, taken from the on-chain stellar complaints indexer, we use match to simulate different functions based on the request. We suggest to have a look at the full example to better understand the code. Here we are simulating the "send" function of the contract, which takes as arguments an address and a message in bytes. To learn more about the data types conversion, head to the working with data section, but just note here that the args you pass to the contract function need to be ScVals.

Contract Calls

Once the transaction is simulated, you can even perform the contract call by making a web request to Horizon.

Advantages of Simulating with Zephyr

It was already said that if already using Zephyr, the user doesn't need to pay for an external RPC provider to simulate its contract calls, having everything in one place and saving some costs.

Moreover, by setting your custom simulation endpoint in a Zephyr program, you can leverage the Soroban/Rust type system, which is more straightforward and user-friendly. You have the flexibility to configure your endpoint as desired, allowing you to define the argument types for function parameters and handle the conversion within the program.

On-Demand getLedger API

Another important feature generally offered by RPC providers is that of providing access to the current ledger state by retrieving ledger entries on demand. As already seen in Accessing the Ledger, this can be easily done also from within a Zephyr program. The only difference now is that we also can retrieve the entries from within a serverless function and, after having set up the endpoint, call it on-demand.

A simple example of how to do that may look like this:

#[contracttype]
pub enum DataKey {
    Balance(Address)
}
#[derive(Serialize, Deserialize)]
pub struct Balance {
    addr: String,
    balance: i128
}
#[derive(Serialize, Deserialize)]
pub struct Request {
    addesses: Vec<String>
}

#[no_mangle]
pub extern "C" fn get_all_balances() {
    let env = EnvClient::empty();
    let req: Request = env.read_request_body();
    let mut balances = Vec::new();
    for addr in req.addesses {
        let source_addr = Address::from_string(&SorString::from_str(&env.soroban(), &addr));
        let res: i128 = env.read_contract_entry_by_key([0;32], DataKey::Balance(source_addr)).unwrap().unwrap();
        balances.push(Balance { addr, balance: res })
    }

    env.conclude(balances)
}

Here we are creating an endpoint that accesses a specific custom entry (Balance(Address)for a contract ([0;32]), using the env.read_contract_entry_by_key() function. The Balance objects holding the balance value for the different addresses are stored in a vector. You can see here how the possibility of using contract custom types reduces the amount of work on the user side.

This is only one of the many things you can do: instead of using read_contract_entry_by_key() you could use any of the functions that access the ledger state and customize the endpoint as you wish.


Resources

Here are additional automation resources:

Last updated