From 6fd7bd0f7da1427b0042a3fcac34239b2103014d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:50:15 -0600 Subject: [PATCH 01/34] rename ContractUpdater to StagedContractUpdates --- README.md | 14 +++++----- ...tUpdater.cdc => StagedContractUpdates.cdc} | 18 ++++++------- flow.json | 9 +++++-- .../check_delegatee_has_valid_updater_cap.cdc | 10 +++---- scripts/get_deployment_from_config.cdc | 6 ++--- scripts/get_updater_deployment.cdc | 4 +-- scripts/get_updater_deployment_order.cdc | 4 +-- scripts/get_updater_info.cdc | 4 +-- transactions/delegate.cdc | 16 ++++++------ transactions/execute_delegated_updates.cdc | 6 ++--- .../publish_auth_account_capability.cdc | 12 ++++----- transactions/remove_delegated_updater.cdc | 6 ++--- .../remove_from_delegatee_as_updater.cdc | 16 ++++++------ transactions/setup_updater_multi_account.cdc | 20 +++++++------- ...up_updater_single_account_and_contract.cdc | 26 +++++++++---------- transactions/update.cdc | 4 +-- 16 files changed, 90 insertions(+), 85 deletions(-) rename contracts/{ContractUpdater.cdc => StagedContractUpdates.cdc} (94%) diff --git a/README.md b/README.md index 7cd1625..b8e1da2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ContractUpdater +# StagedContractUpdates > Enables pre-defined contract update deployments to a set of wrapped account at or beyond a specified block height. For > more details about the purpose of this mechanism, see [FLIP 179](https://github.com/onflow/flips/pull/179) @@ -8,7 +8,7 @@ For this run through, we'll focus on the simple case where a single contract is deployed to a single account that can sign the setup & delegation transactions. -This use case is enough to get the basic concepts involved in the `ContractUpdater` contract, but know that more +This use case is enough to get the basic concepts involved in the `StagedContractUpdates` contract, but know that more advanced deployments are possible with support for multiple contract accounts and customized deployment configurations. ### Setup @@ -33,7 +33,7 @@ advanced deployments are possible with support for multiple contract accounts an flow scripts execute ./scripts/foo.cdc ``` -1. Configure `ContractUpdater.Updater`, passing the block height, contract name, and contract code in hex form (see +1. Configure `StagedContractUpdates.Updater`, passing the block height, contract name, and contract code in hex form (see [`get_code_hex.py`](./src/get_code_hex.py) for simple script hexifying contract code): - `setup_updater_single_account_and_contract.cdc` 1. `blockUpdateBoundary: UInt64` @@ -62,7 +62,7 @@ advanced deployments are possible with support for multiple contract accounts an flow scripts execute ./scripts/get_updater_deployment.cdc 0xe03daebed8ca0615 ``` -1. Next, we'll delegate the `Updater` Capability as `DelegatedUpdater` to the `Delegatee` stored in the `ContractUpdater`'s account. +1. Next, we'll delegate the `Updater` Capability as `DelegatedUpdater` to the `Delegatee` stored in the `StagedContractUpdates`'s account. ```sh flow transactions send ./transactions/delegate.cdc --signer foo @@ -83,7 +83,7 @@ advanced deployments are possible with support for multiple contract accounts an ## Multi-Account Multi-Contract Deployment -As mentioned above, `ContractUpdater` supports update deployments across any number of accounts & contracts. +As mentioned above, `StagedContractUpdates` supports update deployments across any number of accounts & contracts. Developers with a number of owned contracts will find this helpful as they can specify the order in which an update should occur according to the contract set's dependency graph. @@ -124,8 +124,8 @@ their maximum depth in the dependency graph. In this case: - Stage 1: `[B, E]` - Stage 2: `[C]` -Let's continue into a walkthrough with contracts `A`, `B`, and `C` and see how `ContractUpdater` can be configured to -execute these preconfigured updates. +Let's continue into a walkthrough with contracts `A`, `B`, and `C` and see how `StagedContractUpdates` can be configured to +execute these pre-configured updates. ### CLI Walkthrough diff --git a/contracts/ContractUpdater.cdc b/contracts/StagedContractUpdates.cdc similarity index 94% rename from contracts/ContractUpdater.cdc rename to contracts/StagedContractUpdates.cdc index 6c53609..4a7fcd0 100644 --- a/contracts/ContractUpdater.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -17,7 +17,7 @@ // - It's common to chunk contract code and pass over numerous transactions - think about how could support a similar workflow // when configuring an Updater resource // TODO: We can't rely on dependencies updating in the same transaction, we'll need to allow for blocking update deployments -pub contract ContractUpdater { +pub contract StagedContractUpdates { pub let inboxAccountCapabilityNamePrefix: String @@ -357,7 +357,7 @@ pub contract ContractUpdater { let nameAndCode = contractConfig[address]! contractUpdates.append( - ContractUpdater.ContractUpdate( + StagedContractUpdates.ContractUpdate( address: address, name: nameAndCode.keys[0], code: nameAndCode.values[0] @@ -392,15 +392,15 @@ pub contract ContractUpdater { } init() { - self.inboxAccountCapabilityNamePrefix = "ContractUpdaterAccountCapability_" + self.inboxAccountCapabilityNamePrefix = "StagedContractUpdatesAccountCapability_" - self.UpdaterStoragePath = StoragePath(identifier: "ContractUpdater_".concat(self.account.address.toString()))! - self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "ContractUpdaterDelegated_".concat(self.account.address.toString()))! - self.UpdaterPublicPath = PublicPath(identifier: "ContractUpdaterPublic_".concat(self.account.address.toString()))! + self.UpdaterStoragePath = StoragePath(identifier: "StagedContractUpdates_".concat(self.account.address.toString()))! + self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegated_".concat(self.account.address.toString()))! + self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesPublic_".concat(self.account.address.toString()))! self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(self.account.address.toString()))! - self.DelegateeStoragePath = StoragePath(identifier: "ContractUpdaterDelegatee_".concat(self.account.address.toString()))! - self.DelegateePrivatePath = PrivatePath(identifier: "ContractUpdaterDelegatee_".concat(self.account.address.toString()))! - self.DelegateePublicPath = PublicPath(identifier: "ContractUpdaterDelegateePublic_".concat(self.account.address.toString()))! + self.DelegateeStoragePath = StoragePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! + self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! + self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(self.account.address.toString()))! self.account.save(<-create Delegatee(), to: self.DelegateeStoragePath) self.account.link<&Delegatee{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) diff --git a/flow.json b/flow.json index e48c2c9..af87bab 100644 --- a/flow.json +++ b/flow.json @@ -3,7 +3,12 @@ "A": "./contracts/A.cdc", "B": "./contracts/B.cdc", "C": "./contracts/C.cdc", - "ContractUpdater": "./contracts/ContractUpdater.cdc", + "StagedContractUpdates": { + "source": "./contracts/StagedContractUpdates.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7" + } + }, "Foo": "./contracts/Foo.cdc" }, "networks": { @@ -44,7 +49,7 @@ "C" ], "emulator-account": [ - "ContractUpdater" + "StagedContractUpdates" ], "foo": [ "Foo" diff --git a/scripts/check_delegatee_has_valid_updater_cap.cdc b/scripts/check_delegatee_has_valid_updater_cap.cdc index a675a91..3dd7eaf 100644 --- a/scripts/check_delegatee_has_valid_updater_cap.cdc +++ b/scripts/check_delegatee_has_valid_updater_cap.cdc @@ -1,13 +1,13 @@ -import "ContractUpdater" +import "StagedContractUpdates" pub fun main(updaterAddress: Address, delegateeAddress: Address): Bool? { - let updater = getAuthAccount(updaterAddress).borrow<&ContractUpdater.Updater>( - from: ContractUpdater.UpdaterStoragePath + let updater = getAuthAccount(updaterAddress).borrow<&StagedContractUpdates.Updater>( + from: StagedContractUpdates.UpdaterStoragePath ) ?? panic("Could not borrow contract updater reference") let id = updater.getID() - let delegatee = getAuthAccount(delegateeAddress).borrow<&ContractUpdater.Delegatee>( - from: ContractUpdater.DelegateeStoragePath + let delegatee = getAuthAccount(delegateeAddress).borrow<&StagedContractUpdates.Delegatee>( + from: StagedContractUpdates.DelegateeStoragePath ) ?? panic("Could not borrow contract delegatee reference") return delegatee.check(id: id) } \ No newline at end of file diff --git a/scripts/get_deployment_from_config.cdc b/scripts/get_deployment_from_config.cdc index 8094a65..fb2eaf4 100644 --- a/scripts/get_deployment_from_config.cdc +++ b/scripts/get_deployment_from_config.cdc @@ -1,5 +1,5 @@ -import "ContractUpdater" +import "StagedContractUpdates" -pub fun main(config: [[{Address: {String: String}}]]): [[ContractUpdater.ContractUpdate]] { - return ContractUpdater.getDeploymentFromConfig(config) +pub fun main(config: [[{Address: {String: String}}]]): [[StagedContractUpdates.ContractUpdate]] { + return StagedContractUpdates.getDeploymentFromConfig(config) } \ No newline at end of file diff --git a/scripts/get_updater_deployment.cdc b/scripts/get_updater_deployment.cdc index 34c159a..f0ef9f1 100644 --- a/scripts/get_updater_deployment.cdc +++ b/scripts/get_updater_deployment.cdc @@ -1,4 +1,4 @@ -import "ContractUpdater" +import "StagedContractUpdates" pub struct ContractUpdateReadable { pub let address: Address @@ -22,7 +22,7 @@ pub fun main(address: Address): [[ContractUpdateReadable]]? { let account = getAuthAccount(address) - if let updater = account.borrow<&ContractUpdater.Updater>(from: ContractUpdater.UpdaterStoragePath) { + if let updater = account.borrow<&StagedContractUpdates.Updater>(from: StagedContractUpdates.UpdaterStoragePath) { let result: [[ContractUpdateReadable]] = [] let deployments = updater.getDeployments() diff --git a/scripts/get_updater_deployment_order.cdc b/scripts/get_updater_deployment_order.cdc index 9431870..280203a 100644 --- a/scripts/get_updater_deployment_order.cdc +++ b/scripts/get_updater_deployment_order.cdc @@ -1,11 +1,11 @@ -import "ContractUpdater" +import "StagedContractUpdates" /// Returns values of the Updater at the given Address /// pub fun main(address: Address): [[{Address: String}]]? { let account = getAuthAccount(address) - if let updater = account.borrow<&ContractUpdater.Updater>(from: ContractUpdater.UpdaterStoragePath) { + if let updater = account.borrow<&StagedContractUpdates.Updater>(from: StagedContractUpdates.UpdaterStoragePath) { let result: [[{Address: String}]] = [] let deployments = updater.getDeployments() diff --git a/scripts/get_updater_info.cdc b/scripts/get_updater_info.cdc index 7613a0c..c606d96 100644 --- a/scripts/get_updater_info.cdc +++ b/scripts/get_updater_info.cdc @@ -1,4 +1,4 @@ -import "ContractUpdater" +import "StagedContractUpdates" pub struct ContractUpdateReadable { pub let name: String @@ -18,7 +18,7 @@ pub struct ContractUpdateReadable { pub fun main(address: Address): {Int: {Address: [ContractUpdateReadable]}}? { let account = getAuthAccount(address) - if let updater = account.borrow<&ContractUpdater.Updater>(from: ContractUpdater.UpdaterStoragePath) { + if let updater = account.borrow<&StagedContractUpdates.Updater>(from: StagedContractUpdates.UpdaterStoragePath) { let result: {Int: {Address: [ContractUpdateReadable]}} = {} let deployments = updater.getDeployments() diff --git a/transactions/delegate.cdc b/transactions/delegate.cdc index ab2f5a3..c67f4a8 100644 --- a/transactions/delegate.cdc +++ b/transactions/delegate.cdc @@ -1,19 +1,19 @@ -import "ContractUpdater" +import "StagedContractUpdates" transaction { - let delegatee: &ContractUpdater.Delegatee{ContractUpdater.DelegateePublic} - let updaterCap: Capability<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}> + let delegatee: &StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic} + let updaterCap: Capability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}> let updaterID: UInt64 prepare(signer: AuthAccount) { - let delegateeAccount = getAccount(ContractUpdater.getContractDelegateeAddress()) - self.delegatee = delegateeAccount.getCapability<&ContractUpdater.Delegatee{ContractUpdater.DelegateePublic}>( - ContractUpdater.DelegateePublicPath + let delegateeAccount = getAccount(StagedContractUpdates.getContractDelegateeAddress()) + self.delegatee = delegateeAccount.getCapability<&StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic}>( + StagedContractUpdates.DelegateePublicPath ).borrow() ?? panic("Could not borrow Delegatee reference") - self.updaterCap = signer.getCapability<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}>( - ContractUpdater.DelegatedUpdaterPrivatePath + self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.DelegatedUpdaterPrivatePath ) self.updaterID = self.updaterCap.borrow()?.getID() ?? panic("Invalid Updater Capability retrieved from signer!") } diff --git a/transactions/execute_delegated_updates.cdc b/transactions/execute_delegated_updates.cdc index b4f94c6..070f62f 100644 --- a/transactions/execute_delegated_updates.cdc +++ b/transactions/execute_delegated_updates.cdc @@ -1,11 +1,11 @@ -import "ContractUpdater" +import "StagedContractUpdates" transaction { - let delegatee: &ContractUpdater.Delegatee + let delegatee: &StagedContractUpdates.Delegatee prepare(signer: AuthAccount) { - self.delegatee = signer.borrow<&ContractUpdater.Delegatee>(from: ContractUpdater.DelegateeStoragePath) + self.delegatee = signer.borrow<&StagedContractUpdates.Delegatee>(from: StagedContractUpdates.DelegateeStoragePath) ?? panic("Could not borrow Delegatee reference from signer") } diff --git a/transactions/publish_auth_account_capability.cdc b/transactions/publish_auth_account_capability.cdc index 7ab73fa..87e66c6 100644 --- a/transactions/publish_auth_account_capability.cdc +++ b/transactions/publish_auth_account_capability.cdc @@ -1,6 +1,6 @@ #allowAccountLinking -import "ContractUpdater" +import "StagedContractUpdates" /// Publishes an Capability on the signer's AuthAccount for the specified recipient /// @@ -9,19 +9,19 @@ transaction(publishFor: Address) { let accountCap: Capability<&AuthAccount> prepare(signer: AuthAccount) { - if !signer.getCapability<&AuthAccount>(ContractUpdater.UpdaterContractAccountPrivatePath).check() { - signer.unlink(ContractUpdater.UpdaterContractAccountPrivatePath) - self.accountCap = signer.linkAccount(ContractUpdater.UpdaterContractAccountPrivatePath) + if !signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath).check() { + signer.unlink(StagedContractUpdates.UpdaterContractAccountPrivatePath) + self.accountCap = signer.linkAccount(StagedContractUpdates.UpdaterContractAccountPrivatePath) ?? panic("Problem linking AuthAccount Capability") } else { - self.accountCap = signer.getCapability<&AuthAccount>(ContractUpdater.UpdaterContractAccountPrivatePath) + self.accountCap = signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath) } assert(self.accountCap.check(), message: "Invalid AuthAccount Capability retrieved") signer.inbox.publish( self.accountCap, - name: ContractUpdater.inboxAccountCapabilityNamePrefix.concat(publishFor.toString()), + name: StagedContractUpdates.inboxAccountCapabilityNamePrefix.concat(publishFor.toString()), recipient: publishFor ) } diff --git a/transactions/remove_delegated_updater.cdc b/transactions/remove_delegated_updater.cdc index 6f59c4e..37eae81 100644 --- a/transactions/remove_delegated_updater.cdc +++ b/transactions/remove_delegated_updater.cdc @@ -1,11 +1,11 @@ -import "ContractUpdater" +import "StagedContractUpdates" transaction(removeID: UInt64) { - let delegatee: &ContractUpdater.Delegatee + let delegatee: &StagedContractUpdates.Delegatee prepare(signer: AuthAccount) { - self.delegatee = signer.borrow<&ContractUpdater.Delegatee>(from: ContractUpdater.DelegateeStoragePath) + self.delegatee = signer.borrow<&StagedContractUpdates.Delegatee>(from: StagedContractUpdates.DelegateeStoragePath) ?? panic("Could not borrow Delegatee reference from signer") } diff --git a/transactions/remove_from_delegatee_as_updater.cdc b/transactions/remove_from_delegatee_as_updater.cdc index 6e65618..eb344a4 100644 --- a/transactions/remove_from_delegatee_as_updater.cdc +++ b/transactions/remove_from_delegatee_as_updater.cdc @@ -1,19 +1,19 @@ -import "ContractUpdater" +import "StagedContractUpdates" transaction { - let delegatee: &ContractUpdater.Delegatee{ContractUpdater.DelegateePublic} - let updaterCap: Capability<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}> + let delegatee: &StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic} + let updaterCap: Capability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}> let updaterID: UInt64 prepare(signer: AuthAccount) { - let delegateeAccount = getAccount(ContractUpdater.getContractDelegateeAddress()) - self.delegatee = delegateeAccount.getCapability<&ContractUpdater.Delegatee{ContractUpdater.DelegateePublic}>( - ContractUpdater.DelegateePublicPath + let delegateeAccount = getAccount(StagedContractUpdates.getContractDelegateeAddress()) + self.delegatee = delegateeAccount.getCapability<&StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic}>( + StagedContractUpdates.DelegateePublicPath ).borrow() ?? panic("Could not borrow Delegatee reference") - self.updaterCap = signer.getCapability<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}>( - ContractUpdater.DelegatedUpdaterPrivatePath + self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.DelegatedUpdaterPrivatePath ) self.updaterID = self.updaterCap.borrow()?.getID() ?? panic("Invalid Updater Capability retrieved from signer!") } diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/setup_updater_multi_account.cdc index f0e5660..be2a1a8 100644 --- a/transactions/setup_updater_multi_account.cdc +++ b/transactions/setup_updater_multi_account.cdc @@ -1,6 +1,6 @@ #allowAccountLinking -import "ContractUpdater" +import "StagedContractUpdates" /// Configures and Updater resource, assuming signing account is the account with the contract to update. This demos an /// advanced case where an update deployment involves multiple accounts and contracts. @@ -15,7 +15,7 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen prepare(signer: AuthAccount) { // Abort if Updater is already configured in signer's account - if signer.type(at: ContractUpdater.UpdaterStoragePath) != nil { + if signer.type(at: StagedContractUpdates.UpdaterStoragePath) != nil { panic("Updater already configured at expected path!") } @@ -27,28 +27,28 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen continue } let accountCap = signer.inbox.claim<&AuthAccount>( - ContractUpdater.inboxAccountCapabilityNamePrefix.concat(signer.address.toString()), + StagedContractUpdates.inboxAccountCapabilityNamePrefix.concat(signer.address.toString()), provider: address ) ?? panic("No AuthAccount Capability found in Inbox for signer at address: ".concat(address.toString())) accountCaps.append(accountCap) seenAddresses.append(address) } // Construct deployment from config - let deployments = ContractUpdater.getDeploymentFromConfig(deploymentConfig) + let deployments = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) // Construct the updater, save and link Capabilities - let contractUpdater: @ContractUpdater.Updater <- ContractUpdater.createNewUpdater( + let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( blockUpdateBoundary: blockUpdateBoundary, accounts: accountCaps, deployments: deployments ) signer.save( <-contractUpdater, - to: ContractUpdater.UpdaterStoragePath + to: StagedContractUpdates.UpdaterStoragePath ) - signer.unlink(ContractUpdater.UpdaterPublicPath) - signer.unlink(ContractUpdater.DelegatedUpdaterPrivatePath) - signer.link<&ContractUpdater.Updater{ContractUpdater.UpdaterPublic}>(ContractUpdater.UpdaterPublicPath, target: ContractUpdater.UpdaterStoragePath) - signer.link<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}>(ContractUpdater.DelegatedUpdaterPrivatePath, target: ContractUpdater.UpdaterStoragePath) + signer.unlink(StagedContractUpdates.UpdaterPublicPath) + signer.unlink(StagedContractUpdates.DelegatedUpdaterPrivatePath) + signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath) + signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.DelegatedUpdaterPrivatePath, target: StagedContractUpdates.UpdaterStoragePath) } } \ No newline at end of file diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/setup_updater_single_account_and_contract.cdc index fed5aa1..45a7c58 100644 --- a/transactions/setup_updater_single_account_and_contract.cdc +++ b/transactions/setup_updater_single_account_and_contract.cdc @@ -1,37 +1,37 @@ #allowAccountLinking -import "ContractUpdater" +import "StagedContractUpdates" /// Configures and Updater resource, assuming signing account is the account with the contract to update. This demos a /// simple case where the signer is the deployment account and deployment only includes a single contract. /// transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { prepare(signer: AuthAccount) { - if !signer.getCapability<&AuthAccount>(ContractUpdater.UpdaterContractAccountPrivatePath).check() { - signer.unlink(ContractUpdater.UpdaterContractAccountPrivatePath) - signer.linkAccount(ContractUpdater.UpdaterContractAccountPrivatePath) + if !signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath).check() { + signer.unlink(StagedContractUpdates.UpdaterContractAccountPrivatePath) + signer.linkAccount(StagedContractUpdates.UpdaterContractAccountPrivatePath) } - let accountCap: Capability<&AuthAccount> = signer.getCapability<&AuthAccount>(ContractUpdater.UpdaterContractAccountPrivatePath) - if signer.type(at: ContractUpdater.UpdaterStoragePath) != nil { + let accountCap: Capability<&AuthAccount> = signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath) + if signer.type(at: StagedContractUpdates.UpdaterStoragePath) != nil { panic("Updater already configured at expected path!") } signer.save( - <- ContractUpdater.createNewUpdater( + <- StagedContractUpdates.createNewUpdater( blockUpdateBoundary: blockUpdateBoundary, accounts: [accountCap], deployments: [[ - ContractUpdater.ContractUpdate( + StagedContractUpdates.ContractUpdate( address: signer.address, name: contractName, code: code ) ]] ), - to: ContractUpdater.UpdaterStoragePath + to: StagedContractUpdates.UpdaterStoragePath ) - signer.unlink(ContractUpdater.UpdaterPublicPath) - signer.unlink(ContractUpdater.DelegatedUpdaterPrivatePath) - signer.link<&ContractUpdater.Updater{ContractUpdater.UpdaterPublic}>(ContractUpdater.UpdaterPublicPath, target: ContractUpdater.UpdaterStoragePath) - signer.link<&ContractUpdater.Updater{ContractUpdater.DelegatedUpdater, ContractUpdater.UpdaterPublic}>(ContractUpdater.DelegatedUpdaterPrivatePath, target: ContractUpdater.UpdaterStoragePath) + signer.unlink(StagedContractUpdates.UpdaterPublicPath) + signer.unlink(StagedContractUpdates.DelegatedUpdaterPrivatePath) + signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath) + signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.DelegatedUpdaterPrivatePath, target: StagedContractUpdates.UpdaterStoragePath) } } \ No newline at end of file diff --git a/transactions/update.cdc b/transactions/update.cdc index e253a55..965b653 100644 --- a/transactions/update.cdc +++ b/transactions/update.cdc @@ -1,11 +1,11 @@ #allowAccountLinking -import "ContractUpdater" +import "StagedContractUpdates" /// Executes the update of the stored contract code in the signer's Updater resource /// transaction { prepare(signer: AuthAccount) { - signer.borrow<&ContractUpdater.Updater>(from: ContractUpdater.UpdaterStoragePath) + signer.borrow<&StagedContractUpdates.Updater>(from: StagedContractUpdates.UpdaterStoragePath) ?.update() ?? panic("Could not borrow Updater from signer's storage!") } From adbb8a462d0595068b5e6732e48c9772321934f0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:33:15 -0600 Subject: [PATCH 02/34] update StagedContractUpdates to wrap AuthAccount Cap in Host resource --- contracts/StagedContractUpdates.cdc | 239 ++++++++++++++++------------ 1 file changed, 133 insertions(+), 106 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 4a7fcd0..4d3538e 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -2,10 +2,10 @@ /// some blockheight boundary either by the containing resource's owner or by some delegated party. /// /// The two primary resources involved in this are the @Updater and @Delegatee resources. As their names suggest, the -/// @Updater contains Capabilities for all deployment accounts as well as the corresponding contract code + names in -/// the order of their update deployment as well as a blockheight at or beyond which the update can be performed. The -/// @Delegatee resource can receive Capabilities to the @Updater resource and can perform the update on behalf of the -/// @Updater resource's owner. +/// @Updater contains Capabilities for all deployment accounts (wrapped in @Host resources) as well as the +/// corresponding contract code + names in the order of their update deployment as well as a blockheight at or beyond +/// which the update can be performed. The @Delegatee resource can receive Capabilities to the @Updater resource and +/// can perform the update on behalf of the @Updater resource's owner. /// /// At the time of this writing, failed updates are not handled gracefully and will result in the halted iteration, but /// recent conversations point to the possibility of amending the AuthAccount.Contract API to allow for a graceful @@ -16,25 +16,25 @@ // TODO: Consider how to handle large contracts that exceed the transaction limit // - It's common to chunk contract code and pass over numerous transactions - think about how could support a similar workflow // when configuring an Updater resource -// TODO: We can't rely on dependencies updating in the same transaction, we'll need to allow for blocking update deployments -pub contract StagedContractUpdates { +access(all) contract StagedContractUpdates { - pub let inboxAccountCapabilityNamePrefix: String + access(all) let inboxAccountCapabilityNamePrefix: String /* --- Canonical Paths --- */ // - pub let UpdaterStoragePath: StoragePath - pub let DelegatedUpdaterPrivatePath: PrivatePath - pub let UpdaterPublicPath: PublicPath - pub let UpdaterContractAccountPrivatePath: PrivatePath - pub let DelegateeStoragePath: StoragePath - pub let DelegateePrivatePath: PrivatePath - pub let DelegateePublicPath: PublicPath + access(all) let HostStoragePath: StoragePath + access(all) let StagedUpdaterStoragePath: StoragePath + // access(all) let DelegatedUpdaterPrivatePath: PrivatePath + access(all) let UpdaterPublicPath: PublicPath + // access(all) let UpdaterContractAccountPrivatePath: PrivatePath + access(all) let DelegateeStoragePath: StoragePath + // access(all) let DelegateePrivatePath: PrivatePath + access(all) let DelegateePublicPath: PublicPath /* --- Events --- */ // - pub event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) - pub event UpdaterUpdated( + access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) + access(all) event UpdaterUpdated( updaterUUID: UInt64, updaterAddress: Address?, blockUpdateBoundary: UInt64, @@ -44,14 +44,14 @@ pub contract StagedContractUpdates { failedContracts: [String], updateComplete: Bool ) - pub event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool) + access(all) event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool) /// Represents contract and its corresponding code /// - pub struct ContractUpdate { - pub let address: Address - pub let name: String - pub let code: String + access(all) struct ContractUpdate { + access(all) let address: Address + access(all) let name: String + access(all) let code: String init(address: Address, name: String, code: String) { self.address = address @@ -60,46 +60,77 @@ pub contract StagedContractUpdates { } /// Serializes the address and name into a string - pub fun toString(): String { + access(all) fun toString(): String { return self.address.toString().concat(".").concat(self.name) } /// Returns code as a String - pub fun codeAsCadence(): String { + access(all) fun codeAsCadence(): String { return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") } } - /* --- Updater --- */ + /* --- Host --- */ // - /// Private Capability enabling delegated updates + /// Encapsulates an AuthAccount, exposing only the ability to update contracts on the underlying account /// - pub resource interface DelegatedUpdater { - pub fun update(): Bool + access(all) resource Host { + access(self) let accountCapability: Capability<&AuthAccount> + + init(accountCapability: Capability<&AuthAccount>) { + self.accountCapability = accountCapability + } + + /// Updates the contract with the specified name and code + /// + access(all) fun update(name: String, code: [UInt8]): Bool { + if let account = self.accountCapability.borrow() { + // TODO: Replace update__experimental with tryUpdate() once it's available + // let deploymentResult = account.contracts.tryUpdate(name: name, code: code) + // return deploymentResult.success + account.contracts.update__experimental(name: name, code: code) + return true + } + return false + } + + /// Checks the wrapped AuthAccount Capability + /// + access(all) fun checkAccountCapability(): Bool { + return self.accountCapability.check() + } + + /// Returns the Address of the underlying account + /// + access(all) fun getHostAddress(): Address? { + return self.accountCapability.borrow()?.address + } } + /* --- Updater --- */ + // /// Public interface enabling queries about the Updater /// - pub resource interface UpdaterPublic { - pub fun getID(): UInt64 - pub fun getBlockUpdateBoundary(): UInt64 - pub fun getContractAccountAddresses(): [Address] - pub fun getDeployments(): [[ContractUpdate]] - pub fun getCurrentDeploymentStage(): Int - pub fun getFailedDeployments(): {Int: [String]} - pub fun hasBeenUpdated(): Bool + access(all) resource interface UpdaterPublic { + access(all) fun getID(): UInt64 + access(all) fun getBlockUpdateBoundary(): UInt64 + access(all) fun getContractAccountAddresses(): [Address] + access(all) fun getDeployments(): [[ContractUpdate]] + access(all) fun getCurrentDeploymentStage(): Int + access(all) fun getFailedDeployments(): {Int: [String]} + access(all) fun hasBeenUpdated(): Bool } /// Resource that enables delayed contract updates to a wrapped account at or beyond a specified block height /// - pub resource Updater : UpdaterPublic, DelegatedUpdater { + access(all) resource Updater : UpdaterPublic { /// Update to occur at or beyond this block height // TODO: Consider making this a contract-owned value as it's reflective of the spork height access(self) let blockUpdateBoundary: UInt64 /// Update status for each contract access(self) var updateComplete: Bool /// Capabilities for contract hosting accounts - access(self) let accounts: {Address: Capability<&AuthAccount>} + access(self) let hosts: {Address: Capability<&Host>} /// Updates ordered by their deployment sequence and staged by their dependency depth /// NOTE: Dev should be careful to validate their dependency tree such that updates are performed from root /// to leaf dependencies @@ -111,23 +142,23 @@ pub contract StagedContractUpdates { init( blockUpdateBoundary: UInt64, - accounts: [Capability<&AuthAccount>], + hosts: [Capability<&Host>], deployments: [[ContractUpdate]] ) { self.blockUpdateBoundary = blockUpdateBoundary self.updateComplete = false - self.accounts = {} + self.hosts = {} // Validate given Capabilities - for account in accounts { - if !account.check() { - panic("Account capability is invalid for account: ".concat(account.address.toString())) + for host in hosts { + if !host.check() || !host.borrow()!.checkAccountCapability() { + panic("Host capability is invalid for account: ".concat(host.address.toString())) } - self.accounts.insert(key: account.borrow()!.address, account) + self.hosts.insert(key: host.borrow()!.getHostAddress()!, host) } // Validate given deployment has corresponding account Capabilities for stage in deployments { for contractUpdate in stage { - if !self.accounts.containsKey(contractUpdate.address) { + if !self.hosts.containsKey(contractUpdate.address) { panic("Contract address not found in given accounts: ".concat(contractUpdate.address.toString())) } } @@ -137,10 +168,10 @@ pub contract StagedContractUpdates { self.failedDeployments = {} } - /// Executes the next update stabe using Account.Contracts.update__experimental() for all contracts defined in - /// deployment, returning true if all stages have been attempted and false if stages remain + /// Executes the next update stage for all contracts defined in deployment, returning true if all stages have + /// been attempted and false if stages remain /// - pub fun update(): Bool { + access(all) fun update(): Bool { // Return early if we've already updated if self.updateComplete { return true @@ -153,29 +184,18 @@ pub contract StagedContractUpdates { // Update the contracts as specified in the deployment for contractUpdate in self.deployments[self.currentDeploymentStage] { - // Borrow the contract account - if let account = self.accounts[contractUpdate.address]!.borrow() { - // Update the contract - // TODO: Swap out optional/Bool API tryUpdate() (or similar) and do stuff if update fails - // See: https://github.com/onflow/cadence/issues/2700 - // if account.contracts.tryUpdate(name: contractUpdate.name, code: contractUpdate.code) == false { - // failedAddresses.append(account.address) - // failedContracts.append(contractUpdate.toString()) - // continue - // } else { - // if !updatedAddresses.contains(account.address) { - // updatedAddresses.append(account.address) - // } - // if !updatedContracts.contains(contractUpdate.toString()) { - // updatedContracts.append(contractUpdate.toString()) - // } - // } - account.contracts.update__experimental(name: contractUpdate.name, code: contractUpdate.code.decodeHex()) - if !updatedAddresses.contains(account.address) { - updatedAddresses.append(account.address) - } - if !updatedContracts.contains(contractUpdate.toString()) { - updatedContracts.append(contractUpdate.toString()) + if let host = self.hosts[contractUpdate.address]!.borrow() { + if host.update(name: contractUpdate.name, code: contractUpdate.code.decodeHex()) == false { + failedAddresses.append(contractUpdate.address) + failedContracts.append(contractUpdate.toString()) + continue + } else { + if !updatedAddresses.contains(contractUpdate.address) { + updatedAddresses.append(contractUpdate.address) + } + if !updatedContracts.contains(contractUpdate.toString()) { + updatedContracts.append(contractUpdate.toString()) + } } } } @@ -202,31 +222,31 @@ pub contract StagedContractUpdates { /* --- Public getters --- */ - pub fun getID(): UInt64 { + access(all) fun getID(): UInt64 { return self.uuid } - pub fun getBlockUpdateBoundary(): UInt64 { + access(all) fun getBlockUpdateBoundary(): UInt64 { return self.blockUpdateBoundary } - pub fun getContractAccountAddresses(): [Address] { - return self.accounts.keys + access(all) fun getContractAccountAddresses(): [Address] { + return self.hosts.keys } - pub fun getDeployments(): [[ContractUpdate]] { + access(all) fun getDeployments(): [[ContractUpdate]] { return self.deployments } - pub fun getCurrentDeploymentStage(): Int { + access(all) fun getCurrentDeploymentStage(): Int { return self.currentDeploymentStage } - pub fun getFailedDeployments(): {Int: [String]} { + access(all) fun getFailedDeployments(): {Int: [String]} { return self.failedDeployments } - pub fun hasBeenUpdated(): Bool { + access(all) fun hasBeenUpdated(): Bool { return self.updateComplete } } @@ -235,22 +255,22 @@ pub contract StagedContractUpdates { // /// Public interface for Delegatee /// - pub resource interface DelegateePublic { - pub fun check(id: UInt64): Bool? - pub fun getUpdaterIDs(): [UInt64] - pub fun delegate(updaterCap: Capability<&Updater{DelegatedUpdater, UpdaterPublic}>) - pub fun removeAsUpdater(updaterCap: Capability<&Updater{DelegatedUpdater, UpdaterPublic}>) + access(all) resource interface DelegateePublic { + access(all) fun check(id: UInt64): Bool? + access(all) fun getUpdaterIDs(): [UInt64] + access(all) fun delegate(updaterCap: Capability<&Updater>) + access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) } /// Resource that executed delegated updates /// - pub resource Delegatee : DelegateePublic { + access(all) resource Delegatee : DelegateePublic { // TODO: Block Height - All DelegatedUpdaters must be updated at or beyond this block height // access(self) let blockUpdateBoundary: UInt64 /// Track all delegated updaters // TODO: If we support staged updates, we'll want visibility into the number of stages and progress through all // maybe removing after stages have been complete or failed - access(self) let delegatedUpdaters: {UInt64: Capability<&Updater{DelegatedUpdater, UpdaterPublic}>} + access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} init() { self.delegatedUpdaters = {} @@ -258,19 +278,19 @@ pub contract StagedContractUpdates { /// Checks if the specified DelegatedUpdater Capability is contained and valid /// - pub fun check(id: UInt64): Bool? { + access(all) fun check(id: UInt64): Bool? { return self.delegatedUpdaters[id]?.check() ?? nil } /// Returns the IDs of the delegated updaters /// - pub fun getUpdaterIDs(): [UInt64] { + access(all) fun getUpdaterIDs(): [UInt64] { return self.delegatedUpdaters.keys } /// Allows for the delegation of updates to a contract /// - pub fun delegate(updaterCap: Capability<&Updater{DelegatedUpdater, UpdaterPublic}>) { + access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { updaterCap.check(): "Invalid DelegatedUpdater Capability!" } @@ -287,7 +307,7 @@ pub contract StagedContractUpdates { /// Enables Updaters to remove their delegation /// - pub fun removeAsUpdater(updaterCap: Capability<&Updater{DelegatedUpdater, UpdaterPublic}>) { + access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) { pre { updaterCap.check(): "Invalid DelegatedUpdater Capability!" self.delegatedUpdaters.containsKey(updaterCap.borrow()!.getID()): "No Updater found for ID!" @@ -299,7 +319,7 @@ pub contract StagedContractUpdates { /// Executes update on the specified Updater /// // TODO: Consider removing Capabilities once we get signal that the Updater has been completed - pub fun update(updaterIDs: [UInt64]): [UInt64] { + access(all) fun update(updaterIDs: [UInt64]): [UInt64] { let failed: [UInt64] = [] for id in updaterIDs { @@ -321,18 +341,18 @@ pub contract StagedContractUpdates { } /// Enables admin removal of a DelegatedUpdater Capability - pub fun removeDelegatedUpdater(id: UInt64) { + access(all) fun removeDelegatedUpdater(id: UInt64) { if !self.delegatedUpdaters.containsKey(id) { return } - let updaterCap = self.delegatedUpdaters.remove(key: id)! - emit UpdaterDelegationChanged(updaterUUID: id, updaterAddress: updaterCap.borrow()?.owner?.address, delegated: false) + let stagedUpdaterCap = self.delegatedUpdaters.remove(key: id)! + emit UpdaterDelegationChanged(updaterUUID: id, updaterAddress: stagedUpdaterCap.borrow()?.owner?.address, delegated: false) } } /// Returns the Address of the Delegatee associated with this contract /// - pub fun getContractDelegateeAddress(): Address { + access(all) fun getContractDelegateeAddress(): Address { return self.account.address } @@ -343,7 +363,7 @@ pub contract StagedContractUpdates { /// deployment and the order of the deployments themselves. Each entry in the inner array must be exactly one /// key-value pair, where the key is the address of the associated contract name and code. /// - pub fun getDeploymentFromConfig(_ deploymentConfig: [[{Address: {String: String}}]]): [[ContractUpdate]] { + access(all) fun getDeploymentFromConfig(_ deploymentConfig: [[{Address: {String: String}}]]): [[ContractUpdate]] { let deployments: [[ContractUpdate]] = [] for deploymentStage in deploymentConfig { @@ -373,36 +393,43 @@ pub contract StagedContractUpdates { return deployments } + /// Returns a new Host resource + /// + access(all) fun createNewHost(accountCap: Capability<&AuthAccount>): @Host { + return <- create Host(accountCapability: accountCap) + } + /// Returns a new Updater resource /// - pub fun createNewUpdater( + access(all) fun createNewUpdater( blockUpdateBoundary: UInt64, - accounts: [Capability<&AuthAccount>], + hosts: [Capability<&Host>], deployments: [[ContractUpdate]] ): @Updater { - let updater <- create Updater(blockUpdateBoundary: blockUpdateBoundary, accounts: accounts, deployments: deployments) + let updater <- create Updater(blockUpdateBoundary: blockUpdateBoundary, hosts: hosts, deployments: deployments) emit UpdaterCreated(updaterUUID: updater.uuid, blockUpdateBoundary: blockUpdateBoundary) return <- updater } /// Creates a new Delegatee resource enabling caller to self-host their Delegatee /// - pub fun createNewDelegatee(): @Delegatee { + access(all) fun createNewDelegatee(): @Delegatee { return <- create Delegatee() } init() { self.inboxAccountCapabilityNamePrefix = "StagedContractUpdatesAccountCapability_" - - self.UpdaterStoragePath = StoragePath(identifier: "StagedContractUpdates_".concat(self.account.address.toString()))! - self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegated_".concat(self.account.address.toString()))! - self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesPublic_".concat(self.account.address.toString()))! - self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(self.account.address.toString()))! + self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(self.account.address.toString()))! + // self.HostPrivatePath = PrivatePath(identifier: "StagedContractUpdatesHost_".concat(self.account.address.toString()))! + self.StagedUpdaterStoragePath = StoragePath(identifier: "StagedContractUpdatesUpdater_".concat(self.account.address.toString()))! + // self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatedUpdater_".concat(self.account.address.toString()))! + self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesUpdaterPublic_".concat(self.account.address.toString()))! + // self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(self.account.address.toString()))! self.DelegateeStoragePath = StoragePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! - self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! + // self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(self.account.address.toString()))! self.account.save(<-create Delegatee(), to: self.DelegateeStoragePath) - self.account.link<&Delegatee{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) + self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) } } \ No newline at end of file From 2b2187a487f13bb1e8f8a20946fd234e457bdab7 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:32:38 -0600 Subject: [PATCH 03/34] update transactions to new StagedContractUpdates interfaces --- contracts/Foo.cdc | 2 +- contracts/Foo_update.cdc | 2 +- contracts/StagedContractUpdates.cdc | 57 +++++++++++-------- transactions/delegate.cdc | 36 +++++++++--- transactions/execute_delegated_updates.cdc | 10 +++- .../publish_auth_account_capability.cdc | 28 --------- transactions/publish_host_capability.cdc | 46 +++++++++++++++ transactions/setup_updater_multi_account.cdc | 26 ++++----- ...up_updater_single_account_and_contract.cdc | 48 ++++++++++++---- transactions/tick_tock.cdc | 2 +- transactions/update.cdc | 4 +- 11 files changed, 168 insertions(+), 93 deletions(-) delete mode 100644 transactions/publish_auth_account_capability.cdc create mode 100644 transactions/publish_host_capability.cdc diff --git a/contracts/Foo.cdc b/contracts/Foo.cdc index 22be029..41d1bc2 100644 --- a/contracts/Foo.cdc +++ b/contracts/Foo.cdc @@ -2,4 +2,4 @@ pub contract Foo { pub fun foo(): String { return "foo" } -} \ No newline at end of file +} diff --git a/contracts/Foo_update.cdc b/contracts/Foo_update.cdc index b8cc698..d1e69bb 100644 --- a/contracts/Foo_update.cdc +++ b/contracts/Foo_update.cdc @@ -2,4 +2,4 @@ pub contract Foo { pub fun foo(): String { return "bar" } -} \ No newline at end of file +} diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 4d3538e..e389203 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -1,15 +1,15 @@ -/// This contract defines resources which enable storage of contract code for the purposes of updating at or beyond +/// This contract defines resources which enable storage of contract code for the purposes of updating at or beyond /// some blockheight boundary either by the containing resource's owner or by some delegated party. /// /// The two primary resources involved in this are the @Updater and @Delegatee resources. As their names suggest, the -/// @Updater contains Capabilities for all deployment accounts (wrapped in @Host resources) as well as the +/// @Updater contains Capabilities for all deployment accounts (wrapped in @Host resources) as well as the /// corresponding contract code + names in the order of their update deployment as well as a blockheight at or beyond -/// which the update can be performed. The @Delegatee resource can receive Capabilities to the @Updater resource and +/// which the update can be performed. The @Delegatee resource can receive Capabilities to the @Updater resource and /// can perform the update on behalf of the @Updater resource's owner. /// /// At the time of this writing, failed updates are not handled gracefully and will result in the halted iteration, but /// recent conversations point to the possibility of amending the AuthAccount.Contract API to allow for a graceful -/// recovery from failed updates. If this method is not added, we'll want to reconsider the approach in favor of a +/// recovery from failed updates. If this method is not added, we'll want to reconsider the approach in favor of a /// single update() call per transaction. /// See the following issue for more info: https://github.com/onflow/cadence/issues/2700 /// @@ -18,12 +18,12 @@ // when configuring an Updater resource access(all) contract StagedContractUpdates { - access(all) let inboxAccountCapabilityNamePrefix: String + access(all) let inboxHostCapabilityNamePrefix: String /* --- Canonical Paths --- */ // access(all) let HostStoragePath: StoragePath - access(all) let StagedUpdaterStoragePath: StoragePath + access(all) let UpdaterStoragePath: StoragePath // access(all) let DelegatedUpdaterPrivatePath: PrivatePath access(all) let UpdaterPublicPath: PublicPath // access(all) let UpdaterContractAccountPrivatePath: PrivatePath @@ -132,7 +132,7 @@ access(all) contract StagedContractUpdates { /// Capabilities for contract hosting accounts access(self) let hosts: {Address: Capability<&Host>} /// Updates ordered by their deployment sequence and staged by their dependency depth - /// NOTE: Dev should be careful to validate their dependency tree such that updates are performed from root + /// NOTE: Dev should be careful to validate their dependency tree such that updates are performed from root /// to leaf dependencies access(self) let deployments: [[ContractUpdate]] /// Current deployment stage @@ -176,7 +176,7 @@ access(all) contract StagedContractUpdates { if self.updateComplete { return true } - + let updatedAddresses: [Address] = [] let failedAddresses: [Address] = [] let updatedContracts: [String] = [] @@ -199,14 +199,14 @@ access(all) contract StagedContractUpdates { } } } - + if failedContracts.length > 0 { self.failedDeployments.insert(key: self.currentDeploymentStage, failedContracts) } - + self.currentDeploymentStage = self.currentDeploymentStage + 1 self.updateComplete = self.currentDeploymentStage == self.deployments.length - + emit UpdaterUpdated( updaterUUID: self.uuid, updaterAddress: self.owner?.address, @@ -282,7 +282,7 @@ access(all) contract StagedContractUpdates { return self.delegatedUpdaters[id]?.check() ?? nil } - /// Returns the IDs of the delegated updaters + /// Returns the IDs of the delegated updaters /// access(all) fun getUpdaterIDs(): [UInt64] { return self.delegatedUpdaters.keys @@ -345,8 +345,12 @@ access(all) contract StagedContractUpdates { if !self.delegatedUpdaters.containsKey(id) { return } - let stagedUpdaterCap = self.delegatedUpdaters.remove(key: id)! - emit UpdaterDelegationChanged(updaterUUID: id, updaterAddress: stagedUpdaterCap.borrow()?.owner?.address, delegated: false) + let updaterCap = self.delegatedUpdaters.remove(key: id)! + emit UpdaterDelegationChanged( + updaterUUID: id, + updaterAddress: updaterCap.borrow()?.owner?.address, + delegated: false + ) } } @@ -418,18 +422,21 @@ access(all) contract StagedContractUpdates { } init() { - self.inboxAccountCapabilityNamePrefix = "StagedContractUpdatesAccountCapability_" - self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(self.account.address.toString()))! - // self.HostPrivatePath = PrivatePath(identifier: "StagedContractUpdatesHost_".concat(self.account.address.toString()))! - self.StagedUpdaterStoragePath = StoragePath(identifier: "StagedContractUpdatesUpdater_".concat(self.account.address.toString()))! - // self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatedUpdater_".concat(self.account.address.toString()))! - self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesUpdaterPublic_".concat(self.account.address.toString()))! - // self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(self.account.address.toString()))! - self.DelegateeStoragePath = StoragePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! - // self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(self.account.address.toString()))! - self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(self.account.address.toString()))! + + let contractAddress = self.account.address.toString() + self.inboxHostCapabilityNamePrefix = "StagedContractUpdatesHostCapability_" + + self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! + // self.HostPrivatePath = PrivatePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! + self.UpdaterStoragePath = StoragePath(identifier: "StagedContractUpdatesUpdater_".concat(contractAddress))! + // self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatedUpdater_".concat(contractAddress))! + self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesUpdaterPublic_".concat(contractAddress))! + // self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(contractAddress))! + self.DelegateeStoragePath = StoragePath(identifier: "StagedContractUpdatesDelegatee_".concat(contractAddress))! + // self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(contractAddress))! + self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(contractAddress))! self.account.save(<-create Delegatee(), to: self.DelegateeStoragePath) self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) } -} \ No newline at end of file +} diff --git a/transactions/delegate.cdc b/transactions/delegate.cdc index c67f4a8..c5e896d 100644 --- a/transactions/delegate.cdc +++ b/transactions/delegate.cdc @@ -1,20 +1,41 @@ import "StagedContractUpdates" +/// Creates a private Updater Capability and gives it to the StagedContractUpdates Delegatee +/// transaction { - - let delegatee: &StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic} - let updaterCap: Capability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}> + + let delegatee: &{StagedContractUpdates.DelegateePublic} + let updaterCap: Capability<&StagedContractUpdates.Updater> let updaterID: UInt64 - + prepare(signer: AuthAccount) { + + // Revert if the signer doesn't already have an Updater configured + if signer.type(at: StagedContractUpdates.UpdaterStoragePath) == nil { + panic("Signer does not have an Updater configured") + } + // Continue... + let delegateeAccount = getAccount(StagedContractUpdates.getContractDelegateeAddress()) - self.delegatee = delegateeAccount.getCapability<&StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic}>( + let updaterPrivatePath = PrivatePath( + identifier: "StagedContractUpdatesUpdater_".concat(delegateeAccount.address.toString()) + )! + + // Get reference to the contract's DelegateePublic + self.delegatee = delegateeAccount.getCapability<&{StagedContractUpdates.DelegateePublic}>( StagedContractUpdates.DelegateePublicPath ).borrow() ?? panic("Could not borrow Delegatee reference") - self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>( - StagedContractUpdates.DelegatedUpdaterPrivatePath + + // Link Updater Capability in private if needed & retrieve + if !signer.getCapability<&StagedContractUpdates.Updater>(updaterPrivatePath).check() { + signer.unlink(updaterPrivatePath) + signer.link<&StagedContractUpdates.Updater>( + updaterPrivatePath, + target: StagedContractUpdates.UpdaterStoragePath ) + } + self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater>(updaterPrivatePath) self.updaterID = self.updaterCap.borrow()?.getID() ?? panic("Invalid Updater Capability retrieved from signer!") } @@ -23,6 +44,7 @@ transaction { } post { + // Confirm successful delegation self.delegatee.check(id: self.updaterID) == true: "Updater Capability was not properly delegated" } } \ No newline at end of file diff --git a/transactions/execute_delegated_updates.cdc b/transactions/execute_delegated_updates.cdc index 070f62f..5c10cfc 100644 --- a/transactions/execute_delegated_updates.cdc +++ b/transactions/execute_delegated_updates.cdc @@ -1,9 +1,13 @@ import "StagedContractUpdates" +/// Updates all current stages of delegated contract updates in contained Updater Capabilities +/// Note: If there are enough Updaters delegated to the signer's Delegatee, this may need to be done in batches +/// due to transaction computation limits +/// transaction { - + let delegatee: &StagedContractUpdates.Delegatee - + prepare(signer: AuthAccount) { self.delegatee = signer.borrow<&StagedContractUpdates.Delegatee>(from: StagedContractUpdates.DelegateeStoragePath) ?? panic("Could not borrow Delegatee reference from signer") @@ -12,4 +16,4 @@ transaction { execute { self.delegatee.update(updaterIDs: self.delegatee.getUpdaterIDs()) } -} \ No newline at end of file +} diff --git a/transactions/publish_auth_account_capability.cdc b/transactions/publish_auth_account_capability.cdc deleted file mode 100644 index 87e66c6..0000000 --- a/transactions/publish_auth_account_capability.cdc +++ /dev/null @@ -1,28 +0,0 @@ -#allowAccountLinking - -import "StagedContractUpdates" - -/// Publishes an Capability on the signer's AuthAccount for the specified recipient -/// -transaction(publishFor: Address) { - - let accountCap: Capability<&AuthAccount> - - prepare(signer: AuthAccount) { - if !signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath).check() { - signer.unlink(StagedContractUpdates.UpdaterContractAccountPrivatePath) - self.accountCap = signer.linkAccount(StagedContractUpdates.UpdaterContractAccountPrivatePath) - ?? panic("Problem linking AuthAccount Capability") - } else { - self.accountCap = signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath) - } - - assert(self.accountCap.check(), message: "Invalid AuthAccount Capability retrieved") - - signer.inbox.publish( - self.accountCap, - name: StagedContractUpdates.inboxAccountCapabilityNamePrefix.concat(publishFor.toString()), - recipient: publishFor - ) - } -} \ No newline at end of file diff --git a/transactions/publish_host_capability.cdc b/transactions/publish_host_capability.cdc new file mode 100644 index 0000000..b14361e --- /dev/null +++ b/transactions/publish_host_capability.cdc @@ -0,0 +1,46 @@ +#allowAccountLinking + +import "StagedContractUpdates" + +/// Publishes an Capability on the signer's AuthAccount for the specified recipient +/// +transaction(publishFor: Address) { + + prepare(signer: AuthAccount) { + + let accountCapPrivatePath: PrivatePath = /private/StagedContractUpdatesAccountCap + let hostPrivatePath: PrivatePath = /private/StagedContractUpdatesHost + + // Setup Capability on underlying signing host account + if !signer.getCapability<&AuthAccount>(accountCapPrivatePath).check() { + signer.unlink(accountCapPrivatePath) + signer.linkAccount(accountCapPrivatePath) + ?? panic("Problem linking AuthAccount Capability") + } + let accountCap = signer.getCapability<&AuthAccount>(accountCapPrivatePath) + + assert(accountCap.check(), message: "Invalid AuthAccount Capability retrieved") + + // Setup Host resource, wrapping the previously configured account capabaility + if signer.type(at: StagedContractUpdates.HostStoragePath) == nil { + signer.save( + <- StagedContractUpdates.createNewHost(accountCap: accountCap), + to: StagedContractUpdates.HostStoragePath + ) + } + if !signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath).check() { + signer.unlink(hostPrivatePath) + signer.link<&StagedContractUpdates.Host>(hostPrivatePath, target: StagedContractUpdates.HostStoragePath) + } + let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) + + assert(hostCap.check(), message: "Invalid Host Capability retrieved") + + // Finally publish the Host Capability to the account that will store the Updater + signer.inbox.publish( + hostCap, + name: StagedContractUpdates.inboxHostCapabilityNamePrefix.concat(publishFor.toString()), + recipient: publishFor + ) + } +} diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/setup_updater_multi_account.cdc index be2a1a8..a50084e 100644 --- a/transactions/setup_updater_multi_account.cdc +++ b/transactions/setup_updater_multi_account.cdc @@ -2,7 +2,7 @@ import "StagedContractUpdates" -/// Configures and Updater resource, assuming signing account is the account with the contract to update. This demos an +/// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos an /// advanced case where an update deployment involves multiple accounts and contracts. /// /// NOTES: deploymentConfig is ordered, and the order is used to determine the order of the contracts in the deployment. @@ -19,27 +19,27 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen panic("Updater already configured at expected path!") } - // Claim all AuthAccount Capabilities. - let accountCaps: [Capability<&AuthAccount>] = [] + // Claim all Host Capabilities from contract addresses + let hostCaps: [Capability<&StagedContractUpdates.Host>] = [] let seenAddresses: [Address] = [] for address in contractAddresses { if seenAddresses.contains(address) { continue } - let accountCap = signer.inbox.claim<&AuthAccount>( - StagedContractUpdates.inboxAccountCapabilityNamePrefix.concat(signer.address.toString()), + let hostCap: Capability<&StagedContractUpdates.Host> = signer.inbox.claim<&StagedContractUpdates.Host>( + StagedContractUpdates.inboxHostCapabilityNamePrefix.concat(signer.address.toString()), provider: address - ) ?? panic("No AuthAccount Capability found in Inbox for signer at address: ".concat(address.toString())) - accountCaps.append(accountCap) + ) ?? panic("No Host Capability found in Inbox for signer at address: ".concat(address.toString())) + hostCaps.append(hostCap) seenAddresses.append(address) } // Construct deployment from config - let deployments = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) - - // Construct the updater, save and link Capabilities + let deployments: [[StagedContractUpdates.ContractUpdate]] = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) + + // Construct the updater, save and link public Capability let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( blockUpdateBoundary: blockUpdateBoundary, - accounts: accountCaps, + hosts: hostCaps, deployments: deployments ) signer.save( @@ -47,8 +47,6 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen to: StagedContractUpdates.UpdaterStoragePath ) signer.unlink(StagedContractUpdates.UpdaterPublicPath) - signer.unlink(StagedContractUpdates.DelegatedUpdaterPrivatePath) signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath) - signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.DelegatedUpdaterPrivatePath, target: StagedContractUpdates.UpdaterStoragePath) } -} \ No newline at end of file +} diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/setup_updater_single_account_and_contract.cdc index 45a7c58..7a5219e 100644 --- a/transactions/setup_updater_single_account_and_contract.cdc +++ b/transactions/setup_updater_single_account_and_contract.cdc @@ -2,23 +2,48 @@ import "StagedContractUpdates" -/// Configures and Updater resource, assuming signing account is the account with the contract to update. This demos a +/// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos a /// simple case where the signer is the deployment account and deployment only includes a single contract. /// transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { + prepare(signer: AuthAccount) { - if !signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath).check() { - signer.unlink(StagedContractUpdates.UpdaterContractAccountPrivatePath) - signer.linkAccount(StagedContractUpdates.UpdaterContractAccountPrivatePath) - } - let accountCap: Capability<&AuthAccount> = signer.getCapability<&AuthAccount>(StagedContractUpdates.UpdaterContractAccountPrivatePath) + + // Ensure Updater has not already been configured at expected path + // Note: If one was already configured, we'd want to load & destroy, but such action should be taken explicitly if signer.type(at: StagedContractUpdates.UpdaterStoragePath) != nil { panic("Updater already configured at expected path!") } + // Continue configuration... + + let accountCapPrivatePath: PrivatePath = /private/StagedContractUpdatesAccountCap + let hostPrivatePath: PrivatePath = /private/StagedContractUpdatesHost + + // Setup Capability on underlying signing host account + if !signer.getCapability<&AuthAccount>(accountCapPrivatePath).check() { + signer.unlink(accountCapPrivatePath) + signer.linkAccount(accountCapPrivatePath) + } + let accountCap = signer.getCapability<&AuthAccount>(accountCapPrivatePath) + + // Setup Host resource, wrapping the previously configured account capabaility + if signer.type(at: StagedContractUpdates.HostStoragePath) == nil { + signer.save( + <- StagedContractUpdates.createNewHost(accountCap: accountCap), + to: StagedContractUpdates.HostStoragePath + ) + } + if !signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath).check() { + signer.unlink(hostPrivatePath) + signer.link<&StagedContractUpdates.Host>(hostPrivatePath, target: StagedContractUpdates.HostStoragePath) + } + let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) + + // Create Updater resource signer.save( <- StagedContractUpdates.createNewUpdater( blockUpdateBoundary: blockUpdateBoundary, - accounts: [accountCap], + hosts: [hostCap], deployments: [[ StagedContractUpdates.ContractUpdate( address: signer.address, @@ -30,8 +55,9 @@ transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { to: StagedContractUpdates.UpdaterStoragePath ) signer.unlink(StagedContractUpdates.UpdaterPublicPath) - signer.unlink(StagedContractUpdates.DelegatedUpdaterPrivatePath) - signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath) - signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.DelegatedUpdaterPrivatePath, target: StagedContractUpdates.UpdaterStoragePath) + signer.link<&{StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.UpdaterPublicPath, + target: StagedContractUpdates.UpdaterStoragePath + ) } -} \ No newline at end of file +} diff --git a/transactions/tick_tock.cdc b/transactions/tick_tock.cdc index 460cfd4..a4fe765 100644 --- a/transactions/tick_tock.cdc +++ b/transactions/tick_tock.cdc @@ -2,4 +2,4 @@ transaction { prepare(signer: AuthAccount) { log("Block height incremented to: ".concat(getCurrentBlock().height.toString())) } -} \ No newline at end of file +} diff --git a/transactions/update.cdc b/transactions/update.cdc index 965b653..57b0722 100644 --- a/transactions/update.cdc +++ b/transactions/update.cdc @@ -1,7 +1,7 @@ #allowAccountLinking import "StagedContractUpdates" -/// Executes the update of the stored contract code in the signer's Updater resource +/// Executes the currently staged update in the signer's Updater resource /// transaction { prepare(signer: AuthAccount) { @@ -9,4 +9,4 @@ transaction { ?.update() ?? panic("Could not borrow Updater from signer's storage!") } -} \ No newline at end of file +} From 57c7453c62a0e114d271593e478ae05e000ce1a0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:51:10 -0600 Subject: [PATCH 04/34] add initial tests and test ci action --- .github/CODEOWNERS | 1 + .github/workflows/ci.yml | 23 ++++++++ flow.json | 36 ++++++++++-- tests/staged_contract_updater_tests.cdc | 77 +++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yml create mode 100644 tests/staged_contract_updater_tests.cdc diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d343a0e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @onflow/flow-smart-contracts \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e60176 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + tests: + name: Flow CLI Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Install Flow CLI + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.8.0 + - name: Run tests + run: make ci + \ No newline at end of file diff --git a/flow.json b/flow.json index af87bab..fa2c7df 100644 --- a/flow.json +++ b/flow.json @@ -1,18 +1,44 @@ { "contracts": { - "A": "./contracts/A.cdc", - "B": "./contracts/B.cdc", - "C": "./contracts/C.cdc", + "A": { + "source": "./contracts/A.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "testing": "0x0000000000000009" + } + }, + "B": { + "source": "./contracts/B.cdc", + "aliases": { + "emulator": "120e725050340cab", + "testing": "0x0000000000000010" + } + }, + "C": { + "source": "./contracts/C.cdc", + "aliases": { + "emulator": "120e725050340cab", + "testing": "0x0000000000000010" + } + }, "StagedContractUpdates": { "source": "./contracts/StagedContractUpdates.cdc", "aliases": { - "emulator": "f8d6e0586b0a20c7" + "emulator": "f8d6e0586b0a20c7", + "testing": "0x0000000000000007" } }, - "Foo": "./contracts/Foo.cdc" + "Foo": { + "source": "./contracts/Foo.cdc", + "aliases": { + "emulator": "e03daebed8ca0615", + "testing": "0x0000000000000008" + } + } }, "networks": { "emulator": "127.0.0.1:3569", + "testing": "127.0.0.1:3569", "mainnet": "access.mainnet.nodes.onflow.org:9000", "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", "testnet": "access.devnet.nodes.onflow.org:9000" diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc new file mode 100644 index 0000000..fa79c24 --- /dev/null +++ b/tests/staged_contract_updater_tests.cdc @@ -0,0 +1,77 @@ +import Test +import BlockchainHelpers + +import "Foo" +import "StagedContractUpdates" + +access(all) let stagedContractUpdatesAccount = Test.getAccount(0x0000000000000007) +access(all) let fooAccount = Test.getAccount(0x0000000000000008) + +// Foo_update.cdc as hex string +access(all) let fooUpdateCode = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" + +access(all) fun setup() { + var err = Test.deployContract( + name: "StagedContractUpdates", + path: "../contracts/StagedContractUpdates.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "Foo", + path: "../contracts/Foo.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(all) fun testSingleContractSingleHostSelfUpdate() { + + let expectedPreUpdateResult = "foo" + let expectedPostUpdateResult = "bar" + + // Validate the pre-update value of Foo.foo() + var fooResult = Foo.foo() + Test.assertEqual(expectedPreUpdateResult, fooResult) + + // Configure Updater resource in Foo contract account + let blockUpdateBoundary = getCurrentBlock().height + 3 + var txResult = executeTransaction( + "../transactions/setup_updater_single_account_and_contract.cdc", + [blockUpdateBoundary, "Foo", fooUpdateCode], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) + + // Confirm event was properly emitted + var events = Test.eventsOfType(Type()) + Test.assertEqual(1, events.length) + + // Mock block advancement + tickTock(advanceBlocks: 3, fooAccount) + + // Execute update as Foo contract account + txResult = executeTransaction( + "../transactions/update.cdc", + [], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) + + events = Test.eventsOfType(Type()) + Test.assertEqual(1, events.length) +} + +/* --- TEST HELPERS --- */ + +access(all) fun tickTock(advanceBlocks: Int, _ signer: Test.Account) { + var blocksAdvanced = 0 + while blocksAdvanced < advanceBlocks { + + let txResult = executeTransaction("../transactions/tick_tock.cdc", [], signer) + Test.expect(txResult, Test.beSucceeded()) + + blocksAdvanced = blocksAdvanced + 1 + } +} From 980e42affe1fbb0139aa780034be4da572c6356c Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:51:24 -0600 Subject: [PATCH 05/34] add .gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6c7fd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +coverage.json +coverage.lcov + +*.pem +*.pkey + +.env \ No newline at end of file From 341643906f20ea0d7a0196606bb31031926da658 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:12:51 -0600 Subject: [PATCH 06/34] add Makefile --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d0dac55 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: test +test: + go test -v ./... + +.PHONY: ci +ci: test \ No newline at end of file From 406d7a292c61422ab7518aa36eba501cf6c95210 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:13:26 -0600 Subject: [PATCH 07/34] update tick_tock.sh emulator mock block advance script --- tick_tock.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/tick_tock.sh b/tick_tock.sh index 576b08e..44016bb 100644 --- a/tick_tock.sh +++ b/tick_tock.sh @@ -1,6 +1,4 @@ #!/bin/bash # Run this transaction several times to increment the block height -flow transactions send ./transactions/tick_tock.cdc - flow transactions send ./transactions/tick_tock.cdc \ No newline at end of file From f555a985d84926764531ee1997e730534950c3b4 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:15:13 -0600 Subject: [PATCH 08/34] fix 'make test' command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d0dac55..7c4cedf 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: test test: - go test -v ./... + flow test --cover --covercode="contracts" --coverprofile="coverage.lcov" tests/*.cdc .PHONY: ci ci: test \ No newline at end of file From 6108b594a1f7b59ac479d974875e3ecb6b5dd9ce Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:39:56 -0600 Subject: [PATCH 09/34] update tests to work around test import bug --- flow.json | 10 ++--- tests/staged_contract_updater_tests.cdc | 52 +++++++++++++++++-------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/flow.json b/flow.json index fa2c7df..db88e06 100644 --- a/flow.json +++ b/flow.json @@ -4,35 +4,35 @@ "source": "./contracts/A.cdc", "aliases": { "emulator": "045a1763c93006ca", - "testing": "0x0000000000000009" + "testing": "0000000000000007" } }, "B": { "source": "./contracts/B.cdc", "aliases": { "emulator": "120e725050340cab", - "testing": "0x0000000000000010" + "testing": "0000000000000007" } }, "C": { "source": "./contracts/C.cdc", "aliases": { "emulator": "120e725050340cab", - "testing": "0x0000000000000010" + "testing": "0000000000000007" } }, "StagedContractUpdates": { "source": "./contracts/StagedContractUpdates.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", - "testing": "0x0000000000000007" + "testing": "0000000000000007" } }, "Foo": { "source": "./contracts/Foo.cdc", "aliases": { "emulator": "e03daebed8ca0615", - "testing": "0x0000000000000008" + "testing": "0000000000000008" } } }, diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index fa79c24..498a06a 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -1,14 +1,11 @@ import Test import BlockchainHelpers -import "Foo" -import "StagedContractUpdates" - -access(all) let stagedContractUpdatesAccount = Test.getAccount(0x0000000000000007) -access(all) let fooAccount = Test.getAccount(0x0000000000000008) +access(all) let admin: Test.Account = Test.getAccount(0x0000000000000007) +access(all) let fooAccount: Test.Account = Test.getAccount(0x0000000000000008) // Foo_update.cdc as hex string -access(all) let fooUpdateCode = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let fooUpdateCode: String = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" access(all) fun setup() { var err = Test.deployContract( @@ -26,17 +23,30 @@ access(all) fun setup() { Test.expect(err, Test.beNil()) } +access(all) fun testTickTock() { + + var blocksAdvanced = 0 + let advanceBlocks = 3 + while blocksAdvanced < advanceBlocks { + + let txResult = executeTransaction("../transactions/tick_tock.cdc", [], fooAccount) + + blocksAdvanced = blocksAdvanced + 1 + } +} + access(all) fun testSingleContractSingleHostSelfUpdate() { - let expectedPreUpdateResult = "foo" - let expectedPostUpdateResult = "bar" + let expectedPreUpdateResult: String = "foo" + let expectedPostUpdateResult: String = "bar" // Validate the pre-update value of Foo.foo() - var fooResult = Foo.foo() - Test.assertEqual(expectedPreUpdateResult, fooResult) + let actualPreUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? + ?? panic("Problem retrieving result of Foo.foo()") + Test.assertEqual(expectedPreUpdateResult, actualPreUpdateResult) // Configure Updater resource in Foo contract account - let blockUpdateBoundary = getCurrentBlock().height + 3 + let blockUpdateBoundary: UInt64 = getCurrentBlock().height + 3 var txResult = executeTransaction( "../transactions/setup_updater_single_account_and_contract.cdc", [blockUpdateBoundary, "Foo", fooUpdateCode], @@ -44,9 +54,10 @@ access(all) fun testSingleContractSingleHostSelfUpdate() { ) Test.expect(txResult, Test.beSucceeded()) - // Confirm event was properly emitted - var events = Test.eventsOfType(Type()) - Test.assertEqual(1, events.length) + // Confirm UpdaterCreated event was properly emitted + // TODO: Uncomment once bug is fixed allowing contract import + // var events = Test.eventsOfType(Type()) + // Test.assertEqual(1, events.length) // Mock block advancement tickTock(advanceBlocks: 3, fooAccount) @@ -59,10 +70,19 @@ access(all) fun testSingleContractSingleHostSelfUpdate() { ) Test.expect(txResult, Test.beSucceeded()) - events = Test.eventsOfType(Type()) - Test.assertEqual(1, events.length) + // Confirm UpdaterUpdated event was properly emitted + // TODO: Uncomment once bug is fixed allowing contract import + // events = Test.eventsOfType(Type()) + // Test.assertEqual(1, events.length) + + // Validate the post-update value of Foo.foo() + let actualPostUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? + ?? panic("Problem retrieving result of Foo.foo()") + Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) } + + /* --- TEST HELPERS --- */ access(all) fun tickTock(advanceBlocks: Int, _ signer: Test.Account) { From 167bf57f08511829aa85b74470a5bd1d644b0677 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:09:57 -0600 Subject: [PATCH 10/34] fix Updater.update() logic to prevent exec before boundary height --- contracts/StagedContractUpdates.cdc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index e389203..c88c109 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -175,6 +175,10 @@ access(all) contract StagedContractUpdates { // Return early if we've already updated if self.updateComplete { return true + } else if getCurrentBlock().height < self.blockUpdateBoundary { + // TODO: Consider returning nil here - indicates an update isn't even attempted. + // Delegatee could then pop on nil since this Updater won't update at the attempted height anyway + return false } let updatedAddresses: [Address] = [] From ae717f1bb8d6c2ab540fa165554e079c61576dda Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:10:14 -0600 Subject: [PATCH 11/34] add test cases --- scripts/get_current_deployment_stage.cdc | 10 +++ scripts/has_been_updated.cdc | 10 +++ tests/staged_contract_updater_tests.cdc | 77 +++++++++++++++++------- 3 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 scripts/get_current_deployment_stage.cdc create mode 100644 scripts/has_been_updated.cdc diff --git a/scripts/get_current_deployment_stage.cdc b/scripts/get_current_deployment_stage.cdc new file mode 100644 index 0000000..7603dd5 --- /dev/null +++ b/scripts/get_current_deployment_stage.cdc @@ -0,0 +1,10 @@ +import "StagedContractUpdates" + +/// Retrieves the current deployment stage of the Updater at the given Address or nil if an Updater is not found +/// +access(all) fun main(updaterAddress: Address): Int? { + return getAccount(updaterAddress).getCapability<&{StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.UpdaterPublicPath + ).borrow() + ?.getCurrentDeploymentStage() +} diff --git a/scripts/has_been_updated.cdc b/scripts/has_been_updated.cdc new file mode 100644 index 0000000..68e6958 --- /dev/null +++ b/scripts/has_been_updated.cdc @@ -0,0 +1,10 @@ +import "StagedContractUpdates" + +/// Retrieves the update completion status of the Updater at the given Address or nil if an Updater is not found +/// +access(all) fun main(updaterAddress: Address): Bool? { + return getAccount(updaterAddress).getCapability<&{StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.UpdaterPublicPath + ).borrow() + ?.hasBeenUpdated() +} diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 498a06a..1c050fb 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -6,6 +6,7 @@ access(all) let fooAccount: Test.Account = Test.getAccount(0x0000000000000008) // Foo_update.cdc as hex string access(all) let fooUpdateCode: String = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let blockHeightBoundaryDelay: UInt64 = 10 access(all) fun setup() { var err = Test.deployContract( @@ -23,22 +24,9 @@ access(all) fun setup() { Test.expect(err, Test.beNil()) } -access(all) fun testTickTock() { - - var blocksAdvanced = 0 - let advanceBlocks = 3 - while blocksAdvanced < advanceBlocks { - - let txResult = executeTransaction("../transactions/tick_tock.cdc", [], fooAccount) - - blocksAdvanced = blocksAdvanced + 1 - } -} - -access(all) fun testSingleContractSingleHostSelfUpdate() { +access(all) fun testSetupSingleContractSingleHostSelfUpdate() { let expectedPreUpdateResult: String = "foo" - let expectedPostUpdateResult: String = "bar" // Validate the pre-update value of Foo.foo() let actualPreUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? @@ -46,8 +34,8 @@ access(all) fun testSingleContractSingleHostSelfUpdate() { Test.assertEqual(expectedPreUpdateResult, actualPreUpdateResult) // Configure Updater resource in Foo contract account - let blockUpdateBoundary: UInt64 = getCurrentBlock().height + 3 - var txResult = executeTransaction( + let blockUpdateBoundary: UInt64 = getCurrentBlock().height + blockHeightBoundaryDelay + let txResult = executeTransaction( "../transactions/setup_updater_single_account_and_contract.cdc", [blockUpdateBoundary, "Foo", fooUpdateCode], fooAccount @@ -59,16 +47,62 @@ access(all) fun testSingleContractSingleHostSelfUpdate() { // var events = Test.eventsOfType(Type()) // Test.assertEqual(1, events.length) + // Validate the current deployment stage is 0 + let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) +} + +access(all) fun testExecuteUpdateFailsBeforeBoundary() { + + // Validate the current deployment stage is still 0 + let stagePrior = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, stagePrior) + + // Execute update as Foo contract account + let txResult = executeTransaction( + "../transactions/update.cdc", + [], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) + + // Validate the current deployment stage is still 0 + let stagePost = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, stagePost) +} + +access(all) fun testExecuteUpdateSucceedsAfterBoundary() { + + let expectedPostUpdateResult: String = "bar" + // Mock block advancement - tickTock(advanceBlocks: 3, fooAccount) + tickTock(advanceBlocks: blockHeightBoundaryDelay, fooAccount) + + // Validate the current deployment stage is still 0 + let stagePrior = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, stagePrior) // Execute update as Foo contract account - txResult = executeTransaction( + let txResult = executeTransaction( "../transactions/update.cdc", [], fooAccount ) Test.expect(txResult, Test.beSucceeded()) + + // Validate the current deployment stage has advanced + let stagePost = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(1, stagePost) + + // Validate the Updater.hasBeenUpdated() returns true + let hasBeenUpdated = executeScript("../scripts/has_been_updated.cdc", [fooAccount.address]).returnValue as! Bool? + ?? panic("Updater was not found at given address") + Test.assertEqual(true, hasBeenUpdated) // Confirm UpdaterUpdated event was properly emitted // TODO: Uncomment once bug is fixed allowing contract import @@ -79,14 +113,13 @@ access(all) fun testSingleContractSingleHostSelfUpdate() { let actualPostUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? ?? panic("Problem retrieving result of Foo.foo()") Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) -} - +} /* --- TEST HELPERS --- */ -access(all) fun tickTock(advanceBlocks: Int, _ signer: Test.Account) { - var blocksAdvanced = 0 +access(all) fun tickTock(advanceBlocks: UInt64, _ signer: Test.Account) { + var blocksAdvanced: UInt64 = 0 while blocksAdvanced < advanceBlocks { let txResult = executeTransaction("../transactions/tick_tock.cdc", [], signer) From 0516c5385ec8abf532ed09d32ae639dacbce34cd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:10:41 -0600 Subject: [PATCH 12/34] add codecov report automation to ci workflow --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e60176..3bce31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,4 +20,8 @@ jobs: run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.8.0 - name: Run tests run: make ci + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file From 841bf937d268f501a62e08d2ea2c86ff046002ef Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:14:22 -0600 Subject: [PATCH 13/34] add test & codecov badges to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b8e1da2..d97412b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # StagedContractUpdates +![Tests](https://github.com/onflow/contract-updater/actions/workflows/ci.yml/badge.svg) +[![codecov](https://codecov.io/gh/onflow/contract-updater/graph/badge.svg?token=TAIKIA95FU)](https://codecov.io/gh/onflow/contract-updater) + > Enables pre-defined contract update deployments to a set of wrapped account at or beyond a specified block height. For > more details about the purpose of this mechanism, see [FLIP 179](https://github.com/onflow/flips/pull/179) From 20cbca6de4a3b0f6ffb4ecdb307518db121b78c0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:32:03 -0600 Subject: [PATCH 14/34] add coverage normalization script --- .github/workflows/ci.yml | 2 ++ normalize_coverage_report.sh | 5 +++++ 2 files changed, 7 insertions(+) create mode 100755 normalize_coverage_report.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bce31a..6ba13c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.8.0 - name: Run tests run: make ci + - name: Normalize coverage report filepaths + run : sh ./normalize_coverage_report.sh - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: diff --git a/normalize_coverage_report.sh b/normalize_coverage_report.sh new file mode 100755 index 0000000..aa07498 --- /dev/null +++ b/normalize_coverage_report.sh @@ -0,0 +1,5 @@ +sed -i 's/A.0000000000000007.StagedContractUpdates/contracts\/StagedContractUpdates.cdc/' coverage.lcov +sed -i 's/A.0000000000000008.Foo/contracts\/Foo.cdc/' coverage.lcov +sed -i 's/A.0000000000000009.A/contracts\/A.cdc/' coverage.lcov +sed -i 's/A.0000000000000010.B/contracts\/B.cdc/' coverage.lcov +sed -i 's/A.0000000000000010.C/contracts\/C.cdc/' coverage.lcov \ No newline at end of file From 1a2bcfab40a7bb8e9ad3a0382e51257d3707cb08 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:32:32 -0600 Subject: [PATCH 15/34] update contract testing aliases in flow.json --- flow.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flow.json b/flow.json index db88e06..4393a61 100644 --- a/flow.json +++ b/flow.json @@ -4,21 +4,21 @@ "source": "./contracts/A.cdc", "aliases": { "emulator": "045a1763c93006ca", - "testing": "0000000000000007" + "testing": "0000000000000009" } }, "B": { "source": "./contracts/B.cdc", "aliases": { "emulator": "120e725050340cab", - "testing": "0000000000000007" + "testing": "0000000000000010" } }, "C": { "source": "./contracts/C.cdc", "aliases": { "emulator": "120e725050340cab", - "testing": "0000000000000007" + "testing": "0000000000000010" } }, "StagedContractUpdates": { From c304a0e6f77da7c50f8eec9da0de50ab9c53dee4 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:46:22 -0600 Subject: [PATCH 16/34] add multi account, multi contract Updater setup test case --- tests/staged_contract_updater_tests.cdc | 94 +++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 1c050fb..83a572a 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -1,12 +1,22 @@ import Test import BlockchainHelpers -access(all) let admin: Test.Account = Test.getAccount(0x0000000000000007) -access(all) let fooAccount: Test.Account = Test.getAccount(0x0000000000000008) +access(all) let blockHeightBoundaryDelay: UInt64 = 15 -// Foo_update.cdc as hex string -access(all) let fooUpdateCode: String = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" -access(all) let blockHeightBoundaryDelay: UInt64 = 10 +// Contract hosts as defined in flow.json +access(all) let admin = Test.getAccount(0x0000000000000007) +access(all) let fooAccount = Test.getAccount(0x0000000000000008) +access(all) let aAccount = Test.getAccount(0x0000000000000009) +access(all) let bcAccount = Test.getAccount(0x0000000000000010) + +// Account that will host the Updater for contracts A, B, and C +access(all) let abcUpdater = Test.createAccount() + +// Content of update contracts as hex strings +access(all) let fooUpdateCode = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let aUpdateCode = "70756220636f6e747261637420696e746572666163652041207b0a202020200a20202020707562207265736f7572636520696e746572666163652049207b0a20202020202020207075622066756e20666f6f28293a20537472696e670a20202020202020207075622066756e2062617228293a20537472696e670a202020207d0a0a20202020707562207265736f757263652052203a2049207b0a20202020202020207075622066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a20202020202020207075622066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" +access(all) let bUpdateCode = "696d706f727420412066726f6d203078303435613137363363393330303663610a0a70756220636f6e74726163742042203a2041207b0a202020200a20202020707562207265736f757263652052203a20412e49207b0a20202020202020207075622066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a20202020202020207075622066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a202020207075622066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" +access(all) let cUpdateCode = "696d706f727420412066726f6d203078303435613137363363393330303663610a696d706f727420422066726f6d203078313230653732353035303334306361620a0a70756220636f6e74726163742043207b0a0a20202020707562206c65742053746f72616765506174683a2053746f72616765506174680a20202020707562206c6574205075626c6963506174683a205075626c6963506174680a0a20202020707562207265736f7572636520696e74657266616365204f757465725075626c6963207b0a20202020202020207075622066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a20202020202020207075622066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a20202020707562207265736f75726365204f75746572203a204f757465725075626c6963207b0a2020202020202020707562206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a20202020202020207075622066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a20202020202020207075622066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a20202020202020207075622066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a20202020202020207075622066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a20202020202020207075622066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" access(all) fun setup() { var err = Test.deployContract( @@ -22,6 +32,27 @@ access(all) fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "A", + path: "../contracts/A.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "B", + path: "../contracts/B.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "C", + path: "../contracts/C.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) } access(all) fun testSetupSingleContractSingleHostSelfUpdate() { @@ -113,7 +144,60 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { let actualPostUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? ?? panic("Problem retrieving result of Foo.foo()") Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) +} +access(all) fun testSetupMultiContractMultiAccountUpdater() { + + let contractAddresses: [Address] = [aAccount.address, bcAccount.address] + let stage0: [{Address: {String: String}}] = [ + { + aAccount.address: { + "A": aUpdateCode + } + } + ] + let stage1: [{Address: {String: String}}] = [ + { + bcAccount.address: { + "B": bUpdateCode + } + } + ] + let stage2: [{Address: {String: String}}] = [ + { + bcAccount.address: { + "C": cUpdateCode + } + } + ] + // TODO: Negative case where no contract address contained for a given contract + let deploymentConfig: [[{Address: {String: String}}]] = [stage0, stage1, stage2] + let blockHeightBoundary: UInt64 = getCurrentBlock().height + blockHeightBoundaryDelay + + let aHostTxResult = executeTransaction( + "../transactions/publish_host_capability.cdc", + [abcUpdater.address], + aAccount + ) + Test.expect(aHostTxResult, Test.beSucceeded()) + + let bcHostTxResult = executeTransaction( + "../transactions/publish_host_capability.cdc", + [abcUpdater.address], + bcAccount + ) + Test.expect(bcHostTxResult, Test.beSucceeded()) + + let setupUpdaterTxResult = executeTransaction( + "../transactions/setup_updater_multi_account.cdc", + [blockHeightBoundary, contractAddresses, deploymentConfig], + abcUpdater + ) + + // Validate the current deployment stage is 0 + let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) } /* --- TEST HELPERS --- */ From cb143601f1e2d45b76078e4c6d6f10f6e59cba87 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:51:15 -0600 Subject: [PATCH 17/34] add checks on Updater init and Delegatee.delegate --- contracts/StagedContractUpdates.cdc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index c88c109..88e68b7 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -145,6 +145,9 @@ access(all) contract StagedContractUpdates { hosts: [Capability<&Host>], deployments: [[ContractUpdate]] ) { + pre { + hosts.length > 0 && deployments.length > 0: "Must provide at least one host and contract update!" + } self.blockUpdateBoundary = blockUpdateBoundary self.updateComplete = false self.hosts = {} @@ -297,6 +300,7 @@ access(all) contract StagedContractUpdates { access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { updaterCap.check(): "Invalid DelegatedUpdater Capability!" + updaterCap.borrow()!.hasBeenUpdated() == false: "Updater has already been updated!" } let updater = updaterCap.borrow()! if self.delegatedUpdaters.containsKey(updater.getID()) { From b5ede0db6217b8f98b50811d94c0090fe42f365f Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:23:41 -0600 Subject: [PATCH 18/34] impl MetadataViews.Resolver in .Updater & add standard dependencies --- contracts/StagedContractUpdates.cdc | 59 +- contracts/{ => example}/A.cdc | 0 contracts/{ => example}/A_update.cdc | 0 contracts/{ => example}/B.cdc | 0 contracts/{ => example}/B_update.cdc | 0 contracts/{ => example}/C.cdc | 0 contracts/{ => example}/C_update.cdc | 0 contracts/{ => example}/Foo.cdc | 0 contracts/{ => example}/Foo_update.cdc | 0 contracts/standards/FungibleToken.cdc | 237 ++++++++ contracts/standards/MetadataViews.cdc | 740 +++++++++++++++++++++++ contracts/standards/NonFungibleToken.cdc | 202 +++++++ flow.json | 55 +- setup.sh | 8 +- 14 files changed, 1283 insertions(+), 18 deletions(-) rename contracts/{ => example}/A.cdc (100%) rename contracts/{ => example}/A_update.cdc (100%) rename contracts/{ => example}/B.cdc (100%) rename contracts/{ => example}/B_update.cdc (100%) rename contracts/{ => example}/C.cdc (100%) rename contracts/{ => example}/C_update.cdc (100%) rename contracts/{ => example}/Foo.cdc (100%) rename contracts/{ => example}/Foo_update.cdc (100%) create mode 100644 contracts/standards/FungibleToken.cdc create mode 100644 contracts/standards/MetadataViews.cdc create mode 100644 contracts/standards/NonFungibleToken.cdc diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 88e68b7..c696952 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -1,3 +1,5 @@ +import "MetadataViews" + /// This contract defines resources which enable storage of contract code for the purposes of updating at or beyond /// some blockheight boundary either by the containing resource's owner or by some delegated party. /// @@ -70,6 +72,37 @@ access(all) contract StagedContractUpdates { } } + /// Represents the status of an Updater resource, mirroring the encapsulated values of an Updater resource and + /// defined here for ease of querying via MetadataViews.ViewResolver interface + /// + access(all) struct UpdaterInfo { + access(all) let id: UInt64 + access(all) let blockUpdateBoundary: UInt64 + access(all) let updateComplete: Bool + access(all) let hostAddresses: [Address] + access(all) let deployments: [[ContractUpdate]] + access(all) let currentDeploymentStage: Int + access(all) let failedDeployments: {Int: [String]} + + init( + id: UInt64, + blockUpdateBoundary: UInt64, + updateComplete: Bool, + hostAddresses: [Address], + deployments: [[ContractUpdate]], + currentDeploymentStage: Int, + failedDeployments: {Int: [String]} + ) { + self.id = id + self.blockUpdateBoundary = blockUpdateBoundary + self.updateComplete = updateComplete + self.hostAddresses = hostAddresses + self.deployments = deployments + self.currentDeploymentStage = currentDeploymentStage + self.failedDeployments = failedDeployments + } + } + /* --- Host --- */ // /// Encapsulates an AuthAccount, exposing only the ability to update contracts on the underlying account @@ -123,11 +156,12 @@ access(all) contract StagedContractUpdates { /// Resource that enables delayed contract updates to a wrapped account at or beyond a specified block height /// - access(all) resource Updater : UpdaterPublic { + access(all) resource Updater : UpdaterPublic, MetadataViews.Resolver { /// Update to occur at or beyond this block height // TODO: Consider making this a contract-owned value as it's reflective of the spork height access(self) let blockUpdateBoundary: UInt64 - /// Update status for each contract + /// Update status defining whether all update stages have been *attempted* + /// NOTE: `true` does not necessarily mean all updates were successful access(self) var updateComplete: Bool /// Capabilities for contract hosting accounts access(self) let hosts: {Address: Capability<&Host>} @@ -256,6 +290,27 @@ access(all) contract StagedContractUpdates { access(all) fun hasBeenUpdated(): Bool { return self.updateComplete } + + /* --- MetadataViews.Resolver --- */ + + access(all) fun getViews(): [Type] { + return [Type()] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + if view == Type() { + return UpdaterInfo( + id: self.uuid, + blockUpdateBoundary: self.blockUpdateBoundary, + updateComplete: self.updateComplete, + hostAddresses: self.hosts.keys, + deployments: self.deployments, + currentDeploymentStage: self.currentDeploymentStage, + failedDeployments: self.failedDeployments + ) + } + return nil + } } /* --- Delegatee --- */ diff --git a/contracts/A.cdc b/contracts/example/A.cdc similarity index 100% rename from contracts/A.cdc rename to contracts/example/A.cdc diff --git a/contracts/A_update.cdc b/contracts/example/A_update.cdc similarity index 100% rename from contracts/A_update.cdc rename to contracts/example/A_update.cdc diff --git a/contracts/B.cdc b/contracts/example/B.cdc similarity index 100% rename from contracts/B.cdc rename to contracts/example/B.cdc diff --git a/contracts/B_update.cdc b/contracts/example/B_update.cdc similarity index 100% rename from contracts/B_update.cdc rename to contracts/example/B_update.cdc diff --git a/contracts/C.cdc b/contracts/example/C.cdc similarity index 100% rename from contracts/C.cdc rename to contracts/example/C.cdc diff --git a/contracts/C_update.cdc b/contracts/example/C_update.cdc similarity index 100% rename from contracts/C_update.cdc rename to contracts/example/C_update.cdc diff --git a/contracts/Foo.cdc b/contracts/example/Foo.cdc similarity index 100% rename from contracts/Foo.cdc rename to contracts/example/Foo.cdc diff --git a/contracts/Foo_update.cdc b/contracts/example/Foo_update.cdc similarity index 100% rename from contracts/Foo_update.cdc rename to contracts/example/Foo_update.cdc diff --git a/contracts/standards/FungibleToken.cdc b/contracts/standards/FungibleToken.cdc new file mode 100644 index 0000000..48b092d --- /dev/null +++ b/contracts/standards/FungibleToken.cdc @@ -0,0 +1,237 @@ +/** + +# The Flow Fungible Token standard + +## `FungibleToken` contract interface + +The interface that all Fungible Token contracts would have to conform to. +If a users wants to deploy a new token contract, their contract +would need to implement the FungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## `Vault` resource + +Each account that owns tokens would need to have an instance +of the Vault resource stored in their account storage. + +The Vault resource has methods that the owner and other users can call. + +## `Provider`, `Receiver`, and `Balance` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields functions +in one or more of the interfaces. + +It also gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +By using resources and interfaces, users of Fungible Token contracts +can send and receive tokens peer-to-peer, without having to interact +with a central ledger smart contract. To send tokens to another user, +a user would simply withdraw the tokens from their Vault, then call +the deposit function on another user's Vault to complete the transfer. + +*/ + +/// The interface that Fungible Token contracts implement. +/// +pub contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + pub var totalSupply: UFix64 + + /// The event that is emitted when the contract is created + pub event TokensInitialized(initialSupply: UFix64) + + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + /// The event that is emitted when tokens are deposited into a Vault + pub event TokensDeposited(amount: UFix64, to: Address?) + + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on `balance` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// Subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + post { + // `result` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) + + /// Below is referenced from the FLIP #69 https://github.com/onflow/flips/blob/main/flips/20230206-fungible-token-vault-type-discovery.md + /// + /// Returns the dictionary of Vault types that the the receiver is able to accept in its `deposit` method + /// this then it would return `{Type<@FlowToken.Vault>(): true}` and if any custom receiver + /// uses the default implementation then it would return empty dictionary as its parent + /// resource doesn't conform with the `FungibleToken.Vault` resource. + /// + /// Custom receiver implementations are expected to upgrade their contracts to add an implementation + /// that supports this method because it is very valuable for various applications to have. + /// + /// @return dictionary of supported deposit vault types by the implementing resource. + /// + pub fun getSupportedVaultTypes(): {Type: Bool} { + // Below check is implemented to make sure that run-time type would + // only get returned when the parent resource conforms with `FungibleToken.Vault`. + if self.getType().isSubtype(of: Type<@FungibleToken.Vault>()) { + return {self.getType(): true} + } else { + // Return an empty dictionary as the default value for resource who don't + // implement `FungibleToken.Vault`, such as `FungibleTokenSwitchboard`, `TokenForwarder` etc. + return {} + } + } + } + + /// The interface that contains the `balance` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + pub var balance: UFix64 + + init(balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + + /// Function that returns all the Metadata Views implemented by a Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type] { + return [] + } + + /// Function that resolves a metadata view for this fungible token by type. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// The resource that contains the functions to send and receive tokens. + /// The declaration of a concrete type in a contract interface means that + /// every Fungible Token contract that implements the FungibleToken interface + /// must define a concrete `Vault` resource that conforms to the `Provider`, `Receiver`, + /// and `Balance` interfaces, and declares their required fields and functions + /// + pub resource Vault: Provider, Receiver, Balance { + + /// The total balance of the vault + pub var balance: UFix64 + + // The conforming type must declare an initializer + // that allows providing the initial balance of the Vault + // + init(balance: UFix64) + + /// Subtracts `amount` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function `before` to get the value of the `balance` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// Allows any user to create a new Vault that has a zero balance + /// + /// @return The new Vault resource + /// + pub fun createEmptyVault(): @Vault { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } +} diff --git a/contracts/standards/MetadataViews.cdc b/contracts/standards/MetadataViews.cdc new file mode 100644 index 0000000..59c1927 --- /dev/null +++ b/contracts/standards/MetadataViews.cdc @@ -0,0 +1,740 @@ +import "FungibleToken" +import "NonFungibleToken" + +/// This contract implements the metadata standard proposed +/// in FLIP-0636. +/// +/// Ref: https://github.com/onflow/flips/blob/main/application/20210916-nft-metadata.md +/// +/// Structs and resources can implement one or more +/// metadata types, called views. Each view type represents +/// a different kind of metadata, such as a creator biography +/// or a JPEG image file. +/// +pub contract MetadataViews { + + /// Provides access to a set of metadata views. A struct or + /// resource (e.g. an NFT) can implement this interface to provide access to + /// the views that it supports. + /// + pub resource interface Resolver { + pub fun getViews(): [Type] + pub fun resolveView(_ view: Type): AnyStruct? + } + + /// A group of view resolvers indexed by ID. + /// + pub resource interface ResolverCollection { + pub fun borrowViewResolver(id: UInt64): &{Resolver} + pub fun getIDs(): [UInt64] + } + + /// NFTView wraps all Core views along `id` and `uuid` fields, and is used + /// to give a complete picture of an NFT. Most NFTs should implement this + /// view. + /// + pub struct NFTView { + pub let id: UInt64 + pub let uuid: UInt64 + pub let display: Display? + pub let externalURL: ExternalURL? + pub let collectionData: NFTCollectionData? + pub let collectionDisplay: NFTCollectionDisplay? + pub let royalties: Royalties? + pub let traits: Traits? + + init( + id : UInt64, + uuid : UInt64, + display : Display?, + externalURL : ExternalURL?, + collectionData : NFTCollectionData?, + collectionDisplay : NFTCollectionDisplay?, + royalties : Royalties?, + traits: Traits? + ) { + self.id = id + self.uuid = uuid + self.display = display + self.externalURL = externalURL + self.collectionData = collectionData + self.collectionDisplay = collectionDisplay + self.royalties = royalties + self.traits = traits + } + } + + /// Helper to get an NFT view + /// + /// @param id: The NFT id + /// @param viewResolver: A reference to the resolver resource + /// @return A NFTView struct + /// + pub fun getNFTView(id: UInt64, viewResolver: &{Resolver}) : NFTView { + let nftView = viewResolver.resolveView(Type()) + if nftView != nil { + return nftView! as! NFTView + } + + return NFTView( + id : id, + uuid: viewResolver.uuid, + display: self.getDisplay(viewResolver), + externalURL : self.getExternalURL(viewResolver), + collectionData : self.getNFTCollectionData(viewResolver), + collectionDisplay : self.getNFTCollectionDisplay(viewResolver), + royalties : self.getRoyalties(viewResolver), + traits : self.getTraits(viewResolver) + ) + } + + /// Display is a basic view that includes the name, description and + /// thumbnail for an object. Most objects should implement this view. + /// + pub struct Display { + + /// The name of the object. + /// + /// This field will be displayed in lists and therefore should + /// be short an concise. + /// + pub let name: String + + /// A written description of the object. + /// + /// This field will be displayed in a detailed view of the object, + /// so can be more verbose (e.g. a paragraph instead of a single line). + /// + pub let description: String + + /// A small thumbnail representation of the object. + /// + /// This field should be a web-friendly file (i.e JPEG, PNG) + /// that can be displayed in lists, link previews, etc. + /// + pub let thumbnail: AnyStruct{File} + + init( + name: String, + description: String, + thumbnail: AnyStruct{File} + ) { + self.name = name + self.description = description + self.thumbnail = thumbnail + } + } + + /// Helper to get Display in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Display struct + /// + pub fun getDisplay(_ viewResolver: &{Resolver}) : Display? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Display { + return v + } + } + return nil + } + + /// Generic interface that represents a file stored on or off chain. Files + /// can be used to references images, videos and other media. + /// + pub struct interface File { + pub fun uri(): String + } + + /// View to expose a file that is accessible at an HTTP (or HTTPS) URL. + /// + pub struct HTTPFile: File { + pub let url: String + + init(url: String) { + self.url = url + } + + pub fun uri(): String { + return self.url + } + } + + /// View to expose a file stored on IPFS. + /// IPFS images are referenced by their content identifier (CID) + /// rather than a direct URI. A client application can use this CID + /// to find and load the image via an IPFS gateway. + /// + pub struct IPFSFile: File { + + /// CID is the content identifier for this IPFS file. + /// + /// Ref: https://docs.ipfs.io/concepts/content-addressing/ + /// + pub let cid: String + + /// Path is an optional path to the file resource in an IPFS directory. + /// + /// This field is only needed if the file is inside a directory. + /// + /// Ref: https://docs.ipfs.io/concepts/file-systems/ + /// + pub let path: String? + + init(cid: String, path: String?) { + self.cid = cid + self.path = path + } + + /// This function returns the IPFS native URL for this file. + /// Ref: https://docs.ipfs.io/how-to/address-ipfs-on-web/#native-urls + /// + /// @return The string containing the file uri + /// + pub fun uri(): String { + if let path = self.path { + return "ipfs://".concat(self.cid).concat("/").concat(path) + } + + return "ipfs://".concat(self.cid) + } + } + + /// Optional view for collections that issue multiple objects + /// with the same or similar metadata, for example an X of 100 set. This + /// information is useful for wallets and marketplaces. + /// An NFT might be part of multiple editions, which is why the edition + /// information is returned as an arbitrary sized array + /// + pub struct Edition { + + /// The name of the edition + /// For example, this could be Set, Play, Series, + /// or any other way a project could classify its editions + pub let name: String? + + /// The edition number of the object. + /// For an "24 of 100 (#24/100)" item, the number is 24. + pub let number: UInt64 + + /// The max edition number of this type of objects. + /// This field should only be provided for limited-editioned objects. + /// For an "24 of 100 (#24/100)" item, max is 100. + /// For an item with unlimited edition, max should be set to nil. + /// + pub let max: UInt64? + + init(name: String?, number: UInt64, max: UInt64?) { + if max != nil { + assert(number <= max!, message: "The number cannot be greater than the max number!") + } + self.name = name + self.number = number + self.max = max + } + } + + /// Wrapper view for multiple Edition views + /// + pub struct Editions { + + /// An arbitrary-sized list for any number of editions + /// that the NFT might be a part of + pub let infoList: [Edition] + + init(_ infoList: [Edition]) { + self.infoList = infoList + } + } + + /// Helper to get Editions in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Editions struct + /// + pub fun getEditions(_ viewResolver: &{Resolver}) : Editions? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Editions { + return v + } + } + return nil + } + + /// View representing a project-defined serial number for a specific NFT + /// Projects have different definitions for what a serial number should be + /// Some may use the NFTs regular ID and some may use a different + /// classification system. The serial number is expected to be unique among + /// other NFTs within that project + /// + pub struct Serial { + pub let number: UInt64 + + init(_ number: UInt64) { + self.number = number + } + } + + /// Helper to get Serial in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Serial struct + /// + pub fun getSerial(_ viewResolver: &{Resolver}) : Serial? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Serial { + return v + } + } + return nil + } + + /// View that defines the composable royalty standard that gives marketplaces a + /// unified interface to support NFT royalties. + /// + pub struct Royalty { + + /// Generic FungibleToken Receiver for the beneficiary of the royalty + /// Can get the concrete type of the receiver with receiver.getType() + /// Recommendation - Users should create a new link for a FlowToken + /// receiver for this using `getRoyaltyReceiverPublicPath()`, and not + /// use the default FlowToken receiver. This will allow users to update + /// the capability in the future to use a more generic capability + pub let receiver: Capability<&AnyResource{FungibleToken.Receiver}> + + /// Multiplier used to calculate the amount of sale value transferred to + /// royalty receiver. Note - It should be between 0.0 and 1.0 + /// Ex - If the sale value is x and multiplier is 0.56 then the royalty + /// value would be 0.56 * x. + /// Generally percentage get represented in terms of basis points + /// in solidity based smart contracts while cadence offers `UFix64` + /// that already supports the basis points use case because its + /// operations are entirely deterministic integer operations and support + /// up to 8 points of precision. + pub let cut: UFix64 + + /// Optional description: This can be the cause of paying the royalty, + /// the relationship between the `wallet` and the NFT, or anything else + /// that the owner might want to specify. + pub let description: String + + init(receiver: Capability<&AnyResource{FungibleToken.Receiver}>, cut: UFix64, description: String) { + pre { + cut >= 0.0 && cut <= 1.0 : "Cut value should be in valid range i.e [0,1]" + } + self.receiver = receiver + self.cut = cut + self.description = description + } + } + + /// Wrapper view for multiple Royalty views. + /// Marketplaces can query this `Royalties` struct from NFTs + /// and are expected to pay royalties based on these specifications. + /// + pub struct Royalties { + + /// Array that tracks the individual royalties + access(self) let cutInfos: [Royalty] + + pub init(_ cutInfos: [Royalty]) { + // Validate that sum of all cut multipliers should not be greater than 1.0 + var totalCut = 0.0 + for royalty in cutInfos { + totalCut = totalCut + royalty.cut + } + assert(totalCut <= 1.0, message: "Sum of cutInfos multipliers should not be greater than 1.0") + // Assign the cutInfos + self.cutInfos = cutInfos + } + + /// Return the cutInfos list + /// + /// @return An array containing all the royalties structs + /// + pub fun getRoyalties(): [Royalty] { + return self.cutInfos + } + } + + /// Helper to get Royalties in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Royalties struct + /// + pub fun getRoyalties(_ viewResolver: &{Resolver}) : Royalties? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Royalties { + return v + } + } + return nil + } + + /// Get the path that should be used for receiving royalties + /// This is a path that will eventually be used for a generic switchboard receiver, + /// hence the name but will only be used for royalties for now. + /// + /// @return The PublicPath for the generic FT receiver + /// + pub fun getRoyaltyReceiverPublicPath(): PublicPath { + return /public/GenericFTReceiver + } + + /// View to represent, a file with an correspoiding mediaType. + /// + pub struct Media { + + /// File for the media + /// + pub let file: AnyStruct{File} + + /// media-type comes on the form of type/subtype as described here + /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + /// + pub let mediaType: String + + init(file: AnyStruct{File}, mediaType: String) { + self.file=file + self.mediaType=mediaType + } + } + + /// Wrapper view for multiple media views + /// + pub struct Medias { + + /// An arbitrary-sized list for any number of Media items + pub let items: [Media] + + init(_ items: [Media]) { + self.items = items + } + } + + /// Helper to get Medias in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Medias struct + /// + pub fun getMedias(_ viewResolver: &{Resolver}) : Medias? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Medias { + return v + } + } + return nil + } + + /// View to represent a license according to https://spdx.org/licenses/ + /// This view can be used if the content of an NFT is licensed. + /// + pub struct License { + pub let spdxIdentifier: String + + init(_ identifier: String) { + self.spdxIdentifier = identifier + } + } + + /// Helper to get License in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional License struct + /// + pub fun getLicense(_ viewResolver: &{Resolver}) : License? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? License { + return v + } + } + return nil + } + + /// View to expose a URL to this item on an external site. + /// This can be used by applications like .find and Blocto to direct users + /// to the original link for an NFT or a project page that describes the NFT collection. + /// eg https://www.my-nft-project.com/overview-of-nft-collection + /// + pub struct ExternalURL { + pub let url: String + + init(_ url: String) { + self.url=url + } + } + + /// Helper to get ExternalURL in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional ExternalURL struct + /// + pub fun getExternalURL(_ viewResolver: &{Resolver}) : ExternalURL? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? ExternalURL { + return v + } + } + return nil + } + + /// View to expose the information needed store and retrieve an NFT. + /// This can be used by applications to setup a NFT collection with proper + /// storage and public capabilities. + /// + pub struct NFTCollectionData { + /// Path in storage where this NFT is recommended to be stored. + pub let storagePath: StoragePath + + /// Public path which must be linked to expose public capabilities of this NFT + /// including standard NFT interfaces and metadataviews interfaces + pub let publicPath: PublicPath + + /// Private path which should be linked to expose the provider + /// capability to withdraw NFTs from the collection holding NFTs + pub let providerPath: PrivatePath + + /// Public collection type that is expected to provide sufficient read-only access to standard + /// functions (deposit + getIDs + borrowNFT) + /// This field is for backwards compatibility with collections that have not used the standard + /// NonFungibleToken.CollectionPublic interface when setting up collections. For new + /// collections, this may be set to be equal to the type specified in `publicLinkedType`. + pub let publicCollection: Type + + /// Type that should be linked at the aforementioned public path. This is normally a + /// restricted type with many interfaces. Notably the `NFT.CollectionPublic`, + /// `NFT.Receiver`, and `MetadataViews.ResolverCollection` interfaces are required. + pub let publicLinkedType: Type + + /// Type that should be linked at the aforementioned private path. This is normally + /// a restricted type with at a minimum the `NFT.Provider` interface + pub let providerLinkedType: Type + + /// Function that allows creation of an empty NFT collection that is intended to store + /// this NFT. + pub let createEmptyCollection: ((): @NonFungibleToken.Collection) + + init( + storagePath: StoragePath, + publicPath: PublicPath, + providerPath: PrivatePath, + publicCollection: Type, + publicLinkedType: Type, + providerLinkedType: Type, + createEmptyCollectionFunction: ((): @NonFungibleToken.Collection) + ) { + pre { + publicLinkedType.isSubtype(of: Type<&{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>()): "Public type must include NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, and MetadataViews.ResolverCollection interfaces." + providerLinkedType.isSubtype(of: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()): "Provider type must include NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, and MetadataViews.ResolverCollection interface." + } + self.storagePath=storagePath + self.publicPath=publicPath + self.providerPath = providerPath + self.publicCollection=publicCollection + self.publicLinkedType=publicLinkedType + self.providerLinkedType = providerLinkedType + self.createEmptyCollection=createEmptyCollectionFunction + } + } + + /// Helper to get NFTCollectionData in a way that will return an typed Optional + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional NFTCollectionData struct + /// + pub fun getNFTCollectionData(_ viewResolver: &{Resolver}) : NFTCollectionData? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? NFTCollectionData { + return v + } + } + return nil + } + + /// View to expose the information needed to showcase this NFT's + /// collection. This can be used by applications to give an overview and + /// graphics of the NFT collection this NFT belongs to. + /// + pub struct NFTCollectionDisplay { + // Name that should be used when displaying this NFT collection. + pub let name: String + + // Description that should be used to give an overview of this collection. + pub let description: String + + // External link to a URL to view more information about this collection. + pub let externalURL: ExternalURL + + // Square-sized image to represent this collection. + pub let squareImage: Media + + // Banner-sized image for this collection, recommended to have a size near 1200x630. + pub let bannerImage: Media + + // Social links to reach this collection's social homepages. + // Possible keys may be "instagram", "twitter", "discord", etc. + pub let socials: {String: ExternalURL} + + init( + name: String, + description: String, + externalURL: ExternalURL, + squareImage: Media, + bannerImage: Media, + socials: {String: ExternalURL} + ) { + self.name = name + self.description = description + self.externalURL = externalURL + self.squareImage = squareImage + self.bannerImage = bannerImage + self.socials = socials + } + } + + /// Helper to get NFTCollectionDisplay in a way that will return a typed + /// Optional + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional NFTCollection struct + /// + pub fun getNFTCollectionDisplay(_ viewResolver: &{Resolver}) : NFTCollectionDisplay? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? NFTCollectionDisplay { + return v + } + } + return nil + } + + /// View to expose rarity information for a single rarity + /// Note that a rarity needs to have either score or description but it can + /// have both + /// + pub struct Rarity { + /// The score of the rarity as a number + pub let score: UFix64? + + /// The maximum value of score + pub let max: UFix64? + + /// The description of the rarity as a string. + /// + /// This could be Legendary, Epic, Rare, Uncommon, Common or any other string value + pub let description: String? + + init(score: UFix64?, max: UFix64?, description: String?) { + if score == nil && description == nil { + panic("A Rarity needs to set score, description or both") + } + + self.score = score + self.max = max + self.description = description + } + } + + /// Helper to get Rarity view in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Rarity struct + /// + pub fun getRarity(_ viewResolver: &{Resolver}) : Rarity? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Rarity { + return v + } + } + return nil + } + + /// View to represent a single field of metadata on an NFT. + /// This is used to get traits of individual key/value pairs along with some + /// contextualized data about the trait + /// + pub struct Trait { + // The name of the trait. Like Background, Eyes, Hair, etc. + pub let name: String + + // The underlying value of the trait, the rest of the fields of a trait provide context to the value. + pub let value: AnyStruct + + // displayType is used to show some context about what this name and value represent + // for instance, you could set value to a unix timestamp, and specify displayType as "Date" to tell + // platforms to consume this trait as a date and not a number + pub let displayType: String? + + // Rarity can also be used directly on an attribute. + // + // This is optional because not all attributes need to contribute to the NFT's rarity. + pub let rarity: Rarity? + + init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) { + self.name = name + self.value = value + self.displayType = displayType + self.rarity = rarity + } + } + + /// Wrapper view to return all the traits on an NFT. + /// This is used to return traits as individual key/value pairs along with + /// some contextualized data about each trait. + pub struct Traits { + pub let traits: [Trait] + + init(_ traits: [Trait]) { + self.traits = traits + } + + /// Adds a single Trait to the Traits view + /// + /// @param Trait: The trait struct to be added + /// + pub fun addTrait(_ t: Trait) { + self.traits.append(t) + } + } + + /// Helper to get Traits view in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Traits struct + /// + pub fun getTraits(_ viewResolver: &{Resolver}) : Traits? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Traits { + return v + } + } + return nil + } + + /// Helper function to easily convert a dictionary to traits. For NFT + /// collections that do not need either of the optional values of a Trait, + /// this method should suffice to give them an array of valid traits. + /// + /// @param dict: The dictionary to be converted to Traits + /// @param excludedNames: An optional String array specifying the `dict` + /// keys that are not wanted to become `Traits` + /// @return The generated Traits view + /// + pub fun dictToTraits(dict: {String: AnyStruct}, excludedNames: [String]?): Traits { + // Collection owners might not want all the fields in their metadata included. + // They might want to handle some specially, or they might just not want them included at all. + if excludedNames != nil { + for k in excludedNames! { + dict.remove(key: k) + } + } + + let traits: [Trait] = [] + for k in dict.keys { + let trait = Trait(name: k, value: dict[k]!, displayType: nil, rarity: nil) + traits.append(trait) + } + + return Traits(traits) + } + +} diff --git a/contracts/standards/NonFungibleToken.cdc b/contracts/standards/NonFungibleToken.cdc new file mode 100644 index 0000000..5ebd8fc --- /dev/null +++ b/contracts/standards/NonFungibleToken.cdc @@ -0,0 +1,202 @@ +/** + +## The Flow Non-Fungible Token standard + +## `NonFungibleToken` contract interface + +The interface that all Non-Fungible Token contracts could conform to. +If a user wants to deploy a new NFT contract, their contract would need +to implement the NonFungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## `NFT` resource + +The core resource type that represents an NFT in the smart contract. + +## `Collection` Resource + +The resource that stores a user's NFT collection. +It includes a few functions to allow the owner to easily +move tokens in and out of the collection. + +## `Provider` and `Receiver` resource interfaces + +These interfaces declare functions with some pre and post conditions +that require the Collection to follow certain naming and behavior standards. + +They are separate because it gives the user the ability to share a reference +to their Collection that only exposes the fields and functions in one or more +of the interfaces. It also gives users the ability to make custom resources +that implement these interfaces to do various things with the tokens. + +By using resources and interfaces, users of NFT smart contracts can send +and receive tokens peer-to-peer, without having to interact with a central ledger +smart contract. + +To send an NFT to another user, a user would simply withdraw the NFT +from their Collection, then call the deposit function on another user's +Collection to complete the transfer. + +*/ + +/// The main NFT contract interface. Other NFT contracts will +/// import and implement this interface +/// +pub contract interface NonFungibleToken { + + /// The total number of tokens of this type in existence + pub var totalSupply: UInt64 + + /// Event that emitted when the NFT contract is initialized + /// + pub event ContractInitialized() + + /// Event that is emitted when a token is withdrawn, + /// indicating the owner of the collection that it was withdrawn from. + /// + /// If the collection is not in an account's storage, `from` will be `nil`. + /// + pub event Withdraw(id: UInt64, from: Address?) + + /// Event that emitted when a token is deposited to a collection. + /// + /// It indicates the owner of the collection that it was deposited to. + /// + pub event Deposit(id: UInt64, to: Address?) + + /// Interface that the NFTs have to conform to + /// The metadata views methods are included here temporarily + /// because enforcing the metadata interfaces in the standard + /// would break many contracts in an upgrade. Those breaking changes + /// are being saved for the stable cadence milestone + /// + pub resource interface INFT { + /// The unique ID that each NFT has + pub let id: UInt64 + + /// Function that returns all the Metadata Views implemented by a Non Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type] { + return [] + } + + /// Function that resolves a metadata view for this token. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// Requirement that all conforming NFT smart contracts have + /// to define a resource called NFT that conforms to INFT + /// + pub resource NFT: INFT { + pub let id: UInt64 + } + + /// Interface to mediate withdraws from the Collection + /// + pub resource interface Provider { + /// Removes an NFT from the resource implementing it and moves it to the caller + /// + /// @param withdrawID: The ID of the NFT that will be removed + /// @return The NFT resource removed from the implementing resource + /// + pub fun withdraw(withdrawID: UInt64): @NFT { + post { + result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" + } + } + } + + /// Interface to mediate deposits to the Collection + /// + pub resource interface Receiver { + + /// Adds an NFT to the resource implementing it + /// + /// @param token: The NFT resource that will be deposited + /// + pub fun deposit(token: @NFT) + } + + /// Interface that an account would commonly + /// publish for their collection + /// + pub resource interface CollectionPublic { + pub fun deposit(token: @NFT) + pub fun getIDs(): [UInt64] + pub fun borrowNFT(id: UInt64): &NFT + /// Safe way to borrow a reference to an NFT that does not panic + /// + /// @param id: The ID of the NFT that want to be borrowed + /// @return An optional reference to the desired NFT, will be nil if the passed id does not exist + /// + pub fun borrowNFTSafe(id: UInt64): &NFT? { + post { + result == nil || result!.id == id: "The returned reference's ID does not match the requested ID" + } + return nil + } + } + + /// Requirement for the concrete resource type + /// to be declared in the implementing contract + /// + pub resource Collection: Provider, Receiver, CollectionPublic { + + /// Dictionary to hold the NFTs in the Collection + pub var ownedNFTs: @{UInt64: NFT} + + /// Removes an NFT from the collection and moves it to the caller + /// + /// @param withdrawID: The ID of the NFT that will be withdrawn + /// @return The resource containing the desired NFT + /// + pub fun withdraw(withdrawID: UInt64): @NFT + + /// Takes a NFT and adds it to the collections dictionary + /// and adds the ID to the ID array + /// + /// @param token: An NFT resource + /// + pub fun deposit(token: @NFT) + + /// Returns an array of the IDs that are in the collection + /// + /// @return An array containing all the IDs on the collection + /// + pub fun getIDs(): [UInt64] + + /// Returns a borrowed reference to an NFT in the collection + /// so that the caller can read data and call methods from it + /// + /// @param id: The ID of the NFT that want to be borrowed + /// @return A reference to the NFT + /// + pub fun borrowNFT(id: UInt64): &NFT { + pre { + self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" + } + } + } + + /// Creates an empty Collection and returns it to the caller so that they can own NFTs + /// + /// @return A new Collection resource + /// + pub fun createEmptyCollection(): @Collection { + post { + result.getIDs().length == 0: "The created collection must be empty!" + } + } +} + \ No newline at end of file diff --git a/flow.json b/flow.json index 4393a61..00a852c 100644 --- a/flow.json +++ b/flow.json @@ -1,23 +1,23 @@ { "contracts": { "A": { - "source": "./contracts/A.cdc", + "source": "./contracts/example/A.cdc", "aliases": { - "emulator": "045a1763c93006ca", + "emulator": "179b6b1cb6755e31", "testing": "0000000000000009" } }, "B": { - "source": "./contracts/B.cdc", + "source": "./contracts/example/B.cdc", "aliases": { - "emulator": "120e725050340cab", + "emulator": "f3fcd2c1a78f5eee", "testing": "0000000000000010" } }, "C": { - "source": "./contracts/C.cdc", + "source": "./contracts/example/C.cdc", "aliases": { - "emulator": "120e725050340cab", + "emulator": "f3fcd2c1a78f5eee", "testing": "0000000000000010" } }, @@ -29,11 +29,35 @@ } }, "Foo": { - "source": "./contracts/Foo.cdc", + "source": "./contracts/example/Foo.cdc", "aliases": { - "emulator": "e03daebed8ca0615", + "emulator": "01cf0e2f2f715450", "testing": "0000000000000008" } + }, + "FungibleToken": { + "source": "./contracts/standards/FungibleToken.cdc", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "testnet": "9a0766d93b6608b7", + "mainnet": "f233dcee88fe0abe" + } + }, + "MetadataViews": { + "source": "./contracts/standards/MetadataViews.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "631e88ae7f1d7c20", + "mainnet": "1d7e57aa55817448" + } + }, + "NonFungibleToken": { + "source": "./contracts/standards/NonFungibleToken.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "631e88ae7f1d7c20", + "mainnet": "1d7e57aa55817448" + } } }, "networks": { @@ -45,23 +69,27 @@ }, "accounts": { "a-account": { - "address": "045a1763c93006ca", + "address": "179b6b1cb6755e31", "key": "1bbaf3239cfd9e8e35f85723f6c70f2ac5c8f50856c4667021cf9ed72eabd9f8" }, "abc-updater": { - "address": "f669cb8d41ce0c74", + "address": "e03daebed8ca0615", "key": "caa4da634fee3ad45ce67ef8a6813987888d88f4b4e6e70b8d84685845db7f25" }, "bc-account": { - "address": "120e725050340cab", + "address": "f3fcd2c1a78f5eee", "key": "c06a4b0fce3bc3088a2a2e3b11a9ea5d13e251661cefa3f26af1180ad317d3dc" }, "emulator-account": { "address": "f8d6e0586b0a20c7", "key": "a08c990a1f7adb14c290d05df0f397d2de2f4d0cb18cdffed592f611f95f5d08" }, + "emulator-ft": { + "address": "ee82856bf20e2aa6", + "key": "686779d775e5fcbf8d2f4a85cb4c53525d02b7ef53230d180fc16f35d9b7d025" + }, "foo": { - "address": "e03daebed8ca0615", + "address": "01cf0e2f2f715450", "key": "e9b7b36e9d16f47501db73e84c68e441609475ee482ee808411b2fe0bd2329da" } }, @@ -77,6 +105,9 @@ "emulator-account": [ "StagedContractUpdates" ], + "emulator-ft": [ + "FungibleToken" + ], "foo": [ "Foo" ] diff --git a/setup.sh b/setup.sh index 2aba1ea..914c3a0 100644 --- a/setup.sh +++ b/setup.sh @@ -1,15 +1,15 @@ #!/bin/bash -# Create `foo` account e03daebed8ca0615 with private key e9b7b36e9d16f47501db73e84c68e441609475ee482ee808411b2fe0bd2329da +# Create `foo` account 01cf0e2f2f715450 with private key e9b7b36e9d16f47501db73e84c68e441609475ee482ee808411b2fe0bd2329da flow accounts create --key "3bfabba47056a7ce4d1fe258f2dbf824c27e36dc1118335aaab5ba6362bf2f7d23eb9662ccd4d82d732966bd7dc4730da6e3c7ca0b72dd827be8d62271c5ae1b" -# Create `a-account` account 045a1763c93006ca with private key 1bbaf3239cfd9e8e35f85723f6c70f2ac5c8f50856c4667021cf9ed72eabd9f8 +# Create `a-account` account 179b6b1cb6755e31 with private key 1bbaf3239cfd9e8e35f85723f6c70f2ac5c8f50856c4667021cf9ed72eabd9f8 flow accounts create --key "e4d80c05460c9c7e766be0491060f1fc82858cd27c37ef7680a8f75faea89f3f6860735b9e7dce49e2ad97e512e94389c69b942ae0c888053660588588eb7571" -# Create `bc-account` account 120e725050340cab with private key c06a4b0fce3bc3088a2a2e3b11a9ea5d13e251661cefa3f26af1180ad317d3dc +# Create `bc-account` account f3fcd2c1a78f5eee with private key c06a4b0fce3bc3088a2a2e3b11a9ea5d13e251661cefa3f26af1180ad317d3dc flow accounts create --key "93298486140d00b22e2aee56c2bfc8cd5e4ae97fce7a7c0884916bc4561079eab979f6450a12a43eb426c21a19a8584c06af5e32cc2540a4a609dc1bd8f6252d" -# Create `abc-updater` account f669cb8d41ce0c74 with private key caa4da634fee3ad45ce67ef8a6813987888d88f4b4e6e70b8d84685845db7f25 +# Create `abc-updater` account e03daebed8ca0615 with private key caa4da634fee3ad45ce67ef8a6813987888d88f4b4e6e70b8d84685845db7f25 flow accounts create --key "3325eb2ce37b2a8eb45a7262645d5360320f50746c907e53fdcfef88520e7d141c78c677f983e5c3a86b8a19ecc6dca35f9d2b84f3b1ecae84837ed0f8f1c69d" # Deploy contracts From 4e8fa2b18a46297dd6993cbc38da5b82ba4e5fbb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:35:29 -0600 Subject: [PATCH 19/34] restructure scripts/ & transactions/ & match .ViewResolver interface impl --- normalize_coverage_report.sh | 8 +- ...get_block_update_boundary_from_updater.cdc | 10 +++ scripts/get_contract_delegatee_address.cdc | 5 ++ ...dc => get_updater_deployment_readable.cdc} | 0 scripts/get_updater_info.cdc | 51 +++---------- scripts/{ => test}/foo.cdc | 0 scripts/{ => test}/get_block_height.cdc | 0 tests/staged_contract_updater_tests.cdc | 75 +++++++++++++++++-- tick_tock.sh | 2 +- transactions/setup_updater_multi_account.cdc | 7 +- ...up_updater_single_account_and_contract.cdc | 4 +- .../setup_updater_with_empty_deployment.cdc | 15 ++++ transactions/{ => test}/tick_tock.cdc | 0 13 files changed, 120 insertions(+), 57 deletions(-) create mode 100644 scripts/get_block_update_boundary_from_updater.cdc create mode 100644 scripts/get_contract_delegatee_address.cdc rename scripts/{get_updater_deployment.cdc => get_updater_deployment_readable.cdc} (100%) rename scripts/{ => test}/foo.cdc (100%) rename scripts/{ => test}/get_block_height.cdc (100%) create mode 100644 transactions/test/setup_updater_with_empty_deployment.cdc rename transactions/{ => test}/tick_tock.cdc (100%) diff --git a/normalize_coverage_report.sh b/normalize_coverage_report.sh index aa07498..6dcbee8 100755 --- a/normalize_coverage_report.sh +++ b/normalize_coverage_report.sh @@ -1,5 +1,5 @@ sed -i 's/A.0000000000000007.StagedContractUpdates/contracts\/StagedContractUpdates.cdc/' coverage.lcov -sed -i 's/A.0000000000000008.Foo/contracts\/Foo.cdc/' coverage.lcov -sed -i 's/A.0000000000000009.A/contracts\/A.cdc/' coverage.lcov -sed -i 's/A.0000000000000010.B/contracts\/B.cdc/' coverage.lcov -sed -i 's/A.0000000000000010.C/contracts\/C.cdc/' coverage.lcov \ No newline at end of file +sed -i 's/A.0000000000000008.Foo/contracts\/example\/Foo.cdc/' coverage.lcov +sed -i 's/A.0000000000000009.A/contracts\/example\/A.cdc/' coverage.lcov +sed -i 's/A.0000000000000010.B/contracts\/example\/B.cdc/' coverage.lcov +sed -i 's/A.0000000000000010.C/contracts\/example\/C.cdc/' coverage.lcov \ No newline at end of file diff --git a/scripts/get_block_update_boundary_from_updater.cdc b/scripts/get_block_update_boundary_from_updater.cdc new file mode 100644 index 0000000..07d86b5 --- /dev/null +++ b/scripts/get_block_update_boundary_from_updater.cdc @@ -0,0 +1,10 @@ +import "StagedContractUpdates" + +/// Retrieves the block height update boundary from the Updater at the given Address or nil if an Updater is not found +/// +access(all) fun main(updaterAddress: Address): UInt64? { + return getAccount(updaterAddress).getCapability<&{StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.UpdaterPublicPath + ).borrow() + ?.getBlockUpdateBoundary() +} diff --git a/scripts/get_contract_delegatee_address.cdc b/scripts/get_contract_delegatee_address.cdc new file mode 100644 index 0000000..c060e7b --- /dev/null +++ b/scripts/get_contract_delegatee_address.cdc @@ -0,0 +1,5 @@ +import "StagedContractUpdates" + +access(all) fun main(): Address { + return StagedContractUpdates.getContractDelegateeAddress() +} \ No newline at end of file diff --git a/scripts/get_updater_deployment.cdc b/scripts/get_updater_deployment_readable.cdc similarity index 100% rename from scripts/get_updater_deployment.cdc rename to scripts/get_updater_deployment_readable.cdc diff --git a/scripts/get_updater_info.cdc b/scripts/get_updater_info.cdc index c606d96..1b924fe 100644 --- a/scripts/get_updater_info.cdc +++ b/scripts/get_updater_info.cdc @@ -1,46 +1,13 @@ -import "StagedContractUpdates" - -pub struct ContractUpdateReadable { - pub let name: String - pub let code: String +import "MetadataViews" - init( - name: String, - code: String - ) { - self.name = name - self.code = code - } -} +import "StagedContractUpdates" -/// Returns values of the Updater at the given Address +/// Returns UpdaterInfo view from the Updater at the given address or nil if none is found /// -pub fun main(address: Address): {Int: {Address: [ContractUpdateReadable]}}? { - let account = getAuthAccount(address) - - if let updater = account.borrow<&StagedContractUpdates.Updater>(from: StagedContractUpdates.UpdaterStoragePath) { - let result: {Int: {Address: [ContractUpdateReadable]}} = {} - let deployments = updater.getDeployments() - - for i, stage in deployments { - let data: {Address: [ContractUpdateReadable]} = {} - for contractUpdate in stage { - if !data.containsKey(contractUpdate.address) { - data.insert(key: contractUpdate.address, []) - } - data[contractUpdate.address]!.append( - ContractUpdateReadable( - name: contractUpdate.name, - code: contractUpdate.codeAsCadence() - ) - ) - } - result.insert(key: i, data) - } - - return result - } - - return nil - +pub fun main(address: Address): StagedContractUpdates.UpdaterInfo? { + return getAccount(address).getCapability<&{StagedContractUpdates.UpdaterPublic, MetadataViews.Resolver}>( + StagedContractUpdates.UpdaterPublicPath + ) + .borrow() + ?.resolveView(Type()) as! StagedContractUpdates.UpdaterInfo? } diff --git a/scripts/foo.cdc b/scripts/test/foo.cdc similarity index 100% rename from scripts/foo.cdc rename to scripts/test/foo.cdc diff --git a/scripts/get_block_height.cdc b/scripts/test/get_block_height.cdc similarity index 100% rename from scripts/get_block_height.cdc rename to scripts/test/get_block_height.cdc diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 83a572a..5d37991 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -1,6 +1,7 @@ import Test import BlockchainHelpers +// NOTE: This is an artifact of the implicit Test API - it's not clear how block height transitions between test cases access(all) let blockHeightBoundaryDelay: UInt64 = 15 // Contract hosts as defined in flow.json @@ -28,39 +29,56 @@ access(all) fun setup() { err = Test.deployContract( name: "Foo", - path: "../contracts/Foo.cdc", + path: "../contracts/example/Foo.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "A", - path: "../contracts/A.cdc", + path: "../contracts/example/A.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "B", - path: "../contracts/B.cdc", + path: "../contracts/example/B.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "C", - path: "../contracts/C.cdc", + path: "../contracts/example/C.cdc", arguments: [] ) Test.expect(err, Test.beNil()) } +access(all) fun testDeploymentAddressMatchesDelegateeAddress() { + let expectedAddress = admin.address + let actualAddress = executeScript("../scripts/get_contract_delegatee_address.cdc", []).returnValue as! Address? + ?? panic("Problem retrieving deployment address") + Test.assertEqual(expectedAddress, actualAddress) +} + +access(all) fun testEmptyDeploymentUpdaterInitFails() { + let alice = Test.createAccount() + let txResult = executeTransaction( + "../transactions/test/setup_updater_with_empty_deployment.cdc", + [], + alice + ) + Test.expect(txResult, Test.beFailed()) +} + access(all) fun testSetupSingleContractSingleHostSelfUpdate() { let expectedPreUpdateResult: String = "foo" // Validate the pre-update value of Foo.foo() - let actualPreUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? + let actualPreUpdateResult = executeScript("../scripts/test/foo.cdc", []).returnValue as! String? ?? panic("Problem retrieving result of Foo.foo()") Test.assertEqual(expectedPreUpdateResult, actualPreUpdateResult) @@ -105,12 +123,17 @@ access(all) fun testExecuteUpdateFailsBeforeBoundary() { Test.assertEqual(0, stagePost) } +// TODO +// access(all) fun testDelegatedUpdate() { +// /* TODO */ +// } + access(all) fun testExecuteUpdateSucceedsAfterBoundary() { let expectedPostUpdateResult: String = "bar" // Mock block advancement - tickTock(advanceBlocks: blockHeightBoundaryDelay, fooAccount) + jumpToUpdateBoundary(forUpdater: fooAccount.address) // Validate the current deployment stage is still 0 let stagePrior = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? @@ -141,11 +164,20 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { // Test.assertEqual(1, events.length) // Validate the post-update value of Foo.foo() - let actualPostUpdateResult = executeScript("../scripts/foo.cdc", []).returnValue as! String? + let actualPostUpdateResult = executeScript("../scripts/test/foo.cdc", []).returnValue as! String? ?? panic("Problem retrieving result of Foo.foo()") Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) } +access(all) fun testDelegationOfCompletedUpdaterFails() { + let txResult = executeTransaction( + "../transactions/delegate.cdc", + [], + fooAccount + ) + Test.expect(txResult, Test.beFailed()) +} + access(all) fun testSetupMultiContractMultiAccountUpdater() { let contractAddresses: [Address] = [aAccount.address, bcAccount.address] @@ -200,13 +232,40 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { Test.assertEqual(0, currentStage) } +access(all) fun testUpdateMultiContractMultiAccountUpdater() { + // Validate the current deployment stage is still 0 + let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) + + +} + /* --- TEST HELPERS --- */ +access(all) fun jumpToUpdateBoundary(forUpdater: Address) { + // Identify current block height in test environment + var currentHeight = executeScript("../scripts/test/get_block_height.cdc", []).returnValue as! UInt64? + ?? panic("Problem retrieving current block height") + // Identify number of blocks to advance + let updateBoundary = executeScript( + "../scripts/get_block_update_boundary_from_updater.cdc", + [forUpdater] + ).returnValue as! UInt64? + ?? panic("Problem retrieving updater height boundary") + // Return if no advancement needed + if updateBoundary <= currentHeight { + return + } + // Otherwise jump to update boundary + tickTock(advanceBlocks: updateBoundary - currentHeight, admin) +} + access(all) fun tickTock(advanceBlocks: UInt64, _ signer: Test.Account) { var blocksAdvanced: UInt64 = 0 while blocksAdvanced < advanceBlocks { - let txResult = executeTransaction("../transactions/tick_tock.cdc", [], signer) + let txResult = executeTransaction("../transactions/test/tick_tock.cdc", [], signer) Test.expect(txResult, Test.beSucceeded()) blocksAdvanced = blocksAdvanced + 1 diff --git a/tick_tock.sh b/tick_tock.sh index 44016bb..2d2977b 100644 --- a/tick_tock.sh +++ b/tick_tock.sh @@ -1,4 +1,4 @@ #!/bin/bash # Run this transaction several times to increment the block height -flow transactions send ./transactions/tick_tock.cdc \ No newline at end of file +flow transactions send ./transactions/test/tick_tock.cdc \ No newline at end of file diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/setup_updater_multi_account.cdc index a50084e..da53095 100644 --- a/transactions/setup_updater_multi_account.cdc +++ b/transactions/setup_updater_multi_account.cdc @@ -1,5 +1,7 @@ #allowAccountLinking +import "MetadataViews" + import "StagedContractUpdates" /// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos an @@ -47,6 +49,9 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen to: StagedContractUpdates.UpdaterStoragePath ) signer.unlink(StagedContractUpdates.UpdaterPublicPath) - signer.link<&StagedContractUpdates.Updater{StagedContractUpdates.UpdaterPublic}>(StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath) + signer.link<&{StagedContractUpdates.UpdaterPublic, MetadataViews.Resolver}>( + StagedContractUpdates.UpdaterPublicPath, + target: StagedContractUpdates.UpdaterStoragePath + ) } } diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/setup_updater_single_account_and_contract.cdc index 7a5219e..51d9c75 100644 --- a/transactions/setup_updater_single_account_and_contract.cdc +++ b/transactions/setup_updater_single_account_and_contract.cdc @@ -1,5 +1,7 @@ #allowAccountLinking +import "MetadataViews" + import "StagedContractUpdates" /// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos a @@ -55,7 +57,7 @@ transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { to: StagedContractUpdates.UpdaterStoragePath ) signer.unlink(StagedContractUpdates.UpdaterPublicPath) - signer.link<&{StagedContractUpdates.UpdaterPublic}>( + signer.link<&{StagedContractUpdates.UpdaterPublic, MetadataViews.Resolver}>( StagedContractUpdates.UpdaterPublicPath, target: StagedContractUpdates.UpdaterStoragePath ) diff --git a/transactions/test/setup_updater_with_empty_deployment.cdc b/transactions/test/setup_updater_with_empty_deployment.cdc new file mode 100644 index 0000000..fd04118 --- /dev/null +++ b/transactions/test/setup_updater_with_empty_deployment.cdc @@ -0,0 +1,15 @@ +import "StagedContractUpdates" + +/// TEST TRANSACTION +/// Should fail on Updater.init() due to empty hosts and deployments arrays +/// +transaction { + prepare(signer: AuthAccount) { + let updater <- StagedContractUpdates.createNewUpdater( + blockUpdateBoundary: getCurrentBlock().height, + hosts: [], + deployments: [] + ) + signer.save(<-updater, to: StagedContractUpdates.UpdaterStoragePath) + } +} diff --git a/transactions/tick_tock.cdc b/transactions/test/tick_tock.cdc similarity index 100% rename from transactions/tick_tock.cdc rename to transactions/test/tick_tock.cdc From 04ec42933769704d7dbc9d2a0de6482e75f7fd82 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:35:43 -0600 Subject: [PATCH 20/34] update Updater.resolveView impl --- contracts/StagedContractUpdates.cdc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index c696952..7c7fe0d 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -301,12 +301,12 @@ access(all) contract StagedContractUpdates { if view == Type() { return UpdaterInfo( id: self.uuid, - blockUpdateBoundary: self.blockUpdateBoundary, - updateComplete: self.updateComplete, - hostAddresses: self.hosts.keys, - deployments: self.deployments, - currentDeploymentStage: self.currentDeploymentStage, - failedDeployments: self.failedDeployments + blockUpdateBoundary: self.getBlockUpdateBoundary(), + updateComplete: self.hasBeenUpdated(), + hostAddresses: self.getContractAccountAddresses(), + deployments: self.getDeployments(), + currentDeploymentStage: self.getCurrentDeploymentStage(), + failedDeployments: self.getFailedDeployments() ) } return nil From 6c68ae451b3c274e52899652c4c95c02fc2fbc6e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:00:43 -0600 Subject: [PATCH 21/34] move example contracts to test/ dir & update imports to test addresses --- contracts/example/A_update.cdc | 16 --------- contracts/example/B_update.cdc | 17 --------- contracts/example/Foo_update.cdc | 5 --- contracts/{example => test}/A.cdc | 0 contracts/test/A_update.cdc | 16 +++++++++ contracts/{example => test}/B.cdc | 0 contracts/test/B_update.cdc | 17 +++++++++ contracts/{example => test}/C.cdc | 0 contracts/{example => test}/C_update.cdc | 30 ++++++++-------- contracts/{example => test}/Foo.cdc | 0 contracts/test/Foo_update.cdc | 5 +++ flow.json | 46 ++++++++++++++---------- 12 files changed, 80 insertions(+), 72 deletions(-) delete mode 100644 contracts/example/A_update.cdc delete mode 100644 contracts/example/B_update.cdc delete mode 100644 contracts/example/Foo_update.cdc rename contracts/{example => test}/A.cdc (100%) create mode 100644 contracts/test/A_update.cdc rename contracts/{example => test}/B.cdc (100%) create mode 100644 contracts/test/B_update.cdc rename contracts/{example => test}/C.cdc (100%) rename contracts/{example => test}/C_update.cdc (55%) rename contracts/{example => test}/Foo.cdc (100%) create mode 100644 contracts/test/Foo_update.cdc diff --git a/contracts/example/A_update.cdc b/contracts/example/A_update.cdc deleted file mode 100644 index 71b37ee..0000000 --- a/contracts/example/A_update.cdc +++ /dev/null @@ -1,16 +0,0 @@ -pub contract interface A { - - pub resource interface I { - pub fun foo(): String - pub fun bar(): String - } - - pub resource R : I { - pub fun foo(): String { - return "foo" - } - pub fun bar(): String { - return "bar" - } - } -} \ No newline at end of file diff --git a/contracts/example/B_update.cdc b/contracts/example/B_update.cdc deleted file mode 100644 index 74e13f3..0000000 --- a/contracts/example/B_update.cdc +++ /dev/null @@ -1,17 +0,0 @@ -import A from 0x045a1763c93006ca - -pub contract B : A { - - pub resource R : A.I { - pub fun foo(): String { - return "foo" - } - pub fun bar(): String { - return "bar" - } - } - - pub fun createR(): @R { - return <-create R() - } -} \ No newline at end of file diff --git a/contracts/example/Foo_update.cdc b/contracts/example/Foo_update.cdc deleted file mode 100644 index d1e69bb..0000000 --- a/contracts/example/Foo_update.cdc +++ /dev/null @@ -1,5 +0,0 @@ -pub contract Foo { - pub fun foo(): String { - return "bar" - } -} diff --git a/contracts/example/A.cdc b/contracts/test/A.cdc similarity index 100% rename from contracts/example/A.cdc rename to contracts/test/A.cdc diff --git a/contracts/test/A_update.cdc b/contracts/test/A_update.cdc new file mode 100644 index 0000000..44da41d --- /dev/null +++ b/contracts/test/A_update.cdc @@ -0,0 +1,16 @@ +access(all) contract interface A { + + access(all) resource interface I { + access(all) fun foo(): String + access(all) fun bar(): String + } + + access(all) resource R : I { + access(all) fun foo(): String { + return "foo" + } + access(all) fun bar(): String { + return "bar" + } + } +} \ No newline at end of file diff --git a/contracts/example/B.cdc b/contracts/test/B.cdc similarity index 100% rename from contracts/example/B.cdc rename to contracts/test/B.cdc diff --git a/contracts/test/B_update.cdc b/contracts/test/B_update.cdc new file mode 100644 index 0000000..3cb6a4e --- /dev/null +++ b/contracts/test/B_update.cdc @@ -0,0 +1,17 @@ +import A from 0x0000000000000009 + +access(all) contract B : A { + + access(all) resource R : A.I { + access(all) fun foo(): String { + return "foo" + } + access(all) fun bar(): String { + return "bar" + } + } + + access(all) fun createR(): @R { + return <-create R() + } +} \ No newline at end of file diff --git a/contracts/example/C.cdc b/contracts/test/C.cdc similarity index 100% rename from contracts/example/C.cdc rename to contracts/test/C.cdc diff --git a/contracts/example/C_update.cdc b/contracts/test/C_update.cdc similarity index 55% rename from contracts/example/C_update.cdc rename to contracts/test/C_update.cdc index b0361d9..d181917 100644 --- a/contracts/example/C_update.cdc +++ b/contracts/test/C_update.cdc @@ -1,40 +1,40 @@ -import A from 0x045a1763c93006ca -import B from 0x120e725050340cab +import A from 0x0000000000000009 +import B from 0x0000000000000010 -pub contract C { +access(all) contract C { - pub let StoragePath: StoragePath - pub let PublicPath: PublicPath + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath - pub resource interface OuterPublic { - pub fun getFooFrom(id: UInt64): String - pub fun getBarFrom(id: UInt64): String + access(all) resource interface OuterPublic { + access(all) fun getFooFrom(id: UInt64): String + access(all) fun getBarFrom(id: UInt64): String } - pub resource Outer : OuterPublic { - pub let inner: @{UInt64: A.R} + access(all) resource Outer : OuterPublic { + access(all) let inner: @{UInt64: A.R} init() { self.inner <- {} } - pub fun getFooFrom(id: UInt64): String { + access(all) fun getFooFrom(id: UInt64): String { return self.borrowResource(id)?.foo() ?? panic("No resource found with given ID") } - pub fun getBarFrom(id: UInt64): String { + access(all) fun getBarFrom(id: UInt64): String { return self.borrowResource(id)?.bar() ?? panic("No resource found with given ID") } - pub fun addResource(_ i: @A.R) { + access(all) fun addResource(_ i: @A.R) { self.inner[i.uuid] <-! i } - pub fun borrowResource(_ id: UInt64): &{A.I}? { + access(all) fun borrowResource(_ id: UInt64): &{A.I}? { return &self.inner[id] as &{A.I}? } - pub fun removeResource(_ id: UInt64): @A.R? { + access(all) fun removeResource(_ id: UInt64): @A.R? { return <- self.inner.remove(key: id) } diff --git a/contracts/example/Foo.cdc b/contracts/test/Foo.cdc similarity index 100% rename from contracts/example/Foo.cdc rename to contracts/test/Foo.cdc diff --git a/contracts/test/Foo_update.cdc b/contracts/test/Foo_update.cdc new file mode 100644 index 0000000..3fbb49d --- /dev/null +++ b/contracts/test/Foo_update.cdc @@ -0,0 +1,5 @@ +access(all) contract Foo { + access(all) fun foo(): String { + return "bar" + } +} diff --git a/flow.json b/flow.json index 00a852c..d7bb2ad 100644 --- a/flow.json +++ b/flow.json @@ -1,35 +1,28 @@ { "contracts": { "A": { - "source": "./contracts/example/A.cdc", + "source": "./contracts/test/A.cdc", "aliases": { "emulator": "179b6b1cb6755e31", "testing": "0000000000000009" } }, "B": { - "source": "./contracts/example/B.cdc", + "source": "./contracts/test/B.cdc", "aliases": { "emulator": "f3fcd2c1a78f5eee", "testing": "0000000000000010" } }, "C": { - "source": "./contracts/example/C.cdc", + "source": "./contracts/test/C.cdc", "aliases": { "emulator": "f3fcd2c1a78f5eee", "testing": "0000000000000010" } }, - "StagedContractUpdates": { - "source": "./contracts/StagedContractUpdates.cdc", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "testing": "0000000000000007" - } - }, "Foo": { - "source": "./contracts/example/Foo.cdc", + "source": "./contracts/test/Foo.cdc", "aliases": { "emulator": "01cf0e2f2f715450", "testing": "0000000000000008" @@ -39,32 +32,39 @@ "source": "./contracts/standards/FungibleToken.cdc", "aliases": { "emulator": "ee82856bf20e2aa6", - "testnet": "9a0766d93b6608b7", - "mainnet": "f233dcee88fe0abe" + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" } }, "MetadataViews": { "source": "./contracts/standards/MetadataViews.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", - "testnet": "631e88ae7f1d7c20", - "mainnet": "1d7e57aa55817448" + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" } }, "NonFungibleToken": { "source": "./contracts/standards/NonFungibleToken.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", - "testnet": "631e88ae7f1d7c20", - "mainnet": "1d7e57aa55817448" + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "StagedContractUpdates": { + "source": "./contracts/StagedContractUpdates.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" } } }, "networks": { "emulator": "127.0.0.1:3569", - "testing": "127.0.0.1:3569", "mainnet": "access.mainnet.nodes.onflow.org:9000", "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", "testnet": "access.devnet.nodes.onflow.org:9000" }, "accounts": { @@ -103,7 +103,15 @@ "C" ], "emulator-account": [ - "StagedContractUpdates" + { + "name": "StagedContractUpdates", + "args": [ + { + "type": "UInt64", + "value": "10" + } + ] + } ], "emulator-ft": [ "FungibleToken" From 1a25d90a8e281c5ab2b64aeb43f481832a4c92ef Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:01:39 -0600 Subject: [PATCH 22/34] update StagedContractUpdates blockUpdateBoundary handling & scripts + tests --- contracts/StagedContractUpdates.cdc | 99 ++++++++++++------- scripts/get_contract_delegatee_address.cdc | 5 - scripts/test/get_block_height.cdc | 3 - tests/staged_contract_updater_tests.cdc | 35 +++---- transactions/delegate.cdc | 15 ++- ....cdc => execute_all_delegated_updates.cdc} | 0 .../remove_from_delegatee_as_updater.cdc | 20 ++-- transactions/setup_updater_multi_account.cdc | 4 +- ...up_updater_single_account_and_contract.cdc | 6 +- .../setup_updater_with_empty_deployment.cdc | 2 +- 10 files changed, 98 insertions(+), 91 deletions(-) delete mode 100644 scripts/get_contract_delegatee_address.cdc delete mode 100644 scripts/test/get_block_height.cdc rename transactions/{execute_delegated_updates.cdc => execute_all_delegated_updates.cdc} (100%) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 7c7fe0d..4044ccf 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -15,13 +15,15 @@ import "MetadataViews" /// single update() call per transaction. /// See the following issue for more info: https://github.com/onflow/cadence/issues/2700 /// -// TODO: Consider how to handle large contracts that exceed the transaction limit -// - It's common to chunk contract code and pass over numerous transactions - think about how could support a similar workflow -// when configuring an Updater resource access(all) contract StagedContractUpdates { + /// Common inbox name prefix for Host Capabilities access(all) let inboxHostCapabilityNamePrefix: String + /// Common update boundary for those coordinating with contract account-managed Delegatee, enabling opt-in + /// coordinated contract updates + access(all) var blockUpdateBoundary: UInt64 + /* --- Canonical Paths --- */ // access(all) let HostStoragePath: StoragePath @@ -35,6 +37,7 @@ access(all) contract StagedContractUpdates { /* --- Events --- */ // + access(all) event ContractBlockUpdateBoundaryUpdated(old: UInt64?, new: UInt64) access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) access(all) event UpdaterUpdated( updaterUUID: UInt64, @@ -158,7 +161,6 @@ access(all) contract StagedContractUpdates { /// access(all) resource Updater : UpdaterPublic, MetadataViews.Resolver { /// Update to occur at or beyond this block height - // TODO: Consider making this a contract-owned value as it's reflective of the spork height access(self) let blockUpdateBoundary: UInt64 /// Update status defining whether all update stages have been *attempted* /// NOTE: `true` does not necessarily mean all updates were successful @@ -208,14 +210,13 @@ access(all) contract StagedContractUpdates { /// Executes the next update stage for all contracts defined in deployment, returning true if all stages have /// been attempted and false if stages remain /// - access(all) fun update(): Bool { + access(all) fun update(): Bool? { // Return early if we've already updated if self.updateComplete { return true } else if getCurrentBlock().height < self.blockUpdateBoundary { - // TODO: Consider returning nil here - indicates an update isn't even attempted. - // Delegatee could then pop on nil since this Updater won't update at the attempted height anyway - return false + // Return nil to indicate we're not yet at the update boundary + return nil } let updatedAddresses: [Address] = [] @@ -327,14 +328,15 @@ access(all) contract StagedContractUpdates { /// Resource that executed delegated updates /// access(all) resource Delegatee : DelegateePublic { - // TODO: Block Height - All DelegatedUpdaters must be updated at or beyond this block height - // access(self) let blockUpdateBoundary: UInt64 + /// Block height at which delegated updates will be performed + /// NOTE: This may differ from the contract's blockUpdateBoundary, enabling flexibility + /// but any Updater not ready when updates are performed will be revoked from the Delegatee + access(self) let blockUpdateBoundary: UInt64 /// Track all delegated updaters - // TODO: If we support staged updates, we'll want visibility into the number of stages and progress through all - // maybe removing after stages have been complete or failed access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} - init() { + init(blockUpdateBoundary: UInt64) { + self.blockUpdateBoundary = blockUpdateBoundary self.delegatedUpdaters = {} } @@ -356,8 +358,11 @@ access(all) contract StagedContractUpdates { pre { updaterCap.check(): "Invalid DelegatedUpdater Capability!" updaterCap.borrow()!.hasBeenUpdated() == false: "Updater has already been updated!" + updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary: + "Updater will not be ready for updates at Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) } - let updater = updaterCap.borrow()! + + let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! if self.delegatedUpdaters.containsKey(updater.getID()) { // Upsert if updater already exists self.delegatedUpdaters[updater.getID()] = updaterCap @@ -375,35 +380,41 @@ access(all) contract StagedContractUpdates { updaterCap.check(): "Invalid DelegatedUpdater Capability!" self.delegatedUpdaters.containsKey(updaterCap.borrow()!.getID()): "No Updater found for ID!" } - let updater = updaterCap.borrow()! + + let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! self.removeDelegatedUpdater(id: updater.getID()) } - /// Executes update on the specified Updater + /// Executes update on the specified Updaters. All updates are attempted, and if the Updater is not yet ready + /// to be updated (updater.update() returns nil) or the attempted update is the final staged (updater.update() + /// returns true) the corresponding Updater Capability is removed. /// - // TODO: Consider removing Capabilities once we get signal that the Updater has been completed - access(all) fun update(updaterIDs: [UInt64]): [UInt64] { - let failed: [UInt64] = [] - + access(all) fun update(updaterIDs: [UInt64]) { for id in updaterIDs { + // Invalid ID - mark as purged and continue if self.delegatedUpdaters[id] == nil { - failed.append(id) continue } - let updaterCap = self.delegatedUpdaters[id]! + + // Check Capability - if invalid, remove Capability, mark as purged and continue + let updaterCap: Capability<&StagedContractUpdates.Updater> = self.delegatedUpdaters[id]! if !updaterCap.check() { - failed.append(id) + self.delegatedUpdaters.remove(key: id) continue } - let success = updaterCap.borrow()!.update() - if !success { - failed.append(id) + + // Execute currently staged update + let success: Bool? = updaterCap.borrow()!.update() + // If update is not ready or complete, remove Capability and continue + if success == nil || success! == true { + self.delegatedUpdaters.remove(key: id) + continue } } - return failed } /// Enables admin removal of a DelegatedUpdater Capability + /// access(all) fun removeDelegatedUpdater(id: UInt64) { if !self.delegatedUpdaters.containsKey(id) { return @@ -417,10 +428,12 @@ access(all) contract StagedContractUpdates { } } - /// Returns the Address of the Delegatee associated with this contract + /// Returns the Capability of the Delegatee associated with this contract /// - access(all) fun getContractDelegateeAddress(): Address { - return self.account.address + access(all) fun getContractDelegateeCapability(): Capability<&{DelegateePublic}> { + let delegateeCap = self.account.getCapability<&{DelegateePublic}>(self.DelegateePublicPath) + assert(delegateeCap.check(), message: "Invalid Delegatee Capability retrieved") + return delegateeCap } /// Helper method that returns the ordered array reflecting sequenced and staged deployments, with each contract @@ -437,7 +450,6 @@ access(all) contract StagedContractUpdates { let contractUpdates: [ContractUpdate] = [] for contractConfig in deploymentStage { - assert(contractConfig.length == 1, message: "Invalid contract config") let address = contractConfig.keys[0] assert(contractConfig[address]!.length == 1, message: "Invalid contract config") @@ -451,12 +463,10 @@ access(all) contract StagedContractUpdates { ) ) } - deployments.append( contractUpdates ) } - return deployments } @@ -480,13 +490,26 @@ access(all) contract StagedContractUpdates { /// Creates a new Delegatee resource enabling caller to self-host their Delegatee /// - access(all) fun createNewDelegatee(): @Delegatee { - return <- create Delegatee() + access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64): @Delegatee { + return <- create Delegatee(blockUpdateBoundary: blockUpdateBoundary) } - init() { + /// Allows the contract block update boundary to be set + /// + access(account) fun setBlockUpdateBoundary(new: UInt64) { + pre { + new > getCurrentBlock().height: "New boundary must be in the future!" + new > self.blockUpdateBoundary: "New block update boundary must be greater than current boundary!" + } + let old = self.blockUpdateBoundary + self.blockUpdateBoundary = new + emit ContractBlockUpdateBoundaryUpdated(old: old, new: new) + } + init(blockUpdateBoundary: UInt64) { let contractAddress = self.account.address.toString() + + self.blockUpdateBoundary = blockUpdateBoundary self.inboxHostCapabilityNamePrefix = "StagedContractUpdatesHostCapability_" self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! @@ -499,7 +522,9 @@ access(all) contract StagedContractUpdates { // self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(contractAddress))! self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(contractAddress))! - self.account.save(<-create Delegatee(), to: self.DelegateeStoragePath) + self.account.save(<-create Delegatee(blockUpdateBoundary: blockUpdateBoundary), to: self.DelegateeStoragePath) self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) + + emit ContractBlockUpdateBoundaryUpdated(old: nil, new: blockUpdateBoundary) } } diff --git a/scripts/get_contract_delegatee_address.cdc b/scripts/get_contract_delegatee_address.cdc deleted file mode 100644 index c060e7b..0000000 --- a/scripts/get_contract_delegatee_address.cdc +++ /dev/null @@ -1,5 +0,0 @@ -import "StagedContractUpdates" - -access(all) fun main(): Address { - return StagedContractUpdates.getContractDelegateeAddress() -} \ No newline at end of file diff --git a/scripts/test/get_block_height.cdc b/scripts/test/get_block_height.cdc deleted file mode 100644 index b3e79db..0000000 --- a/scripts/test/get_block_height.cdc +++ /dev/null @@ -1,3 +0,0 @@ -pub fun main(): UInt64 { - return getCurrentBlock().height -} \ No newline at end of file diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 5d37991..d9d6579 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -14,55 +14,48 @@ access(all) let bcAccount = Test.getAccount(0x0000000000000010) access(all) let abcUpdater = Test.createAccount() // Content of update contracts as hex strings -access(all) let fooUpdateCode = "70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" -access(all) let aUpdateCode = "70756220636f6e747261637420696e746572666163652041207b0a202020200a20202020707562207265736f7572636520696e746572666163652049207b0a20202020202020207075622066756e20666f6f28293a20537472696e670a20202020202020207075622066756e2062617228293a20537472696e670a202020207d0a0a20202020707562207265736f757263652052203a2049207b0a20202020202020207075622066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a20202020202020207075622066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" -access(all) let bUpdateCode = "696d706f727420412066726f6d203078303435613137363363393330303663610a0a70756220636f6e74726163742042203a2041207b0a202020200a20202020707562207265736f757263652052203a20412e49207b0a20202020202020207075622066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a20202020202020207075622066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a202020207075622066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" -access(all) let cUpdateCode = "696d706f727420412066726f6d203078303435613137363363393330303663610a696d706f727420422066726f6d203078313230653732353035303334306361620a0a70756220636f6e74726163742043207b0a0a20202020707562206c65742053746f72616765506174683a2053746f72616765506174680a20202020707562206c6574205075626c6963506174683a205075626c6963506174680a0a20202020707562207265736f7572636520696e74657266616365204f757465725075626c6963207b0a20202020202020207075622066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a20202020202020207075622066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a20202020707562207265736f75726365204f75746572203a204f757465725075626c6963207b0a2020202020202020707562206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a20202020202020207075622066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a20202020202020207075622066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a20202020202020207075622066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a20202020202020207075622066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a20202020202020207075622066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" +access(all) let fooUpdateCode = "61636365737328616c6c2920636f6e747261637420466f6f207b0a2020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let aUpdateCode = "61636365737328616c6c2920636f6e747261637420696e746572666163652041207b0a202020200a2020202061636365737328616c6c29207265736f7572636520696e746572666163652049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e670a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f757263652052203a2049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" +access(all) let bUpdateCode = "696d706f727420412066726f6d206162636173646672713431320a0a61636365737328616c6c2920636f6e74726163742042203a2041207b0a202020200a2020202061636365737328616c6c29207265736f757263652052203a20412e49207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a2020202061636365737328616c6c292066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" +access(all) let cUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a696d706f727420422066726f6d203078303030303030303030303030303031300a0a61636365737328616c6c2920636f6e74726163742043207b0a0a2020202061636365737328616c6c29206c65742053746f72616765506174683a2053746f72616765506174680a2020202061636365737328616c6c29206c6574205075626c6963506174683a205075626c6963506174680a0a2020202061636365737328616c6c29207265736f7572636520696e74657266616365204f757465725075626c6963207b0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f75726365204f75746572203a204f757465725075626c6963207b0a202020202020202061636365737328616c6c29206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" access(all) fun setup() { var err = Test.deployContract( name: "StagedContractUpdates", path: "../contracts/StagedContractUpdates.cdc", - arguments: [] + arguments: [getCurrentBlockHeight() + blockHeightBoundaryDelay] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "Foo", - path: "../contracts/example/Foo.cdc", + path: "../contracts/test/Foo.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "A", - path: "../contracts/example/A.cdc", + path: "../contracts/test/A.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "B", - path: "../contracts/example/B.cdc", + path: "../contracts/test/B.cdc", arguments: [] ) Test.expect(err, Test.beNil()) err = Test.deployContract( name: "C", - path: "../contracts/example/C.cdc", + path: "../contracts/test/C.cdc", arguments: [] ) Test.expect(err, Test.beNil()) } -access(all) fun testDeploymentAddressMatchesDelegateeAddress() { - let expectedAddress = admin.address - let actualAddress = executeScript("../scripts/get_contract_delegatee_address.cdc", []).returnValue as! Address? - ?? panic("Problem retrieving deployment address") - Test.assertEqual(expectedAddress, actualAddress) -} - access(all) fun testEmptyDeploymentUpdaterInitFails() { let alice = Test.createAccount() let txResult = executeTransaction( @@ -83,10 +76,9 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { Test.assertEqual(expectedPreUpdateResult, actualPreUpdateResult) // Configure Updater resource in Foo contract account - let blockUpdateBoundary: UInt64 = getCurrentBlock().height + blockHeightBoundaryDelay let txResult = executeTransaction( "../transactions/setup_updater_single_account_and_contract.cdc", - [blockUpdateBoundary, "Foo", fooUpdateCode], + ["Foo", fooUpdateCode], fooAccount ) Test.expect(txResult, Test.beSucceeded()) @@ -179,7 +171,6 @@ access(all) fun testDelegationOfCompletedUpdaterFails() { } access(all) fun testSetupMultiContractMultiAccountUpdater() { - let contractAddresses: [Address] = [aAccount.address, bcAccount.address] let stage0: [{Address: {String: String}}] = [ { @@ -204,7 +195,6 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { ] // TODO: Negative case where no contract address contained for a given contract let deploymentConfig: [[{Address: {String: String}}]] = [stage0, stage1, stage2] - let blockHeightBoundary: UInt64 = getCurrentBlock().height + blockHeightBoundaryDelay let aHostTxResult = executeTransaction( "../transactions/publish_host_capability.cdc", @@ -222,7 +212,7 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { let setupUpdaterTxResult = executeTransaction( "../transactions/setup_updater_multi_account.cdc", - [blockHeightBoundary, contractAddresses, deploymentConfig], + [contractAddresses, deploymentConfig], abcUpdater ) @@ -245,8 +235,7 @@ access(all) fun testUpdateMultiContractMultiAccountUpdater() { access(all) fun jumpToUpdateBoundary(forUpdater: Address) { // Identify current block height in test environment - var currentHeight = executeScript("../scripts/test/get_block_height.cdc", []).returnValue as! UInt64? - ?? panic("Problem retrieving current block height") + let currentHeight = getCurrentBlockHeight() // Identify number of blocks to advance let updateBoundary = executeScript( "../scripts/get_block_update_boundary_from_updater.cdc", diff --git a/transactions/delegate.cdc b/transactions/delegate.cdc index c5e896d..5a05659 100644 --- a/transactions/delegate.cdc +++ b/transactions/delegate.cdc @@ -16,17 +16,16 @@ transaction { } // Continue... - let delegateeAccount = getAccount(StagedContractUpdates.getContractDelegateeAddress()) - let updaterPrivatePath = PrivatePath( - identifier: "StagedContractUpdatesUpdater_".concat(delegateeAccount.address.toString()) - )! - // Get reference to the contract's DelegateePublic - self.delegatee = delegateeAccount.getCapability<&{StagedContractUpdates.DelegateePublic}>( - StagedContractUpdates.DelegateePublicPath - ).borrow() + self.delegatee = StagedContractUpdates.getContractDelegateeCapability().borrow() ?? panic("Could not borrow Delegatee reference") + let updaterPrivatePath = PrivatePath( + identifier: "StagedContractUpdatesUpdater_".concat( + self.delegatee.owner?.address?.toString() ?? panic("Problem referencing contract's DelegateePublic owner address") + ) + )! + // Link Updater Capability in private if needed & retrieve if !signer.getCapability<&StagedContractUpdates.Updater>(updaterPrivatePath).check() { signer.unlink(updaterPrivatePath) diff --git a/transactions/execute_delegated_updates.cdc b/transactions/execute_all_delegated_updates.cdc similarity index 100% rename from transactions/execute_delegated_updates.cdc rename to transactions/execute_all_delegated_updates.cdc diff --git a/transactions/remove_from_delegatee_as_updater.cdc b/transactions/remove_from_delegatee_as_updater.cdc index eb344a4..4276a73 100644 --- a/transactions/remove_from_delegatee_as_updater.cdc +++ b/transactions/remove_from_delegatee_as_updater.cdc @@ -2,19 +2,21 @@ import "StagedContractUpdates" transaction { - let delegatee: &StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic} - let updaterCap: Capability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}> + let delegatee: &{StagedContractUpdates.DelegateePublic} + let updaterCap: Capability<&StagedContractUpdates.Updater> let updaterID: UInt64 prepare(signer: AuthAccount) { - let delegateeAccount = getAccount(StagedContractUpdates.getContractDelegateeAddress()) - self.delegatee = delegateeAccount.getCapability<&StagedContractUpdates.Delegatee{StagedContractUpdates.DelegateePublic}>( - StagedContractUpdates.DelegateePublicPath - ).borrow() + self.delegatee = StagedContractUpdates.getContractDelegateeCapability().borrow() ?? panic("Could not borrow Delegatee reference") - self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater{StagedContractUpdates.DelegatedUpdater, StagedContractUpdates.UpdaterPublic}>( - StagedContractUpdates.DelegatedUpdaterPrivatePath - ) + + let updaterPrivatePath = PrivatePath( + identifier: "StagedContractUpdatesUpdater_".concat( + self.delegatee.owner?.address?.toString() ?? panic("Problem referencing contract's DelegateePublic owner address") + ) + )! + + self.updaterCap = signer.getCapability<&StagedContractUpdates.Updater>(updaterPrivatePath) self.updaterID = self.updaterCap.borrow()?.getID() ?? panic("Invalid Updater Capability retrieved from signer!") } diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/setup_updater_multi_account.cdc index da53095..998b7d6 100644 --- a/transactions/setup_updater_multi_account.cdc +++ b/transactions/setup_updater_multi_account.cdc @@ -13,7 +13,7 @@ import "StagedContractUpdates" /// This transaction also assumes that all contract hosting AuthAccount Capabilities have been published for the signer /// to claim. /// -transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymentConfig: [[{Address: {String: String}}]]) { +transaction(contractAddresses: [Address], deploymentConfig: [[{Address: {String: String}}]]) { prepare(signer: AuthAccount) { // Abort if Updater is already configured in signer's account @@ -40,7 +40,7 @@ transaction(blockUpdateBoundary: UInt64, contractAddresses: [Address], deploymen // Construct the updater, save and link public Capability let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockUpdateBoundary, + blockUpdateBoundary: StagedContractUpdates.blockUpdateBoundary, hosts: hostCaps, deployments: deployments ) diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/setup_updater_single_account_and_contract.cdc index 51d9c75..edde25f 100644 --- a/transactions/setup_updater_single_account_and_contract.cdc +++ b/transactions/setup_updater_single_account_and_contract.cdc @@ -7,7 +7,7 @@ import "StagedContractUpdates" /// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos a /// simple case where the signer is the deployment account and deployment only includes a single contract. /// -transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { +transaction(contractName: String, code: String) { prepare(signer: AuthAccount) { @@ -41,10 +41,10 @@ transaction(blockUpdateBoundary: UInt64, contractName: String, code: String) { } let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) - // Create Updater resource + // Create Updater resource, assigning the contract .blockUpdateBoundary to the new Updater signer.save( <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockUpdateBoundary, + blockUpdateBoundary: StagedContractUpdates.blockUpdateBoundary, hosts: [hostCap], deployments: [[ StagedContractUpdates.ContractUpdate( diff --git a/transactions/test/setup_updater_with_empty_deployment.cdc b/transactions/test/setup_updater_with_empty_deployment.cdc index fd04118..8b03ff8 100644 --- a/transactions/test/setup_updater_with_empty_deployment.cdc +++ b/transactions/test/setup_updater_with_empty_deployment.cdc @@ -6,7 +6,7 @@ import "StagedContractUpdates" transaction { prepare(signer: AuthAccount) { let updater <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: getCurrentBlock().height, + blockUpdateBoundary: StagedContractUpdates.blockUpdateBoundary, hosts: [], deployments: [] ) From f2b4a7598b62e4db6ca41285250c3995d7398090 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:57:28 -0600 Subject: [PATCH 23/34] add test coverage for end-to-end delegated & self-hosted updates --- tests/staged_contract_updater_tests.cdc | 242 ++++++++++++------ transactions/setup_updater_multi_account.cdc | 4 +- ...up_updater_single_account_and_contract.cdc | 4 +- 3 files changed, 167 insertions(+), 83 deletions(-) diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index d9d6579..016fa30 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -16,7 +16,7 @@ access(all) let abcUpdater = Test.createAccount() // Content of update contracts as hex strings access(all) let fooUpdateCode = "61636365737328616c6c2920636f6e747261637420466f6f207b0a2020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" access(all) let aUpdateCode = "61636365737328616c6c2920636f6e747261637420696e746572666163652041207b0a202020200a2020202061636365737328616c6c29207265736f7572636520696e746572666163652049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e670a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f757263652052203a2049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" -access(all) let bUpdateCode = "696d706f727420412066726f6d206162636173646672713431320a0a61636365737328616c6c2920636f6e74726163742042203a2041207b0a202020200a2020202061636365737328616c6c29207265736f757263652052203a20412e49207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a2020202061636365737328616c6c292066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" +access(all) let bUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a0a61636365737328616c6c2920636f6e74726163742042203a2041207b0a202020200a2020202061636365737328616c6c29207265736f757263652052203a20412e49207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a2020202061636365737328616c6c292066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" access(all) let cUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a696d706f727420422066726f6d203078303030303030303030303030303031300a0a61636365737328616c6c2920636f6e74726163742043207b0a0a2020202061636365737328616c6c29206c65742053746f72616765506174683a2053746f72616765506174680a2020202061636365737328616c6c29206c6574205075626c6963506174683a205075626c6963506174680a0a2020202061636365737328616c6c29207265736f7572636520696e74657266616365204f757465725075626c6963207b0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f75726365204f75746572203a204f757465725075626c6963207b0a202020202020202061636365737328616c6c29206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" access(all) fun setup() { @@ -66,6 +66,165 @@ access(all) fun testEmptyDeploymentUpdaterInitFails() { Test.expect(txResult, Test.beFailed()) } +access(all) fun testDelegationOfCompletedUpdaterFails() { + let txResult = executeTransaction( + "../transactions/delegate.cdc", + [], + fooAccount + ) + Test.expect(txResult, Test.beFailed()) +} + +access(all) fun testSetupMultiContractMultiAccountUpdater() { + let contractAddresses: [Address] = [aAccount.address, bcAccount.address] + let stage0: [{Address: {String: String}}] = [ + { + aAccount.address: { + "A": aUpdateCode + } + } + ] + let stage1: [{Address: {String: String}}] = [ + { + bcAccount.address: { + "B": bUpdateCode + } + } + ] + let stage2: [{Address: {String: String}}] = [ + { + bcAccount.address: { + "C": cUpdateCode + } + } + ] + + let deploymentConfig: [[{Address: {String: String}}]] = [stage0, stage1, stage2] + + let aHostTxResult = executeTransaction( + "../transactions/publish_host_capability.cdc", + [abcUpdater.address], + aAccount + ) + Test.expect(aHostTxResult, Test.beSucceeded()) + + let bcHostTxResult = executeTransaction( + "../transactions/publish_host_capability.cdc", + [abcUpdater.address], + bcAccount + ) + Test.expect(bcHostTxResult, Test.beSucceeded()) + + let setupUpdaterTxResult = executeTransaction( + "../transactions/setup_updater_multi_account.cdc", + [nil, contractAddresses, deploymentConfig], + abcUpdater + ) + + // Confirm UpdaterCreated event was properly emitted + // TODO: Uncomment once bug is fixed allowing contract import + // var events = Test.eventsOfType(Type()) + // Test.assertEqual(0, events.length) + + // Validate the current deployment stage is 0 + let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) +} + +access(all) fun testUpdaterDelegationSucceeds() { + // Validate the current deployment stage is still 0 + var currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) + + // Delegate ABC updater to contract's delegatee + let txResult = executeTransaction( + "../transactions/delegate.cdc", + [], + abcUpdater + ) + Test.expect(txResult, Test.beSucceeded()) + + // Ensure valid Updater Capability received by Delegatee + let validCapReceived = executeScript( + "../scripts/check_delegatee_has_valid_updater_cap.cdc", + [abcUpdater.address, admin.address] + ).returnValue as! Bool? ?? panic("Updater was not found at given address") + Test.assertEqual(true, validCapReceived) +} + +access(all) fun testDelegatedUpdateSucceeds() { + // Validate the current deployment stage is still 0 + var currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(0, currentStage) + + jumpToUpdateBoundary(forUpdater: abcUpdater.address) + + // Execute first update stage as Delegatee + var updateTxResult = executeTransaction( + "../transactions/execute_all_delegated_updates.cdc", + [], + admin + ) + Test.expect(updateTxResult, Test.beSucceeded()) + + // Validate stage incremented + currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(1, currentStage) + + // Continue through remaining stages (should total 3) + updateTxResult = executeTransaction( + "../transactions/execute_all_delegated_updates.cdc", + [], + admin + ) + Test.expect(updateTxResult, Test.beSucceeded()) + + // Ensure update is not yet complete before final stage + var updateComplete = executeScript( + "../scripts/has_been_updated.cdc", + [abcUpdater.address] + ).returnValue as! Bool? ?? panic("Updater was not found at given address") + Test.assertEqual(false, updateComplete) + + currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(2, currentStage) + + updateTxResult = executeTransaction( + "../transactions/execute_all_delegated_updates.cdc", + [], + admin + ) + Test.expect(updateTxResult, Test.beSucceeded()) + + currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + ?? panic("Updater was not found at given address") + Test.assertEqual(3, currentStage) + + // Validate that Updater has completed all stages + updateComplete = executeScript( + "../scripts/has_been_updated.cdc", + [abcUpdater.address] + ).returnValue as! Bool? ?? panic("Problem validating Updater delegation success") + Test.assertEqual(true, updateComplete) + + // Confirm UpdaterUpdated event was properly emitted + // TODO: Uncomment once bug is fixed allowing contract import + // events = Test.eventsOfType(Type()) + // Test.assertEqual(3, events.length) + + // Validate the Delegatee has removed the Updater Capability after completion + let updaterCapRemoved = executeScript( + "../scripts/check_delegatee_has_valid_updater_cap.cdc", + [abcUpdater.address, admin.address] + ).returnValue as! Bool? + Test.assertEqual(nil, updaterCapRemoved) +} + access(all) fun testSetupSingleContractSingleHostSelfUpdate() { let expectedPreUpdateResult: String = "foo" @@ -78,7 +237,7 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { // Configure Updater resource in Foo contract account let txResult = executeTransaction( "../transactions/setup_updater_single_account_and_contract.cdc", - ["Foo", fooUpdateCode], + [getCurrentBlockHeight() + blockHeightBoundaryDelay, "Foo", fooUpdateCode], fooAccount ) Test.expect(txResult, Test.beSucceeded()) @@ -86,7 +245,7 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { // Confirm UpdaterCreated event was properly emitted // TODO: Uncomment once bug is fixed allowing contract import // var events = Test.eventsOfType(Type()) - // Test.assertEqual(1, events.length) + // Test.assertEqual(2, events.length) // Validate the current deployment stage is 0 let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? @@ -115,11 +274,6 @@ access(all) fun testExecuteUpdateFailsBeforeBoundary() { Test.assertEqual(0, stagePost) } -// TODO -// access(all) fun testDelegatedUpdate() { -// /* TODO */ -// } - access(all) fun testExecuteUpdateSucceedsAfterBoundary() { let expectedPostUpdateResult: String = "bar" @@ -153,7 +307,7 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { // Confirm UpdaterUpdated event was properly emitted // TODO: Uncomment once bug is fixed allowing contract import // events = Test.eventsOfType(Type()) - // Test.assertEqual(1, events.length) + // Test.assertEqual(4, events.length) // Validate the post-update value of Foo.foo() let actualPostUpdateResult = executeScript("../scripts/test/foo.cdc", []).returnValue as! String? @@ -161,76 +315,6 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) } -access(all) fun testDelegationOfCompletedUpdaterFails() { - let txResult = executeTransaction( - "../transactions/delegate.cdc", - [], - fooAccount - ) - Test.expect(txResult, Test.beFailed()) -} - -access(all) fun testSetupMultiContractMultiAccountUpdater() { - let contractAddresses: [Address] = [aAccount.address, bcAccount.address] - let stage0: [{Address: {String: String}}] = [ - { - aAccount.address: { - "A": aUpdateCode - } - } - ] - let stage1: [{Address: {String: String}}] = [ - { - bcAccount.address: { - "B": bUpdateCode - } - } - ] - let stage2: [{Address: {String: String}}] = [ - { - bcAccount.address: { - "C": cUpdateCode - } - } - ] - // TODO: Negative case where no contract address contained for a given contract - let deploymentConfig: [[{Address: {String: String}}]] = [stage0, stage1, stage2] - - let aHostTxResult = executeTransaction( - "../transactions/publish_host_capability.cdc", - [abcUpdater.address], - aAccount - ) - Test.expect(aHostTxResult, Test.beSucceeded()) - - let bcHostTxResult = executeTransaction( - "../transactions/publish_host_capability.cdc", - [abcUpdater.address], - bcAccount - ) - Test.expect(bcHostTxResult, Test.beSucceeded()) - - let setupUpdaterTxResult = executeTransaction( - "../transactions/setup_updater_multi_account.cdc", - [contractAddresses, deploymentConfig], - abcUpdater - ) - - // Validate the current deployment stage is 0 - let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? - ?? panic("Updater was not found at given address") - Test.assertEqual(0, currentStage) -} - -access(all) fun testUpdateMultiContractMultiAccountUpdater() { - // Validate the current deployment stage is still 0 - let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? - ?? panic("Updater was not found at given address") - Test.assertEqual(0, currentStage) - - -} - /* --- TEST HELPERS --- */ access(all) fun jumpToUpdateBoundary(forUpdater: Address) { diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/setup_updater_multi_account.cdc index 998b7d6..f80414b 100644 --- a/transactions/setup_updater_multi_account.cdc +++ b/transactions/setup_updater_multi_account.cdc @@ -13,7 +13,7 @@ import "StagedContractUpdates" /// This transaction also assumes that all contract hosting AuthAccount Capabilities have been published for the signer /// to claim. /// -transaction(contractAddresses: [Address], deploymentConfig: [[{Address: {String: String}}]]) { +transaction(blockHeightBoundary: UInt64?, contractAddresses: [Address], deploymentConfig: [[{Address: {String: String}}]]) { prepare(signer: AuthAccount) { // Abort if Updater is already configured in signer's account @@ -40,7 +40,7 @@ transaction(contractAddresses: [Address], deploymentConfig: [[{Address: {String: // Construct the updater, save and link public Capability let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: StagedContractUpdates.blockUpdateBoundary, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, hosts: hostCaps, deployments: deployments ) diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/setup_updater_single_account_and_contract.cdc index edde25f..eda6d07 100644 --- a/transactions/setup_updater_single_account_and_contract.cdc +++ b/transactions/setup_updater_single_account_and_contract.cdc @@ -7,7 +7,7 @@ import "StagedContractUpdates" /// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos a /// simple case where the signer is the deployment account and deployment only includes a single contract. /// -transaction(contractName: String, code: String) { +transaction(blockHeightBoundary: UInt64?, contractName: String, code: String) { prepare(signer: AuthAccount) { @@ -44,7 +44,7 @@ transaction(contractName: String, code: String) { // Create Updater resource, assigning the contract .blockUpdateBoundary to the new Updater signer.save( <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: StagedContractUpdates.blockUpdateBoundary, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, hosts: [hostCap], deployments: [[ StagedContractUpdates.ContractUpdate( From df74b4b7a43f69a3825f35b915e0b1ba49c32ec4 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:09:25 -0600 Subject: [PATCH 24/34] add Coordinator resource to update .blockUpdateBoundary --- contracts/StagedContractUpdates.cdc | 43 +++++++++++-------- tests/staged_contract_updater_tests.cdc | 22 ++++++++++ .../coordinator/set_block_update_boundary.cdc | 11 +++++ 3 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 transactions/coordinator/set_block_update_boundary.cdc diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 4044ccf..0b0929c 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -28,12 +28,10 @@ access(all) contract StagedContractUpdates { // access(all) let HostStoragePath: StoragePath access(all) let UpdaterStoragePath: StoragePath - // access(all) let DelegatedUpdaterPrivatePath: PrivatePath access(all) let UpdaterPublicPath: PublicPath - // access(all) let UpdaterContractAccountPrivatePath: PrivatePath access(all) let DelegateeStoragePath: StoragePath - // access(all) let DelegateePrivatePath: PrivatePath access(all) let DelegateePublicPath: PublicPath + access(all) let CoordinatorStoragePath: StoragePath /* --- Events --- */ // @@ -428,6 +426,26 @@ access(all) contract StagedContractUpdates { } } + /* --- Coordinator --- */ + // + /// This resource coordinates block update boundaries for all who opt-in to coordinated updates + /// + access(all) resource Coordinator { + /// Allows the contract block update boundary to be set + /// + access(all) fun setBlockUpdateBoundary(new: UInt64) { + pre { + new > getCurrentBlock().height: "New boundary must be in the future!" + new > StagedContractUpdates.blockUpdateBoundary: "New block update boundary must be greater than current boundary!" + } + let old = StagedContractUpdates.blockUpdateBoundary + StagedContractUpdates.blockUpdateBoundary = new + emit ContractBlockUpdateBoundaryUpdated(old: old, new: new) + } + } + + /* --- Contract Methods --- */ + /// Returns the Capability of the Delegatee associated with this contract /// access(all) fun getContractDelegateeCapability(): Capability<&{DelegateePublic}> { @@ -494,18 +512,6 @@ access(all) contract StagedContractUpdates { return <- create Delegatee(blockUpdateBoundary: blockUpdateBoundary) } - /// Allows the contract block update boundary to be set - /// - access(account) fun setBlockUpdateBoundary(new: UInt64) { - pre { - new > getCurrentBlock().height: "New boundary must be in the future!" - new > self.blockUpdateBoundary: "New block update boundary must be greater than current boundary!" - } - let old = self.blockUpdateBoundary - self.blockUpdateBoundary = new - emit ContractBlockUpdateBoundaryUpdated(old: old, new: new) - } - init(blockUpdateBoundary: UInt64) { let contractAddress = self.account.address.toString() @@ -513,18 +519,17 @@ access(all) contract StagedContractUpdates { self.inboxHostCapabilityNamePrefix = "StagedContractUpdatesHostCapability_" self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! - // self.HostPrivatePath = PrivatePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! self.UpdaterStoragePath = StoragePath(identifier: "StagedContractUpdatesUpdater_".concat(contractAddress))! - // self.DelegatedUpdaterPrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatedUpdater_".concat(contractAddress))! self.UpdaterPublicPath = PublicPath(identifier: "StagedContractUpdatesUpdaterPublic_".concat(contractAddress))! - // self.UpdaterContractAccountPrivatePath = PrivatePath(identifier: "UpdaterContractAccount_".concat(contractAddress))! self.DelegateeStoragePath = StoragePath(identifier: "StagedContractUpdatesDelegatee_".concat(contractAddress))! - // self.DelegateePrivatePath = PrivatePath(identifier: "StagedContractUpdatesDelegatee_".concat(contractAddress))! self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(contractAddress))! + self.CoordinatorStoragePath = StoragePath(identifier: "StagedContractUpdatesCoordinator_".concat(contractAddress))! self.account.save(<-create Delegatee(blockUpdateBoundary: blockUpdateBoundary), to: self.DelegateeStoragePath) self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) + self.account.save(<-create Coordinator(), to: self.CoordinatorStoragePath) + emit ContractBlockUpdateBoundaryUpdated(old: nil, new: blockUpdateBoundary) } } diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 016fa30..1fb583b 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -315,6 +315,28 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) } +access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [1], + admin + ) + Test.expect(txResult, Test.beFailed()) +} + +access(all) fun testCoordinatorSetBlockUpdateBoundarySucceeds() { + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [getCurrentBlockHeight() + blockHeightBoundaryDelay], + admin + ) + Test.expect(txResult, Test.beSucceeded()) + + // TODO: Uncomment once bug is fixed allowing contract import + // events = Test.eventsOfType(Type()) + // Test.assertEqual(1, events.length) +} + /* --- TEST HELPERS --- */ access(all) fun jumpToUpdateBoundary(forUpdater: Address) { diff --git a/transactions/coordinator/set_block_update_boundary.cdc b/transactions/coordinator/set_block_update_boundary.cdc new file mode 100644 index 0000000..40b412c --- /dev/null +++ b/transactions/coordinator/set_block_update_boundary.cdc @@ -0,0 +1,11 @@ +import "StagedContractUpdates" + +/// Allows the contract Coordinator to set a new blockUpdateBoundary +/// +transaction(newBoundary: UInt64) { + prepare(signer: AuthAccount) { + signer.borrow<&StagedContractUpdates.Coordinator>(from: StagedContractUpdates.CoordinatorStoragePath) + ?.setBlockUpdateBoundary(new: newBoundary) + ?? panic("No Coordinator in found!") + } +} From e72ccdcd928b1df3cd22392ee5560d74c43ff790 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:20:59 -0600 Subject: [PATCH 25/34] add check on Delegatee.delegate, preventing delegation after boundary --- contracts/StagedContractUpdates.cdc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 0b0929c..a292f7d 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -354,6 +354,8 @@ access(all) contract StagedContractUpdates { /// access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { + getCurrentBlock().height < self.blockUpdateBoundary: + "Delegation must occur before Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) updaterCap.check(): "Invalid DelegatedUpdater Capability!" updaterCap.borrow()!.hasBeenUpdated() == false: "Updater has already been updated!" updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary: From cf74dbc39f238f9d05d60078eb000b611f1bddb7 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:31:27 -0600 Subject: [PATCH 26/34] add getter for invalid Host Caps in Updater --- contracts/StagedContractUpdates.cdc | 14 ++++++++++++++ scripts/get_invalid_hosts.cdc | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 scripts/get_invalid_hosts.cdc diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index a292f7d..46f4f22 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -153,6 +153,7 @@ access(all) contract StagedContractUpdates { access(all) fun getCurrentDeploymentStage(): Int access(all) fun getFailedDeployments(): {Int: [String]} access(all) fun hasBeenUpdated(): Bool + access(all) fun getInvalidHosts(): [Address]? } /// Resource that enables delayed contract updates to a wrapped account at or beyond a specified block height @@ -290,6 +291,19 @@ access(all) contract StagedContractUpdates { return self.updateComplete } + access(all) fun getInvalidHosts(): [Address]? { + var invalidHosts: [Address]? = nil + for host in self.hosts.values { + if !host.check() || !host.borrow()!.checkAccountCapability() { + if invalidHosts == nil { + invalidHosts = [] + } + invalidHosts!.append(host.address) + } + } + return invalidHosts + } + /* --- MetadataViews.Resolver --- */ access(all) fun getViews(): [Type] { diff --git a/scripts/get_invalid_hosts.cdc b/scripts/get_invalid_hosts.cdc new file mode 100644 index 0000000..718d354 --- /dev/null +++ b/scripts/get_invalid_hosts.cdc @@ -0,0 +1,13 @@ +import "MetadataViews" + +import "StagedContractUpdates" + +/// Returns addresses of Hosts with either invalid Host or encapsulate AuthAccount Capabilities from the Updater at the +/// given address or nil if none is found +/// +pub fun main(updaterAddress: Address): [Address]? { + return getAccount(updaterAddress).getCapability<&{StagedContractUpdates.UpdaterPublic}>( + StagedContractUpdates.UpdaterPublicPath + ).borrow() + ?.getInvalidHosts() ?? nil +} From 154aa54aff5074d5cb13b60990c89c6b96d655b1 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:32:06 -0600 Subject: [PATCH 27/34] restructure transactions/ dir for clarity of purpose --- tests/staged_contract_updater_tests.cdc | 45 +++++++++++-------- .../execute_all_delegated_updates.cdc | 0 .../remove_delegated_updater.cdc | 0 .../{ => host}/publish_host_capability.cdc | 0 transactions/{ => updater}/delegate.cdc | 0 .../remove_from_delegatee_as_updater.cdc | 0 .../setup_updater_multi_account.cdc | 0 ...up_updater_single_account_and_contract.cdc | 0 transactions/{ => updater}/update.cdc | 0 9 files changed, 26 insertions(+), 19 deletions(-) rename transactions/{ => delegatee}/execute_all_delegated_updates.cdc (100%) rename transactions/{ => delegatee}/remove_delegated_updater.cdc (100%) rename transactions/{ => host}/publish_host_capability.cdc (100%) rename transactions/{ => updater}/delegate.cdc (100%) rename transactions/{ => updater}/remove_from_delegatee_as_updater.cdc (100%) rename transactions/{ => updater}/setup_updater_multi_account.cdc (100%) rename transactions/{ => updater}/setup_updater_single_account_and_contract.cdc (100%) rename transactions/{ => updater}/update.cdc (100%) diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 1fb583b..055b71a 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -66,15 +66,6 @@ access(all) fun testEmptyDeploymentUpdaterInitFails() { Test.expect(txResult, Test.beFailed()) } -access(all) fun testDelegationOfCompletedUpdaterFails() { - let txResult = executeTransaction( - "../transactions/delegate.cdc", - [], - fooAccount - ) - Test.expect(txResult, Test.beFailed()) -} - access(all) fun testSetupMultiContractMultiAccountUpdater() { let contractAddresses: [Address] = [aAccount.address, bcAccount.address] let stage0: [{Address: {String: String}}] = [ @@ -102,21 +93,21 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { let deploymentConfig: [[{Address: {String: String}}]] = [stage0, stage1, stage2] let aHostTxResult = executeTransaction( - "../transactions/publish_host_capability.cdc", + "../transactions/host/publish_host_capability.cdc", [abcUpdater.address], aAccount ) Test.expect(aHostTxResult, Test.beSucceeded()) let bcHostTxResult = executeTransaction( - "../transactions/publish_host_capability.cdc", + "../transactions/host/publish_host_capability.cdc", [abcUpdater.address], bcAccount ) Test.expect(bcHostTxResult, Test.beSucceeded()) let setupUpdaterTxResult = executeTransaction( - "../transactions/setup_updater_multi_account.cdc", + "../transactions/updater/setup_updater_multi_account.cdc", [nil, contractAddresses, deploymentConfig], abcUpdater ) @@ -130,6 +121,13 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, currentStage) + + // Check Updater has valid Host Capabilities + let invalidHosts = executeScript( + "../scripts/get_invalid_hosts.cdc", + [abcUpdater.address] + ).returnValue as! [Address]? ?? panic("Updater was not found at given address") + Test.assert(invalidHosts.length == 0, message: "Invalid hosts found") } access(all) fun testUpdaterDelegationSucceeds() { @@ -140,7 +138,7 @@ access(all) fun testUpdaterDelegationSucceeds() { // Delegate ABC updater to contract's delegatee let txResult = executeTransaction( - "../transactions/delegate.cdc", + "../transactions/updater/delegate.cdc", [], abcUpdater ) @@ -164,7 +162,7 @@ access(all) fun testDelegatedUpdateSucceeds() { // Execute first update stage as Delegatee var updateTxResult = executeTransaction( - "../transactions/execute_all_delegated_updates.cdc", + "../transactions/delegatee/execute_all_delegated_updates.cdc", [], admin ) @@ -177,7 +175,7 @@ access(all) fun testDelegatedUpdateSucceeds() { // Continue through remaining stages (should total 3) updateTxResult = executeTransaction( - "../transactions/execute_all_delegated_updates.cdc", + "../transactions/delegatee/execute_all_delegated_updates.cdc", [], admin ) @@ -195,7 +193,7 @@ access(all) fun testDelegatedUpdateSucceeds() { Test.assertEqual(2, currentStage) updateTxResult = executeTransaction( - "../transactions/execute_all_delegated_updates.cdc", + "../transactions/delegatee/execute_all_delegated_updates.cdc", [], admin ) @@ -236,7 +234,7 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { // Configure Updater resource in Foo contract account let txResult = executeTransaction( - "../transactions/setup_updater_single_account_and_contract.cdc", + "../transactions/updater/setup_updater_single_account_and_contract.cdc", [getCurrentBlockHeight() + blockHeightBoundaryDelay, "Foo", fooUpdateCode], fooAccount ) @@ -262,7 +260,7 @@ access(all) fun testExecuteUpdateFailsBeforeBoundary() { // Execute update as Foo contract account let txResult = executeTransaction( - "../transactions/update.cdc", + "../transactions/updater/update.cdc", [], fooAccount ) @@ -288,7 +286,7 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { // Execute update as Foo contract account let txResult = executeTransaction( - "../transactions/update.cdc", + "../transactions/updater/update.cdc", [], fooAccount ) @@ -315,6 +313,15 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { Test.assertEqual(expectedPostUpdateResult, actualPostUpdateResult) } +access(all) fun testDelegationOfCompletedUpdaterFails() { + let txResult = executeTransaction( + "../transactions/updater/delegate.cdc", + [], + fooAccount + ) + Test.expect(txResult, Test.beFailed()) +} + access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { let txResult = executeTransaction( "../transactions/coordinator/set_block_update_boundary.cdc", diff --git a/transactions/execute_all_delegated_updates.cdc b/transactions/delegatee/execute_all_delegated_updates.cdc similarity index 100% rename from transactions/execute_all_delegated_updates.cdc rename to transactions/delegatee/execute_all_delegated_updates.cdc diff --git a/transactions/remove_delegated_updater.cdc b/transactions/delegatee/remove_delegated_updater.cdc similarity index 100% rename from transactions/remove_delegated_updater.cdc rename to transactions/delegatee/remove_delegated_updater.cdc diff --git a/transactions/publish_host_capability.cdc b/transactions/host/publish_host_capability.cdc similarity index 100% rename from transactions/publish_host_capability.cdc rename to transactions/host/publish_host_capability.cdc diff --git a/transactions/delegate.cdc b/transactions/updater/delegate.cdc similarity index 100% rename from transactions/delegate.cdc rename to transactions/updater/delegate.cdc diff --git a/transactions/remove_from_delegatee_as_updater.cdc b/transactions/updater/remove_from_delegatee_as_updater.cdc similarity index 100% rename from transactions/remove_from_delegatee_as_updater.cdc rename to transactions/updater/remove_from_delegatee_as_updater.cdc diff --git a/transactions/setup_updater_multi_account.cdc b/transactions/updater/setup_updater_multi_account.cdc similarity index 100% rename from transactions/setup_updater_multi_account.cdc rename to transactions/updater/setup_updater_multi_account.cdc diff --git a/transactions/setup_updater_single_account_and_contract.cdc b/transactions/updater/setup_updater_single_account_and_contract.cdc similarity index 100% rename from transactions/setup_updater_single_account_and_contract.cdc rename to transactions/updater/setup_updater_single_account_and_contract.cdc diff --git a/transactions/update.cdc b/transactions/updater/update.cdc similarity index 100% rename from transactions/update.cdc rename to transactions/updater/update.cdc From 2606e441adb0514af1d7c336b4b44bf293b3a77b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:40:33 -0600 Subject: [PATCH 28/34] fix Updater.getInvalidHosts() to return non-optional --- contracts/StagedContractUpdates.cdc | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 46f4f22..8545c56 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -153,7 +153,7 @@ access(all) contract StagedContractUpdates { access(all) fun getCurrentDeploymentStage(): Int access(all) fun getFailedDeployments(): {Int: [String]} access(all) fun hasBeenUpdated(): Bool - access(all) fun getInvalidHosts(): [Address]? + access(all) fun getInvalidHosts(): [Address] } /// Resource that enables delayed contract updates to a wrapped account at or beyond a specified block height @@ -291,14 +291,11 @@ access(all) contract StagedContractUpdates { return self.updateComplete } - access(all) fun getInvalidHosts(): [Address]? { - var invalidHosts: [Address]? = nil + access(all) fun getInvalidHosts(): [Address] { + var invalidHosts: [Address] = [] for host in self.hosts.values { if !host.check() || !host.borrow()!.checkAccountCapability() { - if invalidHosts == nil { - invalidHosts = [] - } - invalidHosts!.append(host.address) + invalidHosts.append(host.address) } } return invalidHosts From c0fe291886186f32dd29a4eca5a3062bcd3e6164 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:40:57 -0600 Subject: [PATCH 29/34] restructure scripts/ dir for clarity & update tests --- .../check_delegatee_has_valid_updater_cap.cdc | 0 ...get_block_update_boundary_from_updater.cdc | 0 .../get_current_deployment_stage.cdc | 0 scripts/{ => updater}/get_invalid_hosts.cdc | 2 +- .../get_updater_deployment_order.cdc | 0 .../get_updater_deployment_readable.cdc | 0 scripts/{ => updater}/get_updater_info.cdc | 3 +- scripts/{ => updater}/has_been_updated.cdc | 0 .../{ => util}/get_deployment_from_config.cdc | 0 tests/staged_contract_updater_tests.cdc | 36 +++++++++---------- 10 files changed, 20 insertions(+), 21 deletions(-) rename scripts/{ => delegatee}/check_delegatee_has_valid_updater_cap.cdc (100%) rename scripts/{ => updater}/get_block_update_boundary_from_updater.cdc (100%) rename scripts/{ => updater}/get_current_deployment_stage.cdc (100%) rename scripts/{ => updater}/get_invalid_hosts.cdc (92%) rename scripts/{ => updater}/get_updater_deployment_order.cdc (100%) rename scripts/{ => updater}/get_updater_deployment_readable.cdc (100%) rename scripts/{ => updater}/get_updater_info.cdc (94%) rename scripts/{ => updater}/has_been_updated.cdc (100%) rename scripts/{ => util}/get_deployment_from_config.cdc (100%) diff --git a/scripts/check_delegatee_has_valid_updater_cap.cdc b/scripts/delegatee/check_delegatee_has_valid_updater_cap.cdc similarity index 100% rename from scripts/check_delegatee_has_valid_updater_cap.cdc rename to scripts/delegatee/check_delegatee_has_valid_updater_cap.cdc diff --git a/scripts/get_block_update_boundary_from_updater.cdc b/scripts/updater/get_block_update_boundary_from_updater.cdc similarity index 100% rename from scripts/get_block_update_boundary_from_updater.cdc rename to scripts/updater/get_block_update_boundary_from_updater.cdc diff --git a/scripts/get_current_deployment_stage.cdc b/scripts/updater/get_current_deployment_stage.cdc similarity index 100% rename from scripts/get_current_deployment_stage.cdc rename to scripts/updater/get_current_deployment_stage.cdc diff --git a/scripts/get_invalid_hosts.cdc b/scripts/updater/get_invalid_hosts.cdc similarity index 92% rename from scripts/get_invalid_hosts.cdc rename to scripts/updater/get_invalid_hosts.cdc index 718d354..50ec3a1 100644 --- a/scripts/get_invalid_hosts.cdc +++ b/scripts/updater/get_invalid_hosts.cdc @@ -9,5 +9,5 @@ pub fun main(updaterAddress: Address): [Address]? { return getAccount(updaterAddress).getCapability<&{StagedContractUpdates.UpdaterPublic}>( StagedContractUpdates.UpdaterPublicPath ).borrow() - ?.getInvalidHosts() ?? nil + ?.getInvalidHosts() } diff --git a/scripts/get_updater_deployment_order.cdc b/scripts/updater/get_updater_deployment_order.cdc similarity index 100% rename from scripts/get_updater_deployment_order.cdc rename to scripts/updater/get_updater_deployment_order.cdc diff --git a/scripts/get_updater_deployment_readable.cdc b/scripts/updater/get_updater_deployment_readable.cdc similarity index 100% rename from scripts/get_updater_deployment_readable.cdc rename to scripts/updater/get_updater_deployment_readable.cdc diff --git a/scripts/get_updater_info.cdc b/scripts/updater/get_updater_info.cdc similarity index 94% rename from scripts/get_updater_info.cdc rename to scripts/updater/get_updater_info.cdc index 1b924fe..f2e04e5 100644 --- a/scripts/get_updater_info.cdc +++ b/scripts/updater/get_updater_info.cdc @@ -7,7 +7,6 @@ import "StagedContractUpdates" pub fun main(address: Address): StagedContractUpdates.UpdaterInfo? { return getAccount(address).getCapability<&{StagedContractUpdates.UpdaterPublic, MetadataViews.Resolver}>( StagedContractUpdates.UpdaterPublicPath - ) - .borrow() + ).borrow() ?.resolveView(Type()) as! StagedContractUpdates.UpdaterInfo? } diff --git a/scripts/has_been_updated.cdc b/scripts/updater/has_been_updated.cdc similarity index 100% rename from scripts/has_been_updated.cdc rename to scripts/updater/has_been_updated.cdc diff --git a/scripts/get_deployment_from_config.cdc b/scripts/util/get_deployment_from_config.cdc similarity index 100% rename from scripts/get_deployment_from_config.cdc rename to scripts/util/get_deployment_from_config.cdc diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 055b71a..3e68b99 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -118,13 +118,13 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { // Test.assertEqual(0, events.length) // Validate the current deployment stage is 0 - let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + let currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, currentStage) // Check Updater has valid Host Capabilities let invalidHosts = executeScript( - "../scripts/get_invalid_hosts.cdc", + "../scripts/updater/get_invalid_hosts.cdc", [abcUpdater.address] ).returnValue as! [Address]? ?? panic("Updater was not found at given address") Test.assert(invalidHosts.length == 0, message: "Invalid hosts found") @@ -132,7 +132,7 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { access(all) fun testUpdaterDelegationSucceeds() { // Validate the current deployment stage is still 0 - var currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + var currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, currentStage) @@ -146,7 +146,7 @@ access(all) fun testUpdaterDelegationSucceeds() { // Ensure valid Updater Capability received by Delegatee let validCapReceived = executeScript( - "../scripts/check_delegatee_has_valid_updater_cap.cdc", + "../scripts/delegatee/check_delegatee_has_valid_updater_cap.cdc", [abcUpdater.address, admin.address] ).returnValue as! Bool? ?? panic("Updater was not found at given address") Test.assertEqual(true, validCapReceived) @@ -154,7 +154,7 @@ access(all) fun testUpdaterDelegationSucceeds() { access(all) fun testDelegatedUpdateSucceeds() { // Validate the current deployment stage is still 0 - var currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + var currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, currentStage) @@ -169,7 +169,7 @@ access(all) fun testDelegatedUpdateSucceeds() { Test.expect(updateTxResult, Test.beSucceeded()) // Validate stage incremented - currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(1, currentStage) @@ -183,12 +183,12 @@ access(all) fun testDelegatedUpdateSucceeds() { // Ensure update is not yet complete before final stage var updateComplete = executeScript( - "../scripts/has_been_updated.cdc", + "../scripts/updater/has_been_updated.cdc", [abcUpdater.address] ).returnValue as! Bool? ?? panic("Updater was not found at given address") Test.assertEqual(false, updateComplete) - currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(2, currentStage) @@ -199,13 +199,13 @@ access(all) fun testDelegatedUpdateSucceeds() { ) Test.expect(updateTxResult, Test.beSucceeded()) - currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? + currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [abcUpdater.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(3, currentStage) // Validate that Updater has completed all stages updateComplete = executeScript( - "../scripts/has_been_updated.cdc", + "../scripts/updater/has_been_updated.cdc", [abcUpdater.address] ).returnValue as! Bool? ?? panic("Problem validating Updater delegation success") Test.assertEqual(true, updateComplete) @@ -217,7 +217,7 @@ access(all) fun testDelegatedUpdateSucceeds() { // Validate the Delegatee has removed the Updater Capability after completion let updaterCapRemoved = executeScript( - "../scripts/check_delegatee_has_valid_updater_cap.cdc", + "../scripts/delegatee/check_delegatee_has_valid_updater_cap.cdc", [abcUpdater.address, admin.address] ).returnValue as! Bool? Test.assertEqual(nil, updaterCapRemoved) @@ -246,7 +246,7 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { // Test.assertEqual(2, events.length) // Validate the current deployment stage is 0 - let currentStage = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + let currentStage = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, currentStage) } @@ -254,7 +254,7 @@ access(all) fun testSetupSingleContractSingleHostSelfUpdate() { access(all) fun testExecuteUpdateFailsBeforeBoundary() { // Validate the current deployment stage is still 0 - let stagePrior = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + let stagePrior = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, stagePrior) @@ -267,7 +267,7 @@ access(all) fun testExecuteUpdateFailsBeforeBoundary() { Test.expect(txResult, Test.beSucceeded()) // Validate the current deployment stage is still 0 - let stagePost = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + let stagePost = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, stagePost) } @@ -280,7 +280,7 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { jumpToUpdateBoundary(forUpdater: fooAccount.address) // Validate the current deployment stage is still 0 - let stagePrior = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + let stagePrior = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(0, stagePrior) @@ -293,12 +293,12 @@ access(all) fun testExecuteUpdateSucceedsAfterBoundary() { Test.expect(txResult, Test.beSucceeded()) // Validate the current deployment stage has advanced - let stagePost = executeScript("../scripts/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? + let stagePost = executeScript("../scripts/updater/get_current_deployment_stage.cdc", [fooAccount.address]).returnValue as! Int? ?? panic("Updater was not found at given address") Test.assertEqual(1, stagePost) // Validate the Updater.hasBeenUpdated() returns true - let hasBeenUpdated = executeScript("../scripts/has_been_updated.cdc", [fooAccount.address]).returnValue as! Bool? + let hasBeenUpdated = executeScript("../scripts/updater/has_been_updated.cdc", [fooAccount.address]).returnValue as! Bool? ?? panic("Updater was not found at given address") Test.assertEqual(true, hasBeenUpdated) @@ -351,7 +351,7 @@ access(all) fun jumpToUpdateBoundary(forUpdater: Address) { let currentHeight = getCurrentBlockHeight() // Identify number of blocks to advance let updateBoundary = executeScript( - "../scripts/get_block_update_boundary_from_updater.cdc", + "../scripts/updater/get_block_update_boundary_from_updater.cdc", [forUpdater] ).returnValue as! UInt64? ?? panic("Problem retrieving updater height boundary") From b4755840bf1b5c4fa6ce1306e0bdfb7833fd1817 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:46:01 -0600 Subject: [PATCH 30/34] update README --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d97412b..22f3886 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ advanced deployments are possible with support for multiple contract accounts an 1. We can see that the `Foo` has been deployed, and call its only contract method `foo()`, getting back `"foo"`: ```sh - flow scripts execute ./scripts/foo.cdc + flow scripts execute ./scripts/test/foo.cdc ``` 1. Configure `StagedContractUpdates.Updater`, passing the block height, contract name, and contract code in hex form (see @@ -44,7 +44,7 @@ advanced deployments are possible with support for multiple contract accounts an 1. `code: [String]` ```sh - flow transactions send ./transactions/setup_updater_single_account_and_contract.cdc \ + flow transactions send ./transactions/updater/setup_updater_single_account_and_contract.cdc \ 10 "Foo" 70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d \ --signer foo ``` @@ -58,14 +58,14 @@ advanced deployments are possible with support for multiple contract accounts an 1. We can get details from our `Updater` before updating: ```sh - flow scripts execute ./scripts/get_updater_info.cdc 0xe03daebed8ca0615 + flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 ``` ```sh - flow scripts execute ./scripts/get_updater_deployment.cdc 0xe03daebed8ca0615 + flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 ``` -1. Next, we'll delegate the `Updater` Capability as `DelegatedUpdater` to the `Delegatee` stored in the `StagedContractUpdates`'s account. +1. Next, we'll delegate the `Updater` Capability to the `Delegatee` stored in the `StagedContractUpdates`'s account. ```sh flow transactions send ./transactions/delegate.cdc --signer foo @@ -74,14 +74,14 @@ advanced deployments are possible with support for multiple contract accounts an 1. Lastly, we'll run the updating transaction as the `Delegatee`: ```sh - flow transactions send ./transactions/execute_delegated_updates.cdc + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc ``` 1. And we can validate the update has taken place by calling `Foo.foo()` again and seeing the return value is now `"bar"` ```sh - flow scripts execute ./scripts/foo.cdc + flow scripts execute ./scripts/test/foo.cdc ``` ## Multi-Account Multi-Contract Deployment @@ -138,18 +138,19 @@ account. :information_source: If you haven't already, perform the [setup steps above](#setup) 1. Since we'll be configuring an update deployment across a number of contract accounts, we'll need to delegate access - to those accounts via AuthAccount Capabilities on each. Running the following transaction will link an AuthAccount - Capability on the signer's account and publish it for the account where our `Updater` will live. + to those accounts via AuthAccount Capabilities on each. Running the following transaction will link and encapsulate + an AuthAccount Capability in a `Host` within the signer's account and publish a Capability on it for the account + where our `Updater` will live. ```sh - flow transactions send ./transactions/publish_auth_account_capability.cdc \ - 0xf669cb8d41ce0c74 \ + flow transactions send ./transactions/host/publish_host_capability.cdc \ + 0xe03daebed8ca0615 \ --signer a-account ``` ```sh - flow transactions send ./transactions/publish_auth_account_capability.cdc \ - 0xf669cb8d41ce0c74 \ + flow transactions send ./transactions/host/publish_host_capability.cdc \ + 0xe03daebed8ca0615 \ --signer bc-account ``` @@ -164,7 +165,7 @@ account. 1. `deploymentConfig: [[{Address: {String: String}}]]` ```sh - flow transactions send transactions/setup_updater_multi_account.cdc \ + flow transactions send transactions/updater/setup_updater_multi_account.cdc \ --args-json "$(cat args.json)" \ --signer abc-updater ``` @@ -176,17 +177,17 @@ account. resource was created, so let's query against the updater account to get its info. ```sh - flow scripts execute ./scripts/get_updater_info.cdc 0xf669cb8d41ce0c74 + flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 ``` ```sh - flow scripts execute ./scripts/get_updater_deployment.cdc 0xf669cb8d41ce0c74 + flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 ``` 1. Now we'll delegate a Capability on the `Updater` to the `Delegatee`: ```sh - flow transactions send ./transactions/delegate.cdc --signer abc-updater + flow transactions send ./transactions/updater/delegate.cdc --signer abc-updater ``` 1. In the previous transaction we should see that the `UpdaterDelegationChanged` event includes the `Updater` UUID @@ -194,7 +195,7 @@ account. and execute the update. ```sh - flow transactions send ./transactions/execute_delegated_updates.cdc + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc ``` This transaction calls `Updater.update()`, executing the first staged deployment, and updating contract `A`. Note @@ -203,13 +204,13 @@ account. time updating `B`. ```sh - flow transactions send ./transactions/execute_delegated_updates.cdc + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc ``` Now we see `B` has been updated, but we still have one more stage to complete. Let's complete the staged update. ```sh - flow transactions send ./transactions/execute_delegated_updates.cdc + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc ``` And finally, we see that `C` was updated and `updateComplete` is now `true`. \ No newline at end of file From 8fce0c4fea79d4018b68f8e3db8fce02e292a90b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:45:15 -0600 Subject: [PATCH 31/34] update StagedContractUpdates comments and Updater.update impl --- contracts/StagedContractUpdates.cdc | 58 ++++++++++--------- .../remove_from_delegatee_as_updater.cdc | 2 +- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 8545c56..0a91d9f 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -9,11 +9,7 @@ import "MetadataViews" /// which the update can be performed. The @Delegatee resource can receive Capabilities to the @Updater resource and /// can perform the update on behalf of the @Updater resource's owner. /// -/// At the time of this writing, failed updates are not handled gracefully and will result in the halted iteration, but -/// recent conversations point to the possibility of amending the AuthAccount.Contract API to allow for a graceful -/// recovery from failed updates. If this method is not added, we'll want to reconsider the approach in favor of a -/// single update() call per transaction. -/// See the following issue for more info: https://github.com/onflow/cadence/issues/2700 +/// For more info, see FLIP 179: https://github.com/onflow/flips/blob/main/application/20230809-staged-contract-updates.md /// access(all) contract StagedContractUpdates { @@ -21,7 +17,7 @@ access(all) contract StagedContractUpdates { access(all) let inboxHostCapabilityNamePrefix: String /// Common update boundary for those coordinating with contract account-managed Delegatee, enabling opt-in - /// coordinated contract updates + /// Flow coordinated contract updates access(all) var blockUpdateBoundary: UInt64 /* --- Canonical Paths --- */ @@ -35,12 +31,16 @@ access(all) contract StagedContractUpdates { /* --- Events --- */ // + /// Event emitted when the contract block update boundary is updated access(all) event ContractBlockUpdateBoundaryUpdated(old: UInt64?, new: UInt64) + /// Event emitted when an Updater is created access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) + /// Event emitted when an Updater is updated access(all) event UpdaterUpdated( updaterUUID: UInt64, updaterAddress: Address?, blockUpdateBoundary: UInt64, + stageUpdated: Int, updatedAddresses: [Address], updatedContracts: [String], failedAddresses: [Address], @@ -214,17 +214,21 @@ access(all) contract StagedContractUpdates { if self.updateComplete { return true } else if getCurrentBlock().height < self.blockUpdateBoundary { - // Return nil to indicate we're not yet at the update boundary + // Return nil to indicate we're not yet at the update boundary for this Updater return nil } + let currentStage = self.currentDeploymentStage + self.currentDeploymentStage = self.currentDeploymentStage + 1 + + self.updateComplete = self.currentDeploymentStage == self.deployments.length + let updatedAddresses: [Address] = [] let failedAddresses: [Address] = [] let updatedContracts: [String] = [] let failedContracts: [String] = [] - // Update the contracts as specified in the deployment - for contractUpdate in self.deployments[self.currentDeploymentStage] { + for contractUpdate in self.deployments[currentStage] { if let host = self.hosts[contractUpdate.address]!.borrow() { if host.update(name: contractUpdate.name, code: contractUpdate.code.decodeHex()) == false { failedAddresses.append(contractUpdate.address) @@ -242,22 +246,21 @@ access(all) contract StagedContractUpdates { } if failedContracts.length > 0 { - self.failedDeployments.insert(key: self.currentDeploymentStage, failedContracts) + self.failedDeployments.insert(key: currentStage, failedContracts) } - self.currentDeploymentStage = self.currentDeploymentStage + 1 - self.updateComplete = self.currentDeploymentStage == self.deployments.length - emit UpdaterUpdated( updaterUUID: self.uuid, updaterAddress: self.owner?.address, blockUpdateBoundary: self.blockUpdateBoundary, + stageUpdated: currentStage, updatedAddresses: updatedAddresses, updatedContracts: updatedContracts, failedAddresses: failedAddresses, failedContracts: failedContracts, updateComplete: self.updateComplete ) + return self.updateComplete } @@ -334,14 +337,14 @@ access(all) contract StagedContractUpdates { access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) } - /// Resource that executed delegated updates + /// Resource capable of executed delegated updates via encapsulated Updater Capabilities /// access(all) resource Delegatee : DelegateePublic { - /// Block height at which delegated updates will be performed - /// NOTE: This may differ from the contract's blockUpdateBoundary, enabling flexibility - /// but any Updater not ready when updates are performed will be revoked from the Delegatee + /// Block height at which delegated updates will be performed by this Delegatee + /// NOTE: This may differ from the contract's blockUpdateBoundary, enabling flexibility but any Updaters not + /// ready when updates are performed will be revoked from the Delegatee access(self) let blockUpdateBoundary: UInt64 - /// Track all delegated updaters + /// Mapping of all delegated Updater Capabilities by their UUID access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} init(blockUpdateBoundary: UInt64) { @@ -355,13 +358,13 @@ access(all) contract StagedContractUpdates { return self.delegatedUpdaters[id]?.check() ?? nil } - /// Returns the IDs of the delegated updaters + /// Returns the IDs of delegated Updaters /// access(all) fun getUpdaterIDs(): [UInt64] { return self.delegatedUpdaters.keys } - /// Allows for the delegation of updates to a contract + /// Allows for the delegation of contract updates as defined within the Updater resource /// access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { @@ -375,10 +378,10 @@ access(all) contract StagedContractUpdates { let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! if self.delegatedUpdaters.containsKey(updater.getID()) { - // Upsert if updater already exists + // Upsert if updater Capability already contained self.delegatedUpdaters[updater.getID()] = updaterCap } else { - // Insert if updater does not exist + // Insert if updater Capability not yet contained self.delegatedUpdaters.insert(key: updater.getID(), updaterCap) } emit UpdaterDelegationChanged(updaterUUID: updater.getID(), updaterAddress: updater.owner?.address, delegated: true) @@ -398,7 +401,7 @@ access(all) contract StagedContractUpdates { /// Executes update on the specified Updaters. All updates are attempted, and if the Updater is not yet ready /// to be updated (updater.update() returns nil) or the attempted update is the final staged (updater.update() - /// returns true) the corresponding Updater Capability is removed. + /// returns true), the corresponding Updater Capability is removed. /// access(all) fun update(updaterIDs: [UInt64]) { for id in updaterIDs { @@ -424,7 +427,7 @@ access(all) contract StagedContractUpdates { } } - /// Enables admin removal of a DelegatedUpdater Capability + /// Enables admin removal of a Updater Capability /// access(all) fun removeDelegatedUpdater(id: UInt64) { if !self.delegatedUpdaters.containsKey(id) { @@ -459,7 +462,7 @@ access(all) contract StagedContractUpdates { /* --- Contract Methods --- */ - /// Returns the Capability of the Delegatee associated with this contract + /// Returns a Capability on the Delegatee associated with this contract /// access(all) fun getContractDelegateeCapability(): Capability<&{DelegateePublic}> { let delegateeCap = self.account.getCapability<&{DelegateePublic}>(self.DelegateePublicPath) @@ -507,7 +510,7 @@ access(all) contract StagedContractUpdates { return <- create Host(accountCapability: accountCap) } - /// Returns a new Updater resource + /// Returns a new Updater resource encapsulating the given hosts and deployments /// access(all) fun createNewUpdater( blockUpdateBoundary: UInt64, @@ -519,7 +522,8 @@ access(all) contract StagedContractUpdates { return <- updater } - /// Creates a new Delegatee resource enabling caller to self-host their Delegatee + /// Creates a new Delegatee resource enabling caller to self-host their Delegatee to be executed at or beyond + /// given block update boundary /// access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64): @Delegatee { return <- create Delegatee(blockUpdateBoundary: blockUpdateBoundary) diff --git a/transactions/updater/remove_from_delegatee_as_updater.cdc b/transactions/updater/remove_from_delegatee_as_updater.cdc index 4276a73..f9dc332 100644 --- a/transactions/updater/remove_from_delegatee_as_updater.cdc +++ b/transactions/updater/remove_from_delegatee_as_updater.cdc @@ -27,4 +27,4 @@ transaction { post { self.delegatee.check(id: self.updaterID) == nil: "Updater Capability was not properly removed from Delegatee" } -} \ No newline at end of file +} From 862387b5c9b8f379eaa9b24d821ff0a6f55694df Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:58:53 -0600 Subject: [PATCH 32/34] update remove_delegated_updater enabling iterative removal from provided array --- ...gated_updater.cdc => remove_delegated_updaters.cdc} | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) rename transactions/delegatee/{remove_delegated_updater.cdc => remove_delegated_updaters.cdc} (62%) diff --git a/transactions/delegatee/remove_delegated_updater.cdc b/transactions/delegatee/remove_delegated_updaters.cdc similarity index 62% rename from transactions/delegatee/remove_delegated_updater.cdc rename to transactions/delegatee/remove_delegated_updaters.cdc index 37eae81..8b66336 100644 --- a/transactions/delegatee/remove_delegated_updater.cdc +++ b/transactions/delegatee/remove_delegated_updaters.cdc @@ -1,6 +1,8 @@ import "StagedContractUpdates" -transaction(removeID: UInt64) { +/// Removes Updater Capabilities with given IDs from the signer's Delegatee +/// +transaction(removeIDs: [UInt64]) { let delegatee: &StagedContractUpdates.Delegatee @@ -10,6 +12,8 @@ transaction(removeID: UInt64) { } execute { - self.delegatee.removeDelegatedUpdater(id: removeID) + for id in removeIDs { + self.delegatee.removeDelegatedUpdater(id: id) + } } -} \ No newline at end of file +} From 174ed96ae67d36fcab19a08f48e80eb5ef82149d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:59:30 -0600 Subject: [PATCH 33/34] update CapabilityPath derivation in Host setup & publishing txn --- transactions/host/publish_host_capability.cdc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/transactions/host/publish_host_capability.cdc b/transactions/host/publish_host_capability.cdc index b14361e..33065c1 100644 --- a/transactions/host/publish_host_capability.cdc +++ b/transactions/host/publish_host_capability.cdc @@ -2,14 +2,18 @@ import "StagedContractUpdates" -/// Publishes an Capability on the signer's AuthAccount for the specified recipient +/// Links the signer's AuthAccount and encapsulates in a Host resource, publishing a Host Capability for the specified +/// recipient. This would enable the recipient to execute arbitrary contract updates on the signer's behalf. /// transaction(publishFor: Address) { prepare(signer: AuthAccount) { - let accountCapPrivatePath: PrivatePath = /private/StagedContractUpdatesAccountCap - let hostPrivatePath: PrivatePath = /private/StagedContractUpdatesHost + // Derive paths for AuthAccount & Host Capabilities, identifying the recipient on publishing + let accountCapPrivatePath = PrivatePath( + identifier: "StagedContractUpdatesAccountCap_".concat(signer.address.toString()) + )! + let hostPrivatePath = PrivatePath(identifier: "StagedContractUpdatesHost_".concat(publishFor.toString()))! // Setup Capability on underlying signing host account if !signer.getCapability<&AuthAccount>(accountCapPrivatePath).check() { From 8ed457317dec75624e9b647d0f02c2b02078b9f4 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:00:14 -0600 Subject: [PATCH 34/34] update transaction header comments --- contracts/StagedContractUpdates.cdc | 2 +- transactions/coordinator/set_block_update_boundary.cdc | 2 +- transactions/test/tick_tock.cdc | 3 +++ transactions/updater/remove_from_delegatee_as_updater.cdc | 3 +++ transactions/updater/setup_updater_multi_account.cdc | 7 +++---- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 0a91d9f..1e69c2c 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -549,4 +549,4 @@ access(all) contract StagedContractUpdates { emit ContractBlockUpdateBoundaryUpdated(old: nil, new: blockUpdateBoundary) } -} +} diff --git a/transactions/coordinator/set_block_update_boundary.cdc b/transactions/coordinator/set_block_update_boundary.cdc index 40b412c..a16d653 100644 --- a/transactions/coordinator/set_block_update_boundary.cdc +++ b/transactions/coordinator/set_block_update_boundary.cdc @@ -6,6 +6,6 @@ transaction(newBoundary: UInt64) { prepare(signer: AuthAccount) { signer.borrow<&StagedContractUpdates.Coordinator>(from: StagedContractUpdates.CoordinatorStoragePath) ?.setBlockUpdateBoundary(new: newBoundary) - ?? panic("No Coordinator in found!") + ?? panic("Could not borrow reference to Coordinator!") } } diff --git a/transactions/test/tick_tock.cdc b/transactions/test/tick_tock.cdc index a4fe765..90961ef 100644 --- a/transactions/test/tick_tock.cdc +++ b/transactions/test/tick_tock.cdc @@ -1,3 +1,6 @@ +/// TEST TRANSACTION +/// Used to mock block advancement in test suite +/// transaction { prepare(signer: AuthAccount) { log("Block height incremented to: ".concat(getCurrentBlock().height.toString())) diff --git a/transactions/updater/remove_from_delegatee_as_updater.cdc b/transactions/updater/remove_from_delegatee_as_updater.cdc index f9dc332..15ca38c 100644 --- a/transactions/updater/remove_from_delegatee_as_updater.cdc +++ b/transactions/updater/remove_from_delegatee_as_updater.cdc @@ -1,5 +1,8 @@ import "StagedContractUpdates" +/// Retrieves an Updater Capability from the signer's account, assuming one is pre-configured, and removes it from the +/// Delegatee in the StagedContractUpdates contract account. +/// transaction { let delegatee: &{StagedContractUpdates.DelegateePublic} diff --git a/transactions/updater/setup_updater_multi_account.cdc b/transactions/updater/setup_updater_multi_account.cdc index f80414b..2f6ed62 100644 --- a/transactions/updater/setup_updater_multi_account.cdc +++ b/transactions/updater/setup_updater_multi_account.cdc @@ -1,11 +1,10 @@ -#allowAccountLinking - import "MetadataViews" import "StagedContractUpdates" -/// Configures an Updater resource, assuming signing account is the account with the contract to update. This demos an -/// advanced case where an update deployment involves multiple accounts and contracts. +/// Retrieves Host Capabilities from the contract-hosting accounts and assigns an update deployment in an encapsulating +/// Updater. This demos an advanced case where an update deployment involves a network of dependent contracts across +/// multiple hosting accounts. /// /// NOTES: deploymentConfig is ordered, and the order is used to determine the order of the contracts in the deployment. /// Each entry in the array must be exactly one key-value pair, where the key is the address of the associated contract