Thanks for writing this up @marcelo.near !
I have a suggestion for the Promise data structure and corresponding interfaces of the precompile and async contract.
I think Promise should be defined as a recursive data structure
pub enum Promise {
Then {base: Box<Promise>, callback: Box<Promise>},
And(Box<Promise>, Box<Promise>),
Call {target: AccountId, ...}
}
This makes writing invalid promises impossible. When the combinators are done via the index indirection it would be possible to write a promise that depends on an index that does not exist, or even was created as part of a different batch (which could maybe lead to some frontrunning problems?).
With this change the interfaces also become simpler because only a single identifier needs to be returned by the schedule call, and similarly only a single identifier needs to be passed to the execute call.
trait AuroraRouter {
pub fn schedule(&mut self, promise: Promise) -> PromiseId;
pub fn pull(&mut self, promise: PromiseId, total_gas: Gas, total_balance: Balance) -> Option<Promise>;
}
trait AsyncAurora {
pub fn pull_and_execute(&mut self, aurora: AccountId, promise: PromiseId, total_gas: Gas, total_balance: Balance);
pub fn submit_and_execute(&mut self, aurora: AccountId, submit_payload: Vec<u8>, total_gas: Gas, total_balance: Balance);
}
Note that it is still possible to put multiple calls in a single promise because And and Then combinators are baked in to the Promise type. For example instead of passing vec![0, 1] to execute I would call schedule with Promise::And(..., ...), getting back a single identifier to pass to execute that still does both promises.