A factory is a smart contract that stores a compiled contract on itself, and automatizes deploying it into sub-accounts.
This particular example presents a factory of donation contracts, and enables to:
- Create a sub-account of the factory and deploy the stored contract on it (create_factory_subaccount_and_deploy).
- Change the stored contract using the update_stored_contract method.
#[payable]
pub fn create_factory_subaccount_and_deploy(
&mut self,
name: String,
beneficiary: AccountId,
public_key: Option<PublicKey>,
) -> Promise {
// Assert the sub-account is valid
let current_account = env::current_account_id().to_string();
let subaccount: AccountId = format!("{name}.{current_account}").parse().unwrap();
assert!(
env::is_valid_account_id(subaccount.as_bytes()),
"Invalid subaccount"
);
// Assert enough tokens are attached to create the account and deploy the contract
let attached = env::attached_deposit();
let code = self.code.clone().unwrap();
let contract_bytes = code.len() as u128;
let minimum_needed = NEAR_PER_STORAGE.saturating_mul(contract_bytes);
assert!(
attached >= minimum_needed,
"Attach at least {minimum_needed} yⓃ"
);
let init_args = near_sdk::serde_json::to_vec(&DonationInitArgs { beneficiary }).unwrap();
let mut promise = Promise::new(subaccount.clone())
.create_account()
.transfer(attached)
.deploy_contract(code)
.function_call(
"init".to_owned(),
init_args,
NO_DEPOSIT,
TGAS.saturating_mul(5),
);
// Add full access key is the user passes one
if let Some(pk) = public_key {
promise = promise.add_full_access_key(pk);
}
// Add callback
promise.then(
Self::ext(env::current_account_id()).create_factory_subaccount_and_deploy_callback(
subaccount,
env::predecessor_account_id(),
attached,
),
)
}
Install cargo-near
and run:
cd factory
cargo near build
cargo test --workspace
Deployment is automated with GitHub Actions CI/CD pipeline. To deploy manually,
install cargo-near
and run:
cd factory
cargo near deploy <account-id>
In this example we will be using NEAR CLI to intract with the NEAR blockchain and the smart contract
If you want full control over of your interactions we recommend using the near-cli-rs.
create_factory_subaccount_and_deploy
will create a sub-account of the factory
and deploy the stored contract on it.
near call <factory-account> create_factory_subaccount_and_deploy '{ "name": "sub", "beneficiary": "<account-to-be-beneficiary>"}' --deposit 1.24 --accountId <account-id> --gas 300000000000000
This will create the sub.<factory-account>
, which will have a donation
contract deployed on it:
near view sub.<factory-account> get_beneficiary
# expected response is: <account-to-be-beneficiary>
update_stored_contract
enables to change the compiled contract that the
factory stores.
The method is interesting because it has no declared parameters, and yet it takes an input: the new contract to store as a stream of bytes.
To use it, we need to transform the contract we want to store into its base64
representation, and pass the result as input to the method:
# Use near-cli to update stored contract
export BYTES=`cat ./src/to/new-contract/contract.wasm | base64`
near call <factory-account> update_stored_contract "$BYTES" --base64 --accountId <factory-account> --gas 30000000000000
This works because the arguments of a call can be either a
JSON
object or aString Buffer
Factories are an interesting concept, here we further explain some of their implementation aspects, as well as their limitations.
NEAR accounts can only create sub-accounts of themselves, therefore, the
factory
can only create and deploy contracts on its own sub-accounts.
This means that the factory:
- Can create
sub.factory.testnet
and deploy a contract on it. - Cannot create sub-accounts of the
predecessor
. - Can create new accounts (e.g.
account.testnet
), but cannot deploy contracts on them.
It is important to remember that, while factory.testnet
can create
sub.factory.testnet
, it has no control over it after its creation.
The update_stored_contracts
has a very short implementation:
#[private]
pub fn update_stored_contract(&mut self) {
self.code.set(env::input());
}
On first sight it looks like the method takes no input parameters, but we can
see that its only line of code reads from env::input()
. What is happening here
is that update_stored_contract
bypasses the step of deserializing the
input.
You could implement update_stored_contract(&mut self, new_code: Vec<u8>)
,
which takes the compiled code to store as a Vec<u8>
, but that would trigger
the contract to:
- Deserialize the
new_code
variable from the input. - Sanitize it, making sure it is correctly built.
When dealing with big streams of input data (as is the compiled wasm
file to
be stored), this process of deserializing/checking the input ends up consuming
the whole GAS for the transaction.
- cargo-near - NEAR smart contract development toolkit for Rust
- near CLI-rs - Iteract with NEAR blockchain from command line
- NEAR Rust SDK Documentation
- NEAR Documentation
- NEAR StackOverflow
- NEAR Discord
- NEAR Telegram Developers Community Group
- NEAR DevHub: Telegram, Twitter