From 5967c88b3e3ea7f0596fb779441516aa7fd138f5 Mon Sep 17 00:00:00 2001 From: Sorin Petreasca Date: Sat, 4 Nov 2023 01:15:16 +0200 Subject: [PATCH] proxy deployer contract --- Cargo.toml | 2 + contracts/proxy-deployer/.gitignore | 7 + contracts/proxy-deployer/Cargo.toml | 16 ++ contracts/proxy-deployer/meta/Cargo.toml | 13 ++ contracts/proxy-deployer/meta/src/main.rs | 3 + contracts/proxy-deployer/multiversx.json | 3 + .../src/address_to_id_mapper.rs | 192 +++++++++++++++ contracts/proxy-deployer/src/config.rs | 42 ++++ .../src/contract_interactions.rs | 94 ++++++++ contracts/proxy-deployer/src/lib.rs | 16 ++ contracts/proxy-deployer/wasm/Cargo.lock | 219 ++++++++++++++++++ contracts/proxy-deployer/wasm/Cargo.toml | 25 ++ contracts/proxy-deployer/wasm/src/lib.rs | 36 +++ 13 files changed, 668 insertions(+) create mode 100644 contracts/proxy-deployer/.gitignore create mode 100644 contracts/proxy-deployer/Cargo.toml create mode 100644 contracts/proxy-deployer/meta/Cargo.toml create mode 100644 contracts/proxy-deployer/meta/src/main.rs create mode 100644 contracts/proxy-deployer/multiversx.json create mode 100644 contracts/proxy-deployer/src/address_to_id_mapper.rs create mode 100644 contracts/proxy-deployer/src/config.rs create mode 100644 contracts/proxy-deployer/src/contract_interactions.rs create mode 100644 contracts/proxy-deployer/src/lib.rs create mode 100644 contracts/proxy-deployer/wasm/Cargo.lock create mode 100644 contracts/proxy-deployer/wasm/Cargo.toml create mode 100644 contracts/proxy-deployer/wasm/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 05451ba3..47589ced 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,8 @@ members = [ "contracts/paymaster/meta", "contracts/ping-pong-egld", "contracts/ping-pong-egld/meta", + "contracts/proxy-deployer", + "contracts/proxy-deployer/meta", "contracts/proxy-pause", "contracts/proxy-pause/meta", "contracts/rewards-distribution", diff --git a/contracts/proxy-deployer/.gitignore b/contracts/proxy-deployer/.gitignore new file mode 100644 index 00000000..920d759a --- /dev/null +++ b/contracts/proxy-deployer/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +*/target/ + +# The erdpy output +output* diff --git a/contracts/proxy-deployer/Cargo.toml b/contracts/proxy-deployer/Cargo.toml new file mode 100644 index 00000000..5eb07fcd --- /dev/null +++ b/contracts/proxy-deployer/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "proxy-deployer" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "=0.43.4" +features = ["esdt-token-payment-legacy-decode"] + +[dev-dependencies.multiversx-sc-scenario] +version = "=0.43.4" diff --git a/contracts/proxy-deployer/meta/Cargo.toml b/contracts/proxy-deployer/meta/Cargo.toml new file mode 100644 index 00000000..b1d9a531 --- /dev/null +++ b/contracts/proxy-deployer/meta/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "proxy-deployer-meta" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["MultiversX "] + +[dev-dependencies] +[dependencies.proxy-deployer] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.43.4" diff --git a/contracts/proxy-deployer/meta/src/main.rs b/contracts/proxy-deployer/meta/src/main.rs new file mode 100644 index 00000000..6df25a6b --- /dev/null +++ b/contracts/proxy-deployer/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/contracts/proxy-deployer/multiversx.json b/contracts/proxy-deployer/multiversx.json new file mode 100644 index 00000000..73655396 --- /dev/null +++ b/contracts/proxy-deployer/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/proxy-deployer/src/address_to_id_mapper.rs b/contracts/proxy-deployer/src/address_to_id_mapper.rs new file mode 100644 index 00000000..f9888f76 --- /dev/null +++ b/contracts/proxy-deployer/src/address_to_id_mapper.rs @@ -0,0 +1,192 @@ +use core::marker::PhantomData; + +use multiversx_sc::{ + api::StorageMapperApi, + storage::{storage_get_from_address, storage_get_len_from_address, StorageKey}, + storage_clear, storage_get, storage_get_len, storage_set, +}; + +multiversx_sc::imports!(); + +static ID_SUFFIX: &[u8] = b"userId"; +static ADDRESS_SUFFIX: &[u8] = b"addr"; +static LAST_ID_SUFFIX: &[u8] = b"lastId"; + +static UNKNOW_ADDR_ERR_MSG: &[u8] = b"Unknown address"; + +pub type AddressId = u64; +pub const NULL_ID: AddressId = 0; + +pub struct AddressToIdMapper +where + SA: StorageMapperApi, +{ + _phantom_api: PhantomData, + base_key: StorageKey, +} + +impl StorageMapper for AddressToIdMapper +where + SA: StorageMapperApi, +{ + fn new(base_key: StorageKey) -> Self { + AddressToIdMapper { + _phantom_api: PhantomData, + base_key, + } + } +} + +impl AddressToIdMapper +where + SA: StorageMapperApi, +{ + pub fn contains_id(&self, id: AddressId) -> bool { + let key = self.id_to_address_key(id); + storage_get_len(key.as_ref()) != 0 + } + + pub fn get_id(&self, address: &ManagedAddress) -> AddressId { + let key = self.address_to_id_key(address); + storage_get(key.as_ref()) + } + + pub fn get_id_at_address( + &self, + sc_address: &ManagedAddress, + address_to_find: &ManagedAddress, + ) -> AddressId { + let key = self.address_to_id_key(address_to_find); + storage_get_from_address(sc_address.as_ref(), key.as_ref()) + } + + pub fn get_id_non_zero(&self, address: &ManagedAddress) -> AddressId { + let id = self.get_id(address); + if id == NULL_ID { + SA::error_api_impl().signal_error(UNKNOW_ADDR_ERR_MSG); + } + + id + } + + pub fn get_id_at_address_non_zero( + &self, + sc_address: &ManagedAddress, + address_to_find: &ManagedAddress, + ) -> AddressId { + let id = self.get_id_at_address(sc_address, address_to_find); + if id == NULL_ID { + SA::error_api_impl().signal_error(UNKNOW_ADDR_ERR_MSG); + } + + id + } + + pub fn insert_new(&self, address: &ManagedAddress) -> AddressId { + let existing_id = self.get_id(address); + if existing_id != NULL_ID { + SA::error_api_impl().signal_error(b"Address already registered"); + } + + self.insert_address(address) + } + + pub fn get_address(&self, id: AddressId) -> Option> { + let key = self.id_to_address_key(id); + if storage_get_len(key.as_ref()) == 0 { + return None; + } + + let addr = storage_get(key.as_ref()); + Some(addr) + } + + pub fn get_address_at_address( + &self, + sc_address: &ManagedAddress, + id: AddressId, + ) -> Option> { + let key = self.id_to_address_key(id); + if storage_get_len_from_address(sc_address.as_ref(), key.as_ref()) == 0 { + return None; + } + + let addr = storage_get_from_address(sc_address.as_ref(), key.as_ref()); + Some(addr) + } + + pub fn get_id_or_insert(&self, address: &ManagedAddress) -> AddressId { + let current_id = storage_get(self.address_to_id_key(address).as_ref()); + if current_id != 0 { + return current_id; + } + + self.insert_address(address) + } + + pub fn remove_by_id(&self, id: AddressId) -> Option> { + let address = self.get_address(id)?; + self.remove_entry(id, &address); + + Some(address) + } + + pub fn remove_by_address(&self, address: &ManagedAddress) -> AddressId { + let current_id = self.get_id(address); + if current_id != NULL_ID { + self.remove_entry(current_id, address); + } + + current_id + } + + fn insert_address(&self, address: &ManagedAddress) -> AddressId { + let new_id = self.get_last_id() + 1; + storage_set(self.address_to_id_key(address).as_ref(), &new_id); + storage_set(self.id_to_address_key(new_id).as_ref(), address); + + self.set_last_id(new_id); + + new_id + } + + fn remove_entry(&self, id: AddressId, address: &ManagedAddress) { + storage_clear(self.address_to_id_key(address).as_ref()); + storage_clear(self.id_to_address_key(id).as_ref()); + } + + fn id_to_address_key(&self, id: AddressId) -> StorageKey { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(ID_SUFFIX); + item_key.append_item(&id); + + item_key + } + + fn address_to_id_key(&self, address: &ManagedAddress) -> StorageKey { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(ADDRESS_SUFFIX); + item_key.append_item(address); + + item_key + } + + fn last_id_key(&self) -> StorageKey { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(LAST_ID_SUFFIX); + + item_key + } + + pub fn get_last_id(&self) -> AddressId { + storage_get(self.last_id_key().as_ref()) + } + + fn set_last_id(&self, last_id: AddressId) { + if last_id == 0 { + SA::error_api_impl().signal_error(b"ID Overflow"); + } + + storage_set(self.last_id_key().as_ref(), &last_id); + } +} diff --git a/contracts/proxy-deployer/src/config.rs b/contracts/proxy-deployer/src/config.rs new file mode 100644 index 00000000..4e8492da --- /dev/null +++ b/contracts/proxy-deployer/src/config.rs @@ -0,0 +1,42 @@ +multiversx_sc::imports!(); + +use crate::address_to_id_mapper::{AddressId, AddressToIdMapper}; + +#[multiversx_sc::module] +pub trait ConfigModule { + #[only_owner] + #[endpoint(addContractTemplate)] + fn add_contract_template(&self, template_address: ManagedAddress) -> AddressId { + require!( + self.blockchain().is_smart_contract(&template_address), + "Invalid template address" + ); + + self.address_ids().insert_new(&template_address) + } + + #[only_owner] + #[endpoint(removeContractTemplate)] + fn remove_contract_template(&self, address_id: AddressId) { + require!( + self.address_ids().contains_id(address_id), + "Invalid address id" + ); + + self.address_ids().remove_by_id(address_id); + } + + #[storage_mapper("addressIds")] + fn address_ids(&self) -> AddressToIdMapper; + + #[view(getAllDeployers)] + #[storage_mapper("deployersList")] + fn deployers_list(&self) -> UnorderedSetMapper; + + #[view(getDeployerContractAddresses)] + #[storage_mapper("deployerContractAddresses")] + fn deployer_contract_addresses( + &self, + deployer_address: &ManagedAddress, + ) -> UnorderedSetMapper; +} diff --git a/contracts/proxy-deployer/src/contract_interactions.rs b/contracts/proxy-deployer/src/contract_interactions.rs new file mode 100644 index 00000000..51b70ed7 --- /dev/null +++ b/contracts/proxy-deployer/src/contract_interactions.rs @@ -0,0 +1,94 @@ +multiversx_sc::imports!(); + +use crate::address_to_id_mapper::AddressId; +use crate::config; + +#[multiversx_sc::module] +pub trait ContractInteractionsModule: config::ConfigModule { + #[endpoint(deployContract)] + fn deploy_contract( + &self, + template_address_id: AddressId, + args: MultiValueEncoded, + ) -> ManagedAddress { + let caller = self.blockchain().get_caller(); + + let mut arguments = ManagedArgBuffer::new(); + for arg in args { + arguments.push_arg(arg); + } + + let opt_template_address = self.address_ids().get_address(template_address_id); + let template_address = match opt_template_address { + Some(template_address) => template_address, + None => sc_panic!("Template not found"), + }; + + let (new_contract_address, _) = self.send_raw().deploy_from_source_contract( + self.blockchain().get_gas_left(), + &BigUint::zero(), + &template_address, + CodeMetadata::DEFAULT, + &arguments, + ); + + self.deployer_contract_addresses(&caller) + .insert(new_contract_address.clone()); + self.deployers_list().insert(caller); + + new_contract_address + } + + #[endpoint(upgradeContract)] + fn upgrade_contract( + &self, + contract_address: ManagedAddress, + template_address_id: AddressId, + args: MultiValueEncoded, + ) { + let caller = self.blockchain().get_caller(); + require!(self.deployer_contract_addresses(&caller).contains(&contract_address), "Caller is not the deployer of the contract"); + + let mut arguments = ManagedArgBuffer::new(); + for arg in args { + arguments.push_arg(arg); + } + + let opt_template_address = self.address_ids().get_address(template_address_id); + let template_address = match opt_template_address { + Some(template_address) => template_address, + None => sc_panic!("Template not found"), + }; + + self.send_raw().upgrade_from_source_contract( + &contract_address, + self.blockchain().get_gas_left(), + &BigUint::zero(), + &template_address, + CodeMetadata::DEFAULT, + &arguments, + ); + } + + #[endpoint(callContractEndpoint)] + fn call_contract_endpoint( + &self, + contract_address: ManagedAddress, + function_name: ManagedBuffer, + args: MultiValueEncoded, + ) { + let caller = self.blockchain().get_caller(); + require!(self.deployer_contract_addresses(&caller).contains(&contract_address), "Caller is not the deployer of the contract"); + + let gas_left = self.blockchain().get_gas_left(); + let mut contract_call = self + .send() + .contract_call::<()>(contract_address, function_name) + .with_gas_limit(gas_left); + + for arg in args { + contract_call.push_raw_argument(arg); + } + let _: IgnoreValue = contract_call.execute_on_dest_context(); + } +} diff --git a/contracts/proxy-deployer/src/lib.rs b/contracts/proxy-deployer/src/lib.rs new file mode 100644 index 00000000..15800090 --- /dev/null +++ b/contracts/proxy-deployer/src/lib.rs @@ -0,0 +1,16 @@ +#![no_std] + +multiversx_sc::imports!(); + +pub mod address_to_id_mapper; +pub mod config; +pub mod contract_interactions; + +#[multiversx_sc::contract] +pub trait ProxyDeployer: contract_interactions::ContractInteractionsModule + config::ConfigModule { + #[init] + fn init(&self) {} + + #[endpoint] + fn upgrade(&self) {} +} diff --git a/contracts/proxy-deployer/wasm/Cargo.lock b/contracts/proxy-deployer/wasm/Cargo.lock new file mode 100644 index 00000000..8066d1b0 --- /dev/null +++ b/contracts/proxy-deployer/wasm/Cargo.lock @@ -0,0 +1,219 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "multiversx-sc" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406939660d0c79dd191c6677f4b048df873a95f4531d8abafc9cdbe282bf1725" +dependencies = [ + "bitflags", + "hashbrown", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e15b46c17b87c0c7cdd79b041a4abd7f3a2b45f3c993f6ce38c0f233e82b6" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7bc0762cd6d88f8bc54805bc652b042a61cd7fbc2d0a325010f088b78fb2ac" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e006240993963b482fe0682ae49b2d07255495e3c86706925d119137376cdfc" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e721d1bc80de2ede4099a9040519486c3c1139cb0287d8fc4f9fc3e8a3f19e" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxy-deployer" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "proxy-deployer-wasm" +version = "0.0.0" +dependencies = [ + "multiversx-sc-wasm-adapter", + "proxy-deployer", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/contracts/proxy-deployer/wasm/Cargo.toml b/contracts/proxy-deployer/wasm/Cargo.toml new file mode 100644 index 00000000..b2efd65a --- /dev/null +++ b/contracts/proxy-deployer/wasm/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "proxy-deployer-wasm" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["MultiversX "] + +[lib] +crate-type = ["cdylib"] + +[workspace] +members = ["."] + +[dev-dependencies] +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +[dependencies.proxy-deployer] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "=0.43.4" diff --git a/contracts/proxy-deployer/wasm/src/lib.rs b/contracts/proxy-deployer/wasm/src/lib.rs new file mode 100644 index 00000000..a832535a --- /dev/null +++ b/contracts/proxy-deployer/wasm/src/lib.rs @@ -0,0 +1,36 @@ +// Code generated by the multiversx-sc multi-contract system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 8 +// Async Callback (empty): 1 +// Total number of exported functions: 10 + +#![no_std] + +// Configuration that works with rustc < 1.73.0. +// TODO: Recommended rustc version: 1.73.0 or newer. +#![feature(lang_items)] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + proxy_deployer + ( + init => init + upgrade => upgrade + deployContract => deploy_contract + upgradeContract => upgrade_contract + callContractEndpoint => call_contract_endpoint + addContractTemplate => add_contract_template + removeContractTemplate => remove_contract_template + getAllDeployers => deployers_list + getDeployerContractAddresses => deployer_contract_addresses + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {}