Overview
This post relates to the changes to the staging contract outlined in FLIP 179. While the mechanism outlined in that FLIP may be useful for the general purpose of coordinating contract updates, it was decided that the network-wide updates required for Cadence 1.0 would be better served by state migration.
To ease the path for both state migration as well as developers updating their contracts, the staging contract has been simplified for the purposes of storing staged code, broadcasting when changes are made to staged code (addition, replacement, and removal), and serving queries to retrieve said code.
What follows is a primer on the updated staging mechanic with the hopes of providing clear primitives on which enhanced developer tooling can be built.
Staging Process
Pre-Requisites
- An existing contract deployed to your target network. For example, if you’re staging
A
in address0x01
, you should already have a contract namedA
deployed to0x01
. - A Cadence 1.0 compatible contract serving as an update to your existing contract. Extending our example, if you’re staging
A
in address0x01
, you should have a contract namedA
that is Cadence 1.0 compatible. See the references below for more information on Cadence 1.0 language changes.
Staging Your Contract Update
Armed with your pre-requisites, you’re ready to stage your contract update. Run the stage_contract.cdc
transaction, passing your contract’s name and Cadence code as arguments and signing as the contract host account.
import "MigrationContractStaging"
transaction(contractName: String, contractCode: String) {
let host: &MigrationContractStaging.Host
prepare(signer: AuthAccount) {
// Configure Host resource if needed
if signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil {
signer.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath)
}
// Assign Host reference
self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)!
}
execute {
// Call staging contract, storing the contract code that will update during Cadence 1.0 migration
MigrationContractStaging.stageContract(host: self.host, name: contractName, code: contractCode)
}
post {
MigrationContractStaging.isStaged(address: self.host.address(), name: contractName):
"Problem while staging update"
}
}
At the end of this transaction, your contract will be staged in the MigrationContractStaging
account. If you staged this contract’s code previously, it will be overwritten by the code provided in this transaction.
Checking Staging Status
Retrieving staged code will be critical for both developers building on unowned dependencies as well as the creation of state migrations based on staged code. To do so, run the get_staged_contract_code.cdc
script, passing the address & name of the contract you’re requesting and getting the Cadence code in return. This script can also help you get the staged code for your dependencies if the project owner has staged their code.
import "MigrationContractStaging"
/// Returns the code as it is staged or nil if it not currently staged.
///
access(all) fun main(contractAddress: Address, contractName: String): String? {
return MigrationContractStaging.getStagedContractCode(address: contractAddress, name: contractName)
}
Developer Support
While the MigrationContractStaging
contract serves as a central repository for staged code, much more can be done to support developers during the staging process as well as ensure a rapid feedback loop regarding status of both their staged updates and any unowned dependency contracts.
It will be critical for network health that the refactor and staging process be as simple and frictionless as possible. Below are several crowdsourced tooling ideas for that might make for good next steps given the interface and primitives provided by this contract:
- Adding a staged contract badge on contracts in popular web tools such as Flowdiver, ContractBrowser, Flowview, etc.
- See
is_staged.cdc
- See
- Diff views comparing current & staged contracts in contract inspectors including Flowdiver, ContractBrowser, etc.
- A GUI from which developers can connect & stage their contracts
- A dashboard to track network-wide staging progress
- Flow CLI command that stages a contract, replacing contract aliases for the named network and running a sanity check (such as a 1.0 parser) on the code before submitting the staging transaction
- A page where one can create a watchlist of contracts to check staging status and/or be notified when they’re staged
- Emulator + CLI version that enables mocking the HCU locally so you can test staging & migration of your staged contracts locally
These ideas are not an exhaustive list and are presented in no particular order, so please do provide feedback. In the interest of fostering ideation towards this effort, below is a summary of the contract interface.
MigrationContractStaging
Contract Details
The basic interface to stage a contract is the same as deploying a contract - name + code. See the stage_contract
& unstage_contract
transactions. Note that calling stageContract()
again for the same contract will overwrite any existing staged code for that contract.
/// 1 - Create a host and save it in your contract-hosting account at MigrationContractStaging.HostStoragePath
access(all) fun createHost(): @Host
/// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage.
access(all) fun stageContract(host: &Host, name: String, code: String)
/// Removes the staged contract code from the staging environment.
access(all) fun unstageContract(host: &Host, name: String)
To stage a contract, the developer first saves a Host
resource in their account which they pass as a reference along with the contract name and code they wish to stage. The Host
reference simply serves as proof of authority that the caller has access to the contract-hosting account, which in the simplest case would be the signer of the staging transaction, though conceivably this could be delegated to some other account via Capability - possibly helpful for some multisig contract hosts.
/// Serves as identification for a caller's address.
access(all) resource Host {
/// Returns the resource owner's address
access(all) view fun address(): Address
}
Within the MigrationContractStaging
contract account, code is saved on a contract-basis as a ContractUpdate
struct within a Capsule
resource and stored at a the derived path. The Capsule
simply serves as a dedicated repository for staged contract code.
/// Represents contract and its corresponding code.
access(all) struct ContractUpdate {
access(all) let address: Address
access(all) let name: String
access(all) var code: String
/// Validates that the named contract exists at the target address.
access(all) view fun isValid(): Bool
/// Serializes the address and name into a string of the form 0xADDRESS.NAME
access(all) view fun toString(): String
/// Replaces the ContractUpdate code with that provided.
access(contract) fun replaceCode(_ code: String)
}
/// Resource that stores pending contract updates in a ContractUpdate struct.
/// Saved at /storage/MigrationContractStaging.capsulePathPrefix_ADDRESS_NAME
access(all) resource Capsule {
/// The address, name and code of the contract that will be updated.
access(self) let update: ContractUpdate
/// Returns the staged contract update in the form of a ContractUpdate struct.
access(all) view fun getContractUpdate(): ContractUpdate
/// Replaces the staged contract code with the given Cadence code.
access(contract) fun replaceCode(code: String)
}
To support monitoring staging progress across the network, the single StagingStatusUpdated
event is emitted any time a contract is staged (status == "stage"
), staged code is replaced (status == "replace"
), or a contract is unstaged (status == "unstage"
).
/// Event emitted when a contract's code is staged, replaced or unstaged
/// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract
/// NOTE: Does not guarantee that the contract code is valid Cadence
access(all) event StagingStatusUpdated(
capsuleUUID: UInt64,
address: Address,
code: String,
contract: String,
action: String
)
Included in the contact are methods for querying staging status and retrieval of staged code. This enables platforms like Flowview, Flowdiver, ContractBrowser, etc. to display the staging status of contracts on any given account.
/* --- Public Getters --- */
//
/// Returns true if the contract is currently staged.
access(all) view fun isStaged(address: Address, name: String): Bool
/// Returns the names of all staged contracts for the given address.
access(all) view fun getStagedContractNames(forAddress: Address): [String]
/// Returns the staged contract Cadence code for the given address and name.
access(all) view fun getStagedContractCode(address: Address, name: String): String?
/// Returns an array of all staged contract host addresses.
access(all) view fun getAllStagedContractHosts(): [Address]
/// Returns a dictionary of all staged contract code for the given address.
access(all) view fun getAllStagedContractCode(forAddress: Address): {String: String}
/// Returns all staged contracts as a mapping of address to an array of contract names
access(all) view fun getAllStagedContracts(): {Address: [String]}
Next Steps
Assuming alignment following on from conversation in the recent PR, the staging contract will be deployed to a Flow-managed Testnet account by the end of this week. Once deployed, tooling can be built against it to smoothen the staging path. Please feel free to ask any questions or highlight any gaps in the staging process outlined above.