Skip to content

Cross-app calls

Task: have one app tell, or ask, another app in the same organization.

The protocol side, delivery order, dead letters and the node’s checks, is in Cross-app calls.

A method is reachable from another app only if it says so:

/// Records a ping from the app `from`; only `pinger` may call it.
#[app::allow(app = "pinger")]
pub fn pong(&mut self, from: String) -> app::Result<()> {
let env::Caller::App(origin) = env::caller() else {
return Err(AppError::forbidden());
};
self.pings.insert(&origin, &from)?;
Ok(())
}

#[app::allow(app = "<app id>")] admits one app; allow(app = "*") admits any app the node delivers from. The node refuses every other app. Inside the method, env::caller() is Caller::App(<id>), set by the node, so it cannot be forged: use it, not an argument, to know who called.

pub fn ping(&mut self, target: String) -> app::Result<()> {
let args = std::collections::BTreeMap::from([("from", env::app_id())]);
env::xcall(&target, "pong", &args)?;
Ok(())
}

args is any serde value, sent as the JSON arguments a client would send. env::xcall(app, method, &args) queues the message in this run’s outbox. The node delivers it after the entry commits, only if the run succeeds, at least once and in order per sending app. The target must be in the same organization (or have the same owner). A run may queue at most 100.

let args = std::collections::BTreeMap::from([("account", account)]);
// A u64 result arrives as a decimal string, like every u64 on the wire.
let balance: String = env::xcall_view("ledger", "balance", &args)?;

env::xcall_call::<T>(app, method, &args) and env::xcall_view::<T>(..) run the target now and decode its JSON result as T. A failure is AppError XcallFailed, carrying the target’s error. Messages, calls and views all reach only methods that allow this app.

TestApp keeps the messages a run sent: take_xcalls() returns them and deliver(..) runs one against another TestApp; mock_app(..) stands in for the target of a synchronous call (Testing).