Batch actions vs chained calls
Several actions on one NEAR Promise form one atomic receipt; chained .then calls are separate receipts. Build a sub-account factory that refunds on failure.
Intermediate10 min read3-question check
Every Promise targets exactly one receiver account and carries a list of actions: CreateAccount, Transfer, AddKey, DeleteKey, DeployContract, FunctionCall, Stake, DeleteAccount. Chain several of them on the same Promise::new(receiver) and you get a batch: one receipt whose actions run in order, in the same block, and either all succeed or all are reverted.
Use .then(...) instead and each step is its own receipt, executed in its own block, committing independently. Batches are the only atomic multi-step unit NEAR gives you, and they work on one receiver only.
One receipt or many#
Batch: Promise::new(x).a().b().c() | Chain: p1.then(p2).then(p3) | |
|---|---|---|
| Receipts | One | One per promise |
| Receivers | Exactly one account | Any accounts |
| Atomic? | Yes: any failing action reverts every action in the receipt | No: each receipt commits on its own |
| Timing | All actions in the same block | At least one block per step |
| Can step 2 see step 1’s result? | Only through state (e.g. the contract deploy just installed) | Yes, as a promise result |
| Failure refunds | All deposits in the receipt go back to the predecessor | Only the failed receipt’s deposit is refunded |
A factory that deploys sub-accounts#
The classic batch is a factory: create name.factory.near, fund it, deploy code, and initialise it, as one unit. If initialisation panics, you do not want a half-built account with code but no state. A batch guarantees you get all four steps or none.
use near_sdk::serde_json::json;
use near_sdk::{
env, log, near, require, AccountId, Gas, NearToken, PanicOnDefault, Promise, PromiseError,
PublicKey,
};
const CHILD_WASM: &[u8] = include_bytes!("../res/child.wasm");
const MIN_DEPOSIT: NearToken = NearToken::from_near(2); // covers the child's storage
const GAS_FOR_INIT: Gas = Gas::from_tgas(20);
const GAS_FOR_CALLBACK: Gas = Gas::from_tgas(10);
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Factory {
created: u64,
}
#[near]
impl Factory {
#[init]
pub fn new() -> Self {
Self { created: 0 }
}
#[payable]
pub fn create_child(&mut self, name: String, owner_key: PublicKey) -> Promise {
let payer = env::predecessor_account_id();
let deposit = env::attached_deposit();
require!(deposit >= MIN_DEPOSIT, "attach at least 2 NEAR");
// a factory can only create sub-accounts of itself
let child: AccountId = format!("{}.{}", name, env::current_account_id())
.parse()
.unwrap_or_else(|_| env::panic_str("invalid sub-account name"));
let init_args = json!({ "owner_id": payer }).to_string().into_bytes();
// ONE receipt to the child: these five actions are atomic
Promise::new(child.clone())
.create_account()
.transfer(deposit)
.add_full_access_key(owner_key)
.deploy_contract(CHILD_WASM.to_vec())
.function_call("new".to_string(), init_args, NearToken::from_near(0), GAS_FOR_INIT)
// a SEPARATE receipt back on the factory
.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_CALLBACK)
.with_unused_gas_weight(0)
.on_child_created(child, payer, deposit),
)
}
#[private]
pub fn on_child_created(
&mut self,
child: AccountId,
payer: AccountId,
deposit: NearToken,
#[callback_result] result: Result<(), PromiseError>,
) -> bool {
if result.is_ok() {
self.created += 1;
log!("created {}", child);
return true;
}
// The whole batch was reverted: no account, no code, no key.
// The 2 NEAR came back to the FACTORY (the predecessor of the batch),
// so pass it on to the user who paid.
log!("creating {} failed; refunding {}", child, payer);
Promise::new(payer).transfer(deposit).detach();
false
}
}- Order matters inside a batch.
create_accountmust come first;function_callmust come afterdeploy_contract. Actions execute top to bottom. - Sub-accounts only.
create_accountonPromise::new(x)only works whenxis a direct sub-account of the current account. A name likealice.nearcan only be created by its parent: on mainnet you call thenearaccount’screate_accountmethod, which is a cross-contract call, not a batch of yours. - Know what keys you add.
add_full_access_keygives the owner the power to redeploy or delete the child. Leave it out for an immutable child, or add a function-call key instead. See Access keys risk. - Initialise in the same batch. A deploy without the
newcall leaves a contract anyone could initialise first.
When a chain is the right tool#
You need a chain whenever steps touch different accounts or a later step needs an earlier step’s return value. Swap tokens, then stake the output; read a price, then lend against it. Each arrow is a commit point, so design the chain as a sequence of states you can recover from (next lessons: gas for call graphs and rollback-safe designs).
Check yourself
3 questions · progress saved in this browser