diff --git a/packages/lib/contracts/application/AppDirectory.sol b/packages/lib/contracts/application/AppDirectory.sol deleted file mode 100644 index ccd3495c7..000000000 --- a/packages/lib/contracts/application/AppDirectory.sol +++ /dev/null @@ -1,53 +0,0 @@ -pragma solidity ^0.4.24; - -import "./versioning/ImplementationProvider.sol"; -import "./versioning/ImplementationDirectory.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; - -/** - * @title AppDirectory - * @dev Implementation directory with a standard library as a fallback provider. - * @dev If the implementation is not found in the directory, it will search in the standard library. - */ -contract AppDirectory is ImplementationDirectory { - /** - * @dev Emitted when the standard library is changed. - * @param newStdlib Address of the new standard library. - */ - event StdlibChanged(address indexed newStdlib); - - /** - * @dev Provider for standard library implementations. - */ - ImplementationProvider public stdlib; - - /** - * @dev Constructor function. - * @param _stdlib Provider for standard library implementations. - */ - constructor(ImplementationProvider _stdlib) public { - stdlib = _stdlib; - } - - /** - * @dev Returns the implementation address for a given contract name. - * @dev If the implementation is not found in the directory, it will search in the standard library. - * @param contractName Name of the contract. - * @return Address where the contract is implemented, or 0 if it is not found. - */ - function getImplementation(string contractName) public view returns (address) { - address implementation = super.getImplementation(contractName); - if(implementation != address(0)) return implementation; - if(stdlib != address(0)) return stdlib.getImplementation(contractName); - return address(0); - } - - /** - * @dev Sets a new implementation provider for standard library contracts. - * @param _stdlib Standard library implementation provider. - */ - function setStdlib(ImplementationProvider _stdlib) public onlyOwner { - stdlib = _stdlib; - emit StdlibChanged(_stdlib); - } -} diff --git a/packages/lib/contracts/application/BaseApp.sol b/packages/lib/contracts/application/BaseApp.sol index f5b6f7859..08374c99d 100644 --- a/packages/lib/contracts/application/BaseApp.sol +++ b/packages/lib/contracts/application/BaseApp.sol @@ -24,18 +24,22 @@ contract BaseApp is Ownable { } /** - * @dev Abstract function to return the implementation provider. - * @return The implementation provider. + * @dev Abstract function to return the implementation provider for a given package name. + * @param packageName Name of the package to be retrieved. + * @return The implementation provider for the package. */ - function getProvider() internal view returns (ImplementationProvider); + function getProvider(string packageName) public view returns (ImplementationProvider); /** * @dev Returns the implementation address for a given contract name, provided by the `ImplementationProvider`. + * @param packageName Name of the package where the contract is contained. * @param contractName Name of the contract. * @return Address where the contract is implemented. */ - function getImplementation(string contractName) public view returns (address) { - return getProvider().getImplementation(contractName); + function getImplementation(string packageName, string contractName) public view returns (address) { + ImplementationProvider provider = getProvider(packageName); + if (address(provider) == address(0)) return address(0); + return provider.getImplementation(contractName); } /** @@ -66,35 +70,38 @@ contract BaseApp is Ownable { /** * @dev Creates a new proxy for the given contract. + * @param packageName Name of the package where the contract is contained. * @param contractName Name of the contract. * @return Address of the new proxy. */ - function create(string contractName) public returns (AdminUpgradeabilityProxy) { - address implementation = getImplementation(contractName); + function create(string packageName, string contractName) public returns (AdminUpgradeabilityProxy) { + address implementation = getImplementation(packageName, contractName); return factory.createProxy(this, implementation); } /** * @dev Creates a new proxy for the given contract and forwards a function call to it. * This is useful to initialize the proxied contract. + * @param packageName Name of the package where the contract is contained. * @param contractName Name of the contract. * @param data Data to send as msg.data in the low level call. * It should include the signature and the parameters of the function to be called, as described in * https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector-and-argument-encoding. * @return Address of the new proxy. */ - function createAndCall(string contractName, bytes data) payable public returns (AdminUpgradeabilityProxy) { - address implementation = getImplementation(contractName); + function createAndCall(string packageName, string contractName, bytes data) payable public returns (AdminUpgradeabilityProxy) { + address implementation = getImplementation(packageName, contractName); return factory.createProxyAndCall.value(msg.value)(this, implementation, data); } /** * @dev Upgrades a proxy to the newest implementation of a contract. * @param proxy Proxy to be upgraded. + * @param packageName Name of the package where the contract is contained. * @param contractName Name of the contract. */ - function upgrade(AdminUpgradeabilityProxy proxy, string contractName) public onlyOwner { - address implementation = getImplementation(contractName); + function upgrade(AdminUpgradeabilityProxy proxy, string packageName, string contractName) public onlyOwner { + address implementation = getImplementation(packageName, contractName); proxy.upgradeTo(implementation); } @@ -102,13 +109,14 @@ contract BaseApp is Ownable { * @dev Upgrades a proxy to the newest implementation of a contract and forwards a function call to it. * This is useful to initialize the proxied contract. * @param proxy Proxy to be upgraded. + * @param packageName Name of the package where the contract is contained. * @param contractName Name of the contract. * @param data Data to send as msg.data in the low level call. * It should include the signature and the parameters of the function to be called, as described in * https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector-and-argument-encoding. */ - function upgradeAndCall(AdminUpgradeabilityProxy proxy, string contractName, bytes data) payable public onlyOwner { - address implementation = getImplementation(contractName); + function upgradeAndCall(AdminUpgradeabilityProxy proxy, string packageName, string contractName, bytes data) payable public onlyOwner { + address implementation = getImplementation(packageName, contractName); proxy.upgradeToAndCall.value(msg.value)(implementation, data); } } diff --git a/packages/lib/contracts/application/PackagedApp.sol b/packages/lib/contracts/application/PackagedApp.sol deleted file mode 100644 index 304a8706f..000000000 --- a/packages/lib/contracts/application/PackagedApp.sol +++ /dev/null @@ -1,48 +0,0 @@ -pragma solidity ^0.4.24; - -import "./BaseApp.sol"; -import "./versioning/Package.sol"; -import "../upgradeability/UpgradeabilityProxyFactory.sol"; - -/** - * @title PackagedApp - * @dev App for an upgradeable project that can use different versions. - * This is the standard entry point for an upgradeable app. - */ -contract PackagedApp is BaseApp { - /// @dev Package that stores the contract implementation addresses. - Package public package; - /// @dev App version. - string public version; - - /** - * @dev Constructor function. - * @param _package Package that stores the contract implementation addresses. - * @param _version Initial version of the app. - * @param _factory Proxy factory. - */ - constructor(Package _package, string _version, UpgradeabilityProxyFactory _factory) BaseApp(_factory) public { - require(address(_package) != address(0), "Cannot set the package of an app to the zero address"); - require(_package.hasVersion(_version), "The requested version must be registered in the given package"); - package = _package; - version = _version; - } - - /** - * @dev Sets the current version of the application. - * Contract implementations for the given version must already be registered in the package. - * @param newVersion Name of the new version. - */ - function setVersion(string newVersion) public onlyOwner { - require(package.hasVersion(newVersion), "The requested version must be registered in the given package"); - version = newVersion; - } - - /** - * @dev Returns the provider for the current version. - * @return The provider for the current version. - */ - function getProvider() internal view returns (ImplementationProvider) { - return package.getVersion(version); - } -} diff --git a/packages/lib/contracts/application/UnversionedApp.sol b/packages/lib/contracts/application/UnversionedApp.sol index 6339299c8..fc3c7cf46 100644 --- a/packages/lib/contracts/application/UnversionedApp.sol +++ b/packages/lib/contracts/application/UnversionedApp.sol @@ -10,33 +10,50 @@ import "../upgradeability/UpgradeabilityProxyFactory.sol"; */ contract UnversionedApp is BaseApp { /* - * @dev Provider that stores the contract implementation addresses. + * @dev Providers for contract implementation addresses. */ - ImplementationProvider internal provider; + mapping(string => ImplementationProvider) internal providers; + + /** + * @dev Emitted when a provider dependency is changed in the application. + * @param providerName Name of the provider that changed. + * @param implementation Address of the provider associated to the name. + */ + event ProviderChanged(string providerName, address implementation); /** * @dev Constructor function. - * @param _provider Implementation provider. * @param _factory Proxy factory. */ - constructor(ImplementationProvider _provider, UpgradeabilityProxyFactory _factory) BaseApp(_factory) public { - setProvider(_provider); - } + constructor(UpgradeabilityProxyFactory _factory) BaseApp(_factory) public { } /** - * @dev Returns the provider used by the app. + * @dev Returns the provider for a given package name, or zero if not set. + * @param packageName Name of the package to be retrieved. * @return The provider. */ - function getProvider() internal view returns (ImplementationProvider) { - return provider; + function getProvider(string packageName) public view returns (ImplementationProvider) { + return providers[packageName]; } /** * @dev Sets a new implementation provider. - * @param _provider New implementation provider + * @param packageName Name under which the provider is to be registered in the app. + * @param _provider New implementation provider. */ - function setProvider(ImplementationProvider _provider) public onlyOwner { + function setProvider(string packageName, ImplementationProvider _provider) public onlyOwner { require(address(_provider) != address(0), "Cannot set the implementation provider of an app to the zero address"); - provider = _provider; + providers[packageName] = _provider; + emit ProviderChanged(packageName, _provider); + } + + /** + * @dev Unsets an existing provider. Reverts if the provider does not exist. + * @param packageName Name of the provider to be removed. + */ + function unsetProvider(string packageName) public onlyOwner { + require(providers[packageName] != address(0), "Provider to unset not found"); + delete providers[packageName]; + emit ProviderChanged(packageName, address(0)); } } diff --git a/packages/lib/contracts/application/VersionedApp.sol b/packages/lib/contracts/application/VersionedApp.sol new file mode 100644 index 000000000..fcd7f6f58 --- /dev/null +++ b/packages/lib/contracts/application/VersionedApp.sol @@ -0,0 +1,78 @@ +pragma solidity ^0.4.24; + +import "./BaseApp.sol"; +import "./versioning/Package.sol"; +import "../upgradeability/UpgradeabilityProxyFactory.sol"; + +/** + * @title VersionedApp + * @dev App for an upgradeable project that can use different versions from packages. + * This is the standard entry point for an upgradeable app. + */ +contract VersionedApp is BaseApp { + + struct ProviderInfo { + Package package; + string version; + } + + mapping(string => ProviderInfo) internal providers; + + /** + * @dev Emitted when a package dependency is changed in the application. + * @param providerName Name of the package that changed. + * @param package Address of the package associated to the name. + * @param version Version of the package in use. + */ + event PackageChanged(string providerName, address package, string version); + + /** + * @dev Constructor function. + * @param _factory Proxy factory. + */ + constructor(UpgradeabilityProxyFactory _factory) BaseApp(_factory) public { } + + /** + * @dev Returns the provider for a given package name, or zero if not set. + * @param packageName Name of the package to be retrieved. + * @return The provider. + */ + function getProvider(string packageName) public view returns (ImplementationProvider) { + ProviderInfo storage info = providers[packageName]; + if (address(info.package) == address(0)) return ImplementationProvider(0); + return info.package.getVersion(info.version); + } + + /** + * @dev Returns information on a package given its name. + * @param packageName Name of the package to be queried. + * @return A tuple with the package address and pinned version given a package name, or zero if not set + */ + function getPackage(string packageName) public view returns (Package, string) { + ProviderInfo storage info = providers[packageName]; + return (info.package, info.version); + } + + /** + * @dev Sets a package in a specific version as a dependency for this application. + * Requires the version to be present in the package. + * @param packageName Name of the package to set or overwrite. + * @param package Address of the package to register. + * @param version Version of the package to use in this application. + */ + function setPackage(string packageName, Package package, string version) public onlyOwner { + require(package.hasVersion(version), "The requested version must be registered in the given package"); + providers[packageName] = ProviderInfo(package, version); + emit PackageChanged(packageName, package, version); + } + + /** + * @dev Unsets a package given its name. + * Reverts if the package is not set in the application. + * @param packageName Name of the package to remove. + */ + function unsetPackage(string packageName) public onlyOwner { + require(address(providers[packageName].package) != address(0), "Package to unset not found"); + delete providers[packageName]; + } +} diff --git a/packages/lib/contracts/application/versioning/ImplementationProvider.sol b/packages/lib/contracts/application/versioning/ImplementationProvider.sol index f1497b8fa..fabc9a4fe 100644 --- a/packages/lib/contracts/application/versioning/ImplementationProvider.sol +++ b/packages/lib/contracts/application/versioning/ImplementationProvider.sol @@ -12,3 +12,4 @@ interface ImplementationProvider { */ function getImplementation(string contractName) public view returns (address); } + diff --git a/packages/lib/contracts/mocks/DummyImplementation.sol b/packages/lib/contracts/mocks/DummyImplementation.sol index 72a266a52..d7e849c14 100644 --- a/packages/lib/contracts/mocks/DummyImplementation.sol +++ b/packages/lib/contracts/mocks/DummyImplementation.sol @@ -9,7 +9,7 @@ contract DummyImplementation { string public text; uint256[] public values; - function initialize(uint256 _value) public { + function initialize(uint256 _value) payable public { value = _value; } @@ -33,7 +33,7 @@ contract DummyImplementation { } contract DummyImplementationV2 is DummyImplementation { - function migrate(uint256 newVal) public { + function migrate(uint256 newVal) payable public { value = newVal; } diff --git a/packages/lib/src/app/App.js b/packages/lib/src/app/App.js deleted file mode 100644 index 4a7f5db11..000000000 --- a/packages/lib/src/app/App.js +++ /dev/null @@ -1,175 +0,0 @@ -'use strict' - -import Logger from '../utils/Logger' -import Contracts from '../utils/Contracts' -import decodeLogs from '../helpers/decodeLogs' -import encodeCall from '../helpers/encodeCall' -import copyContract from '../helpers/copyContract' -import { sendTransaction } from '../utils/Transactions' - -import AppProvider from './AppProvider' -import AppDeployer from './AppDeployer' - -const log = new Logger('App') -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - -export default class App { - - static async fetch(address, txParams = {}) { - const provider = new AppProvider(txParams) - return await provider.from(address) - } - - static async deploy(version, txParams = {}) { - const deployer = new AppDeployer(txParams) - return await deployer.deploy(version) - } - - static async deployWithStdlib(version, stdlibAddress, txParams = {}) { - const deployer = new AppDeployer(txParams) - return await deployer.deployWithStdlib(version, stdlibAddress) - } - - constructor(_app, factory, appDirectory, _package, version, txParams = {}) { - this._app = _app - this.factory = factory - this.package = _package - this.version = version - this.directory = appDirectory - this.txParams = txParams - } - - get address() { - return this._app.address - } - - async currentStdlib() { - return this.directory.stdlib() - } - - async hasStdlib() { - return (await this.currentStdlib()) !== ZERO_ADDRESS - } - - async getImplementation(contractName) { - return this._app.getImplementation(contractName) - } - - async getProxyImplementation(proxyAddress) { - return this._app.getProxyImplementation(proxyAddress, this.txParams) - } - - async setImplementation(contractClass, contractName) { - return this.package.setImplementation(this.version, contractClass, contractName) - } - - async unsetImplementation(contractName) { - await this.directory.unsetImplementation(contractName, this.txParams) - } - - async setStdlib(stdlibAddress = 0x0) { - return this.directory.setStdlib(stdlibAddress) - } - - async newVersion(version, stdlibAddress = 0x0) { - const directory = await this.package.newVersion(version, stdlibAddress) - await sendTransaction(this._app.setVersion, [version], this.txParams) - log.info(`Version ${version} set in app.`) - this.directory = directory - this.version = version - } - - async changeProxyAdmin(proxyAddress, newAdmin) { - log.info(`Changing admin for proxy ${proxyAddress} to ${newAdmin}...`) - await sendTransaction(this._app.changeProxyAdmin, [proxyAddress, newAdmin], this.txParams) - log.info(` Admin for proxy ${proxyAddress} set to ${newAdmin}`) - } - - async createContract(contractClass, contractName, initMethodName, initArgs) { - if (!contractName) contractName = contractClass.contractName; - const instance = await this._copyContract(contractName, contractClass) - await this._initNonUpgradeableInstance(instance, contractClass, contractName, initMethodName, initArgs) - return instance - } - - async createProxy(contractClass, contractName, initMethodName, initArgs) { - if (!contractName) contractName = contractClass.contractName; - const { receipt } = typeof(initArgs) === 'undefined' - ? await this._createProxy(contractName) - : await this._createProxyAndCall(contractClass, contractName, initMethodName, initArgs) - - log.info(`TX receipt received: ${receipt.transactionHash}`) - const UpgradeabilityProxyFactory = Contracts.getFromLib('UpgradeabilityProxyFactory') - const logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - const address = logs.find(l => l.event === 'ProxyCreated').args.proxy - log.info(`${contractName} proxy: ${address}`) - return new contractClass(address) - } - - async upgradeProxy(proxyAddress, contractClass, contractName, initMethodName, initArgs) { - if (!contractName) contractName = contractClass.contractName; - const { receipt } = typeof(initArgs) === 'undefined' - ? await this._upgradeProxy(proxyAddress, contractName) - : await this._upgradeProxyAndCall(proxyAddress, contractClass, contractName, initMethodName, initArgs) - log.info(`TX receipt received: ${receipt.transactionHash}`) - } - - async _createProxy(contractName) { - log.info(`Creating ${contractName} proxy without initializing...`) - return sendTransaction(this._app.create, [contractName], this.txParams) - } - - async _createProxyAndCall(contractClass, contractName, initMethodName, initArgs) { - const { initMethod, callData } = this._buildInitCallData(contractClass, initMethodName, initArgs) - log.info(`Creating ${contractName} proxy and calling ${this._callInfo(initMethod, initArgs)}`) - return sendTransaction(this._app.createAndCall, [contractName, callData], this.txParams) - } - - async _upgradeProxy(proxyAddress, contractName) { - log.info(`Upgrading ${contractName} proxy without running migrations...`) - return sendTransaction(this._app.upgrade, [proxyAddress, contractName], this.txParams) - } - - async _upgradeProxyAndCall(proxyAddress, contractClass, contractName, initMethodName, initArgs) { - const initMethod = this._validateInitMethod(contractClass, initMethodName, initArgs) - const initArgTypes = initMethod.inputs.map(input => input.type) - log.info(`Upgrading ${contractName} proxy and calling ${this._callInfo(initMethod, initArgs)}...`) - const callData = encodeCall(initMethodName, initArgTypes, initArgs) - return sendTransaction(this._app.upgradeAndCall, [proxyAddress, contractName, callData], this.txParams) - } - - async _copyContract(contractName, contractClass) { - log.info(`Creating new non-upgradeable instance of ${contractName}...`) - const implementation = await this.getImplementation(contractName) - const instance = await copyContract(contractClass, implementation, this.txParams) - log.info(`${contractName} instance created at ${instance.address}`) - return instance; - } - - async _initNonUpgradeableInstance(instance, contractClass, contractName, initMethodName, initArgs) { - if (typeof(initArgs) !== 'undefined') { - // this could be front-run, waiting for new initializers model - const {initMethod, callData} = this._buildInitCallData(contractClass, initMethodName, initArgs) - log.info(`Initializing ${contractName} instance at ${instance.address} by calling ${this._callInfo(initMethod, initArgs)}`) - await instance.sendTransaction(Object.assign({}, this.txParams, {data: callData})) - } - } - - _buildInitCallData(contractClass, initMethodName, initArgs) { - const initMethod = this._validateInitMethod(contractClass, initMethodName, initArgs) - const initArgTypes = initMethod.inputs.map(input => input.type) - const callData = encodeCall(initMethodName, initArgTypes, initArgs) - return { initMethod, callData } - } - - _validateInitMethod(contractClass, initMethodName, initArgs) { - const initMethod = contractClass.abi.find(fn => fn.name === initMethodName && fn.inputs.length === initArgs.length) - if (!initMethod) throw Error(`Could not find initialize method '${initMethodName}' with ${initArgs.length} arguments in contract class`) - return initMethod - } - - _callInfo(initMethod, initArgs) { - const args = initMethod.inputs.map((input, index) => ` - ${input.name} (${input.type}): ${JSON.stringify(initArgs[index])}`) - return `${initMethod.name} with: \n${args.join('\n')}` - } -} diff --git a/packages/lib/src/app/AppDeployer.js b/packages/lib/src/app/AppDeployer.js deleted file mode 100644 index 276fcbefb..000000000 --- a/packages/lib/src/app/AppDeployer.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import Logger from '../utils/Logger' -import Contracts from '../utils/Contracts' -import { deploy, sendTransaction } from '../utils/Transactions' - -import App from './App' -import Package from '../package/Package' - -const log = new Logger('AppDeployer') - -export default class AppDeployer { - constructor(txParams = {}) { - this.txParams = txParams - } - - async deploy(version) { - return this.deployWithStdlib(version, 0x0) - } - - async deployWithStdlib(version, stdlibAddress) { - await this.createFactory() - await this.createPackage() - await this.addVersion(version, stdlibAddress) - await this.createApp(version) - return new App(this.packagedApp, this.factory, this.appDirectory, this.package, this.version, this.txParams) - } - - async createApp(version) { - log.info('Deploying new PackagedApp...') - const PackagedApp = Contracts.getFromLib('PackagedApp') - this.packagedApp = await deploy(PackagedApp, [this.package.address, version, this.factory.address], this.txParams) - log.info(`Deployed PackagedApp ${this.packagedApp.address}`) - } - - async createFactory() { - log.info('Deploying new UpgradeabilityProxyFactory...') - const UpgradeabilityProxyFactory = Contracts.getFromLib('UpgradeabilityProxyFactory') - this.factory = await deploy(UpgradeabilityProxyFactory, [], this.txParams) - log.info(`Deployed UpgradeabilityProxyFactory ${this.factory.address}`) - } - - async createPackage() { - this.package = await Package.deploy(this.txParams) - } - - async addVersion(version, stdlibAddress) { - this.version = version - this.appDirectory = await this.package.newVersion(version, stdlibAddress) - } -} diff --git a/packages/lib/src/app/AppProvider.js b/packages/lib/src/app/AppProvider.js deleted file mode 100644 index dffcf0fc2..000000000 --- a/packages/lib/src/app/AppProvider.js +++ /dev/null @@ -1,40 +0,0 @@ -import App from './App' -import Package from '../package/Package' -import Contracts from '../utils/Contracts' -import AppDirectory from '../directory/AppDirectory' - -export default class AppProvider { - constructor(txParams = {}) { - this.txParams = txParams - } - - async from(address) { - this._fetchPackagedApp(address) - await this._fetchFactory() - await this._fetchPackage() - await this._fetchAppDirectory() - return new App(this.packagedApp, this.factory, this.appDirectory, this.package, this.version, this.txParams); - } - - _fetchPackagedApp(address) { - const PackagedApp = Contracts.getFromLib('PackagedApp') - this.packagedApp = new PackagedApp(address) - } - - async _fetchAppDirectory() { - this.version = await this.packagedApp.version() - const appDirectory = await this.package.getDirectory(this.version) - this.appDirectory = AppDirectory.fetch(appDirectory.address, this.txParams) - } - - async _fetchPackage() { - const packageAddress = await this.packagedApp.package() - this.package = Package.fetch(packageAddress, this.txParams) - } - - async _fetchFactory() { - const UpgradeabilityProxyFactory = Contracts.getFromLib('UpgradeabilityProxyFactory') - const factoryAddress = await this.packagedApp.factory() - this.factory = new UpgradeabilityProxyFactory(factoryAddress) - } -} diff --git a/packages/lib/src/app/BaseApp.js b/packages/lib/src/app/BaseApp.js new file mode 100644 index 000000000..299bd60bd --- /dev/null +++ b/packages/lib/src/app/BaseApp.js @@ -0,0 +1,156 @@ +'use strict' + +import Logger from '../utils/Logger' +import Contracts from '../utils/Contracts' +import decodeLogs from '../helpers/decodeLogs' +import encodeCall from '../helpers/encodeCall' +import copyContract from '../helpers/copyContract' +import { deploy as deployContract, sendTransaction } from '../utils/Transactions' + +import UpgradeabilityProxyFactory from '../factory/UpgradeabilityProxyFactory'; +import FreezableImplementationDirectory from '../directory/FreezableImplementationDirectory'; +import { isZeroAddress } from '../utils/Addresses'; + +const log = new Logger('App') + +export default class BaseApp { + + static async fetch(address, txParams = {}) { + const appContract = await this.getContractClass().at(address) + return new this(appContract, txParams) + } + + static async deploy(txParams = {}) { + const factory = await UpgradeabilityProxyFactory.deploy(txParams) + log.info('Deploying new App...') + const appContract = await deployContract(this.getContractClass(), [factory.address], txParams) + log.info(`Deployed App at ${appContract.address}`) + return new this(appContract, txParams) + } + + static getContractClass() { + throw Error("Unimplemented") + } + + constructor(appContract, txParams = {}) { + this.appContract = appContract + this.txParams = txParams + } + + get address() { + return this.appContract.address + } + + get contract() { + return this.appContract + } + + async getImplementation(packageName, contractName) { + return this.appContract.getImplementation(packageName, contractName) + } + + async getProxyImplementation(proxyAddress) { + return this.appContract.getProxyImplementation(proxyAddress, this.txParams) + } + + async hasProvider(name) { + return (await this.getProvider(name) != null); + } + + async getProvider(name, providerClass = FreezableImplementationDirectory) { + const address = await this.appContract.getProvider(name) + if (isZeroAddress(address)) return null + return await providerClass.fetch(address, this.txParams) + } + + async changeProxyAdmin(proxyAddress, newAdmin) { + log.info(`Changing admin for proxy ${proxyAddress} to ${newAdmin}...`) + await sendTransaction(this.appContract.changeProxyAdmin, [proxyAddress, newAdmin], this.txParams) + log.info(`Admin for proxy ${proxyAddress} set to ${newAdmin}`) + } + + async createContract(contractClass, packageName, contractName, initMethodName, initArgs) { + const instance = await this._copyContract(packageName, contractName, contractClass) + await this._initNonUpgradeableInstance(instance, contractClass, packageName, contractName, initMethodName, initArgs) + return instance + } + + async createProxy(contractClass, packageName, contractName, initMethodName, initArgs) { + const { receipt } = typeof(initArgs) === 'undefined' + ? await this._createProxy(packageName, contractName) + : await this._createProxyAndCall(contractClass, packageName, contractName, initMethodName, initArgs) + + log.info(`TX receipt received: ${receipt.transactionHash}`) + const UpgradeabilityProxyFactory = Contracts.getFromLib('UpgradeabilityProxyFactory') + const logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + const address = logs.find(l => l.event === 'ProxyCreated').args.proxy + log.info(`${packageName} ${contractName} proxy: ${address}`) + return new contractClass(address) + } + + async upgradeProxy(proxyAddress, contractClass, packageName, contractName, initMethodName, initArgs) { + const { receipt } = typeof(initArgs) === 'undefined' + ? await this._upgradeProxy(proxyAddress, packageName, contractName) + : await this._upgradeProxyAndCall(proxyAddress, contractClass, packageName, contractName, initMethodName, initArgs) + log.info(`TX receipt received: ${receipt.transactionHash}`) + } + + async _createProxy(packageName, contractName) { + log.info(`Creating ${packageName} ${contractName} proxy without initializing...`) + return sendTransaction(this.appContract.create, [packageName, contractName], this.txParams) + } + + async _createProxyAndCall(contractClass, packageName, contractName, initMethodName, initArgs) { + const { initMethod, callData } = this._buildInitCallData(contractClass, initMethodName, initArgs) + log.info(`Creating ${packageName} ${contractName} proxy and calling ${this._callInfo(initMethod, initArgs)}`) + return sendTransaction(this.appContract.createAndCall, [packageName, contractName, callData], this.txParams) + } + + async _upgradeProxy(proxyAddress, packageName, contractName) { + log.info(`Upgrading ${packageName} ${contractName} proxy without running migrations...`) + return sendTransaction(this.appContract.upgrade, [proxyAddress, packageName, contractName], this.txParams) + } + + async _upgradeProxyAndCall(proxyAddress, contractClass, packageName, contractName, initMethodName, initArgs) { + const initMethod = this._validateInitMethod(contractClass, initMethodName, initArgs) + const initArgTypes = initMethod.inputs.map(input => input.type) + log.info(`Upgrading ${packageName} ${contractName} proxy and calling ${this._callInfo(initMethod, initArgs)}...`) + const callData = encodeCall(initMethodName, initArgTypes, initArgs) + return sendTransaction(this.appContract.upgradeAndCall, [proxyAddress, packageName, contractName, callData], this.txParams) + } + + async _copyContract(packageName, contractName, contractClass) { + log.info(`Creating new non-upgradeable instance of ${packageName} ${contractName}...`) + const implementation = await this.getImplementation(packageName, contractName) + const instance = await copyContract(contractClass, implementation, this.txParams) + log.info(`${packageName} ${contractName} instance created at ${instance.address}`) + return instance; + } + + async _initNonUpgradeableInstance(instance, contractClass, packageName, contractName, initMethodName, initArgs) { + if (typeof(initArgs) !== 'undefined') { + // this could be front-run, waiting for new initializers model + const {initMethod, callData} = this._buildInitCallData(contractClass, initMethodName, initArgs) + log.info(`Initializing ${packageName} ${contractName} instance at ${instance.address} by calling ${this._callInfo(initMethod, initArgs)}`) + await instance.sendTransaction(Object.assign({}, this.txParams, {data: callData})) + } + } + + _buildInitCallData(contractClass, initMethodName, initArgs) { + const initMethod = this._validateInitMethod(contractClass, initMethodName, initArgs) + const initArgTypes = initMethod.inputs.map(input => input.type) + const callData = encodeCall(initMethodName, initArgTypes, initArgs) + return { initMethod, callData } + } + + _validateInitMethod(contractClass, initMethodName, initArgs) { + const initMethod = contractClass.abi.find(fn => fn.name === initMethodName && fn.inputs.length === initArgs.length) + if (!initMethod) throw Error(`Could not find initialize method '${initMethodName}' with ${initArgs.length} arguments in contract class`) + return initMethod + } + + _callInfo(initMethod, initArgs) { + const args = initMethod.inputs.map((input, index) => ` - ${input.name} (${input.type}): ${JSON.stringify(initArgs[index])}`) + return `${initMethod.name} with: \n${args.join('\n')}` + } +} diff --git a/packages/lib/src/app/UnversionedApp.js b/packages/lib/src/app/UnversionedApp.js new file mode 100644 index 000000000..3bf5b369f --- /dev/null +++ b/packages/lib/src/app/UnversionedApp.js @@ -0,0 +1,21 @@ +'use strict' + +import BaseApp from "./BaseApp"; +import { toAddress } from "../utils/Addresses"; +import Contracts from "../utils/Contracts"; +import { sendTransaction } from "../utils/Transactions"; + +export default class UnversionedApp extends BaseApp { + + static getContractClass() { + return Contracts.getFromLib('UnversionedApp') + } + + async setProvider(name, providerAddress) { + return await sendTransaction(this.appContract.setProvider, [name, toAddress(providerAddress)], this.txParams) + } + + async unsetProvider(name) { + return await sendTransaction(this.appContract.unsetProvider, [name], this.txParams) + } +} diff --git a/packages/lib/src/app/VersionedApp.js b/packages/lib/src/app/VersionedApp.js new file mode 100644 index 000000000..561727c0a --- /dev/null +++ b/packages/lib/src/app/VersionedApp.js @@ -0,0 +1,33 @@ +'use strict' + +import BaseApp from "./BaseApp"; +import Package from "../package/Package"; +import { toAddress, isZeroAddress } from "../utils/Addresses"; +import Contracts from "../utils/Contracts"; +import { sendTransaction } from "../utils/Transactions"; + +export default class VersionedApp extends BaseApp { + + static getContractClass() { + return Contracts.getFromLib('VersionedApp') + } + + async getPackage(name) { + const [address, version] = await this.appContract.getPackage(name) + const thepackage = await Package.fetch(address, this.txParams) + return { package: thepackage, version } + } + + async hasPackage(name) { + const [address, _version] = await this.appContract.getPackage(name) + return !isZeroAddress(address) + } + + async setPackage(name, packageAddress, version) { + return await sendTransaction(this.appContract.setPackage, [name, toAddress(packageAddress), version], this.txParams) + } + + async unsetPackage(name) { + return await sendTransaction(this.appContract.unsetPackage, [name], this.txParams) + } +} diff --git a/packages/lib/src/directory/AppDirectory.js b/packages/lib/src/directory/AppDirectory.js deleted file mode 100644 index 14e31108a..000000000 --- a/packages/lib/src/directory/AppDirectory.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import Logger from '../utils/Logger' -import { sendTransaction } from '../utils/Transactions' - -import AppDirectoryDeployer from './AppDirectoryDeployer' -import ImplementationDirectory from './ImplementationDirectory' -import AppDirectoryProvider from './AppDirectoryProvider' - -export default class AppDirectory extends ImplementationDirectory { - - static fetch(address, txParams = {}) { - const provider = new AppDirectoryProvider(txParams) - return provider.fetch(address) - } - - static async deploy(stdlibAddress = 0x0, txParams = {}) { - const deployer = new AppDirectoryDeployer(txParams) - return deployer.deploy(stdlibAddress) - } - - constructor(directory, txParams = {}) { - const log = new Logger('AppDirectory'); - super(directory, txParams, log) - } - - async stdlib() { - return this.directory.stdlib() - } - - async setStdlib(stdlibAddress) { - this.log.info(`Setting stdlib ${stdlibAddress}...`) - await sendTransaction(this.directory.setStdlib, [stdlibAddress], this.txParams) - this.log.info(`Stdlib ${stdlibAddress} set`) - return stdlibAddress - } -} diff --git a/packages/lib/src/directory/AppDirectoryDeployer.js b/packages/lib/src/directory/AppDirectoryDeployer.js deleted file mode 100644 index 167b22b1d..000000000 --- a/packages/lib/src/directory/AppDirectoryDeployer.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -import Logger from '../utils/Logger' -import Contracts from '../utils/Contracts' -import { deploy } from '../utils/Transactions' - -import AppDirectory from './AppDirectory' - -const log = new Logger('AppDirectoryDeployer') - -export default class AppDirectoryDeployer { - constructor(txParams = {}) { - this.txParams = txParams - } - - async deploy(stdlibAddress = 0x0) { - await this._deployAppDirectory(stdlibAddress) - return new AppDirectory(this.directory, this.txParams) - } - - async _deployAppDirectory(stdlibAddress) { - log.info('Deploying new AppDirectory...') - const AppDirectory = Contracts.getFromLib('AppDirectory') - this.directory = await deploy(AppDirectory, [stdlibAddress], this.txParams) - log.info(`App directory created at ${this.directory.address}`) - } -} diff --git a/packages/lib/src/directory/AppDirectoryProvider.js b/packages/lib/src/directory/AppDirectoryProvider.js deleted file mode 100644 index 4273d4baa..000000000 --- a/packages/lib/src/directory/AppDirectoryProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -import Contracts from '../utils/Contracts' -import AppDirectory from './AppDirectory' - -export default class AppDirectoryProvider { - constructor(txParams = {}) { - this.txParams = txParams - } - - fetch(address) { - this._fetchAppDirectory(address) - return new AppDirectory(this.directory, this.txParams) - } - - _fetchAppDirectory(address) { - const AppDirectoryContract = Contracts.getFromLib('AppDirectory') - this.directory = new AppDirectoryContract(address) - } -} diff --git a/packages/lib/src/directory/BaseImplementationDirectory.js b/packages/lib/src/directory/BaseImplementationDirectory.js new file mode 100644 index 000000000..9afc963f7 --- /dev/null +++ b/packages/lib/src/directory/BaseImplementationDirectory.js @@ -0,0 +1,72 @@ +import Logger from '../utils/Logger' +import { sendTransaction } from '../utils/Transactions' + +import ImplementationDirectoryDeployer from './ImplementationDirectoryDeployer' + +export default class BaseImplementationDirectory { + + static deployLocal(contracts = [], txParams = {}) { + return this.deploy(null, contracts, txParams); + } + + static async deployDependency(dependencyName, contracts = [], txParams = {}) { + return this.deploy(dependencyName, contracts, txParams); + } + + static async deploy(dependencyName, contracts = [], txParams = {}) { + const deployer = new ImplementationDirectoryDeployer(this.getContractClass(), txParams) + const directory = await (dependencyName + ? deployer.deployDependency(dependencyName, contracts) + : deployer.deployLocal(contracts)) + return new this(directory, txParams) + } + + static async fetch(address, txParams = {}) { + const klazz = this.getContractClass(); + const directory = await klazz.at(address); + return new this(directory, txParams); + } + + static getContractClass() { + throw Error("Unimplemented method getContractClass()") + } + + constructor(directory, txParams = {}, log = new Logger('BaseImplementationDirectory')) { + this.directoryContract = directory + this.txParams = txParams + this.log = log + } + + get contract() { + return this.directoryContract + } + + get address() { + return this.directoryContract.address + } + + async owner() { + return this.directoryContract.owner(this.txParams) + } + + async isFrozen() { + return false + } + + async getImplementation(contractName) { + if (!contractName) throw Error('Contract name is required to retrieve an implementation') + return await this.directoryContract.getImplementation(contractName, this.txParams) + } + + async setImplementation(contractName, implementationAddress) { + this.log.info(`Setting ${contractName} implementation ${implementationAddress}...`) + await sendTransaction(this.directoryContract.setImplementation, [contractName, implementationAddress], this.txParams) + this.log.info(`Implementation set ${implementationAddress}`) + } + + async unsetImplementation(contractName) { + this.log.info(`Unsetting ${contractName} implementation...`) + await sendTransaction(this.directoryContract.unsetImplementation, [contractName], this.txParams) + this.log.info(`${contractName} implementation unset`) + } +} diff --git a/packages/lib/src/directory/FreezableImplementationDirectory.js b/packages/lib/src/directory/FreezableImplementationDirectory.js index 2ab3bec11..6d922869a 100644 --- a/packages/lib/src/directory/FreezableImplementationDirectory.js +++ b/packages/lib/src/directory/FreezableImplementationDirectory.js @@ -1,35 +1,25 @@ import Logger from '../utils/Logger' import { sendTransaction } from '../utils/Transactions' +import BaseImplementationDirectory from './BaseImplementationDirectory' +import Contracts from '../utils/Contracts'; -import ImplementationDirectory from './ImplementationDirectory' -import ImplementationDirectoryDeployer from './ImplementationDirectoryDeployer' +export default class FreezableImplementationDirectory extends BaseImplementationDirectory { -export default class FreezableImplementationDirectory extends ImplementationDirectory { - - static async deployLocal(contracts, txParams = {}) { - const deployer = ImplementationDirectoryDeployer.freezable(txParams) - const directory = await deployer.deployLocal(contracts) - return new FreezableImplementationDirectory(directory, txParams) - } - - static async deployDependency(dependencyName, contracts, txParams = {}) { - const deployer = ImplementationDirectoryDeployer.freezable(txParams); - const directory = await deployer.deployDependency(dependencyName, contracts) - return new FreezableImplementationDirectory(directory, txParams) + static getContractClass() { + return Contracts.getFromLib('FreezableImplementationDirectory') } constructor(directory, txParams = {}) { - const log = new Logger('FreezableImplementationDirectory') - super(directory, txParams, log) + super(directory, txParams, new Logger('FreezableImplementationDirectory')) } async freeze() { this.log.info('Freezing implementation directory...') - await sendTransaction(this.directory.freeze, [], this.txParams) + await sendTransaction(this.directoryContract.freeze, [], this.txParams) this.log.info('Frozen') } async isFrozen() { - return await this.directory.frozen() + return await this.directoryContract.frozen() } } diff --git a/packages/lib/src/directory/ImplementationDirectory.js b/packages/lib/src/directory/ImplementationDirectory.js index f690b7276..097af085e 100644 --- a/packages/lib/src/directory/ImplementationDirectory.js +++ b/packages/lib/src/directory/ImplementationDirectory.js @@ -1,49 +1,16 @@ import Logger from '../utils/Logger' -import { sendTransaction } from '../utils/Transactions' -import ImplementationDirectoryDeployer from './ImplementationDirectoryDeployer' +import BaseImplementationDirectory from './BaseImplementationDirectory' +import Contracts from '../utils/Contracts'; -export default class ImplementationDirectory { +export default class ImplementationDirectory extends BaseImplementationDirectory { - static async deployLocal(contracts, txParams = {}) { - const deployer = ImplementationDirectoryDeployer.nonFreezable(txParams) - const directory = await deployer.deployLocal(contracts); - return new ImplementationDirectory(directory, txParams) + static getContractClass() { + return Contracts.getFromLib('ImplementationDirectory') } - static async deployDependency(dependencyName, contracts, txParams = {}) { - const deployer = ImplementationDirectoryDeployer.nonFreezable(txParams) - const directory = await deployer.deployDependency(dependencyName, contracts) - return new ImplementationDirectory(directory, txParams) + constructor(directory, txParams = {}) { + super(directory, txParams, new Logger('ImplementationDirectory')) } - constructor(directory, txParams = {}, log = new Logger('ImplementationDirectory')) { - this.directory = directory - this.txParams = txParams - this.log = log - } - - get address() { - return this.directory.address - } - - async owner() { - return this.directory.owner(this.txParams) - } - - async getImplementation(contractName) { - return await this.directory.getImplementation(contractName, this.txParams) - } - - async setImplementation(contractName, implementationAddress) { - this.log.info(`Setting ${contractName} implementation ${implementationAddress}...`) - await sendTransaction(this.directory.setImplementation, [contractName, implementationAddress], this.txParams) - this.log.info(`Implementation set ${implementationAddress}`) - } - - async unsetImplementation(contractName) { - this.log.info(`Unsetting ${contractName} implementation...`) - await sendTransaction(this.directory.unsetImplementation, [contractName], this.txParams) - this.log.info(`${contractName} implementation unset`) - } } diff --git a/packages/lib/src/directory/ImplementationDirectoryDeployer.js b/packages/lib/src/directory/ImplementationDirectoryDeployer.js index ede05c8a3..c101a83f8 100644 --- a/packages/lib/src/directory/ImplementationDirectoryDeployer.js +++ b/packages/lib/src/directory/ImplementationDirectoryDeployer.js @@ -4,17 +4,9 @@ import { deploy, sendTransaction } from '../utils/Transactions' const log = new Logger('ImplementationDirectoryDeployer') -export default class ImplementationDirectoryDeployer { - static freezable(txParams = {}) { - const contractClass = Contracts.getFromLib('FreezableImplementationDirectory') - return new ImplementationDirectoryDeployer(contractClass, txParams) - } - - static nonFreezable(txParams = {}) { - const contractClass = Contracts.getFromLib('ImplementationDirectory') - return new ImplementationDirectoryDeployer(contractClass, txParams) - } +// TODO: Check whether the deployment with contracts is used from the CLI, and consider removing it or moving to another class (such as project) +export default class ImplementationDirectoryDeployer { constructor(contractClass, txParams = {}) { this.contractClass = contractClass this.txParams = txParams diff --git a/packages/lib/src/directory/ImplementationDirectoryProvider.js b/packages/lib/src/directory/ImplementationDirectoryProvider.js deleted file mode 100644 index c1d878b52..000000000 --- a/packages/lib/src/directory/ImplementationDirectoryProvider.js +++ /dev/null @@ -1,17 +0,0 @@ -import Contracts from '../utils/Contracts' -import ImplementationDirectory from './ImplementationDirectory' -import FreezableImplementationDirectory from './FreezableImplementationDirectory' - -export default class ImplementationDirectoryProvider { - static freezable(address, txParams = {}) { - const contractClass = Contracts.getFromLib('FreezableImplementationDirectory') - const directory = new contractClass(address) - return new FreezableImplementationDirectory(directory, txParams) - } - - static nonFreezable(address, txParams = {}) { - const contractClass = Contracts.getFromLib('ImplementationDirectory') - const directory = new contractClass(address) - return new ImplementationDirectory(directory, txParams) - } -} diff --git a/packages/lib/src/factory/UpgradeabilityProxyFactory.js b/packages/lib/src/factory/UpgradeabilityProxyFactory.js new file mode 100644 index 000000000..90364fd0e --- /dev/null +++ b/packages/lib/src/factory/UpgradeabilityProxyFactory.js @@ -0,0 +1,35 @@ +'use strict' + +import Logger from '../utils/Logger' +import Contracts from '../utils/Contracts' +import { deploy as deployContract } from '../utils/Transactions'; + +const log = new Logger('UpgradeabilityProxyFactory') + +export default class UpgradeabilityProxyFactory { + + static async deploy(txParams = {}) { + log.info('Deploying new UpgradeabilityProxyFactory...') + const factoryContract = await deployContract(Contracts.getFromLib('UpgradeabilityProxyFactory'), [], txParams) + log.info(`Deployed UpgradeabilityProxyFactory ${factoryContract.address}`) + return new this(factoryContract, txParams) + } + + static async fetch(address, txParams = {}) { + const factoryContract = await Contracts.getFromLib('UpgradeabilityProxyFactory').at(address); + return new this(factoryContract, txParams); + } + + constructor(factoryContract, txParams = {}) { + this.factoryContract = factoryContract; + this.txParams = txParams; + } + + get address() { + return this.factoryContract.address; + } + + get contract() { + return this.factoryContract; + } +} \ No newline at end of file diff --git a/packages/lib/src/index.js b/packages/lib/src/index.js index bc2b5cc96..8aef6ec16 100644 --- a/packages/lib/src/index.js +++ b/packages/lib/src/index.js @@ -18,12 +18,13 @@ const assertions = helpers.assertions const assertRevert = helpers.assertRevert // model objects -import App from './app/App' -import Package from './package/Package' -import AppDirectory from './directory/AppDirectory' -import ImplementationDirectory from './directory/ImplementationDirectory' -import FreezableImplementationDirectory from './directory/FreezableImplementationDirectory' +// import App from './app/App' +// import Package from './package/Package' +// import AppDirectory from './directory/AppDirectory' +// import ImplementationDirectory from './directory/ImplementationDirectory' +// import FreezableImplementationDirectory from './directory/FreezableImplementationDirectory' +// TODO: UPDATE me export { version, decodeLogs, @@ -37,9 +38,9 @@ export { Logger, FileSystem, Contracts, - App, - ImplementationDirectory, - FreezableImplementationDirectory, - AppDirectory, - Package, + // App, + // ImplementationDirectory, + // FreezableImplementationDirectory, + // AppDirectory, + // Package, } diff --git a/packages/lib/src/package/Package.js b/packages/lib/src/package/Package.js index 2e7efab5a..379bd17d1 100644 --- a/packages/lib/src/package/Package.js +++ b/packages/lib/src/package/Package.js @@ -1,88 +1,86 @@ import Logger from '../utils/Logger' -import { deploy, sendTransaction } from '../utils/Transactions' - -import PackageProvider from './PackageProvider' -import PackageDeployer from './PackageDeployer' +import { deploy as deployContract, sendTransaction } from '../utils/Transactions' +import FreezableImplementationDirectory from '../directory/FreezableImplementationDirectory'; +import Contracts from '../utils/Contracts'; +import { toAddress, isZeroAddress } from '../utils/Addresses'; const log = new Logger('Package') export default class Package { - static fetch(address, txParams = {}, klass = require('./PackageWithAppDirectories')) { - const provider = new PackageProvider(txParams) - return provider.fetch(address, klass.default) - } - - static async deploy(txParams = {}, klass = require('./PackageWithAppDirectories')) { - const deployer = new PackageDeployer(txParams) - return deployer.deploy(klass.default) + static async fetch(address, txParams = {}, directoryClass = FreezableImplementationDirectory) { + if (isZeroAddress(address)) return null + const Package = Contracts.getFromLib('Package') + const packageContract = await Package.at(address) + return new this(packageContract, txParams, directoryClass) } - static async deployWithFreezableDirectories(txParams = {}) { - return this.deploy(txParams, require('./PackageWithFreezableDirectories')) + static async deploy(txParams = {}, directoryClass = FreezableImplementationDirectory) { + log.info('Deploying new Package...') + const Package = Contracts.getFromLib('Package') + const packageContract = await deployContract(Package, [], txParams) + log.info(`Deployed Package ${packageContract.address}`) + return new this(packageContract, txParams, directoryClass) } - static fetchWithFreezableDirectories(address, txParams = {}) { - return this.fetch(address, txParams, require('./PackageWithFreezableDirectories')) + constructor(packageContract, txParams = {}, directoryClass = FreezableImplementationDirectory) { + this.packageContract = packageContract + this.txParams = txParams + this.directoryClass = directoryClass } - static async deployWithNonFreezableDirectories(txParams = {}) { - return this.deploy(txParams, require('./PackageWithNonFreezableDirectories')) + get contract() { + return this.packageContract } - static fetchWithNonFreezableDirectories(address, txParams = {}) { - return this.fetch(address, txParams, require('./PackageWithNonFreezableDirectories')) + get address() { + return this.packageContract.address } - constructor(_package, txParams = {}) { - this.package = _package - this.txParams = txParams + async hasVersion(version) { + return sendTransaction(this.packageContract.hasVersion, [version], this.txParams) } - get address() { - return this.package.address + async isFrozen(version) { + const directory = await this.getDirectory(version) + return directory.isFrozen() } - async hasVersion(version) { - return sendTransaction(this.package.hasVersion, [version], this.txParams) + async freeze(version) { + const directory = await this.getDirectory(version) + if (!directory.freeze) throw Error("Implementation directory does not support freezing") + return directory.freeze() } async getImplementation(version, contractName) { - return this.package.getImplementation(version, contractName) + return this.packageContract.getImplementation(version, contractName) } - async setImplementation(version, contractClass, contractName) { - log.info(`Setting implementation of ${contractName} in version ${version}...`) - const implementation = await deploy(contractClass, [], this.txParams) + async setImplementation(version, contractName, contractAddress) { const directory = await this.getDirectory(version) - await directory.setImplementation(contractName, implementation.address) - return implementation + await directory.setImplementation(contractName, toAddress(contractAddress)) } async unsetImplementation (version, contractName) { - log.info(`Unsetting implementation of ${contractName} in version ${version}...`) const directory = await this.getDirectory(version) await directory.unsetImplementation(contractName, this.txParams) } - async newVersion(version, stdlibAddress) { + async newVersion(version) { log.info('Adding new version...') - const directory = await this.newDirectory(stdlibAddress) - await sendTransaction(this.package.addVersion, [version, directory.address], this.txParams) + const directory = await this._newDirectory() + await sendTransaction(this.packageContract.addVersion, [version, directory.address], this.txParams) log.info(`Added version ${version}`) return directory } async getDirectory(version) { - const directoryAddress = await this.package.getVersion(version) - return this.wrapImplementationDirectory(directoryAddress) - } - - wrapImplementationDirectory() { - throw Error('Cannot call abstract method wrapImplementationDirectory()') + if (!version) throw Error("Cannot get a directory from a package without specifying a version") + const directoryAddress = await this.packageContract.getVersion(version) + return this.directoryClass.fetch(directoryAddress, this.txParams) } - async newDirectory() { - throw Error('Cannot call abstract method newDirectory()') + async _newDirectory() { + return this.directoryClass.deployLocal([], this.txParams) } } diff --git a/packages/lib/src/package/PackageDeployer.js b/packages/lib/src/package/PackageDeployer.js deleted file mode 100644 index 553be6a04..000000000 --- a/packages/lib/src/package/PackageDeployer.js +++ /dev/null @@ -1,23 +0,0 @@ -import Logger from '../utils/Logger' -import Contracts from '../utils/Contracts' -import { deploy } from '../utils/Transactions' - -const log = new Logger('PackageDeployer') - -export default class PackageDeployer { - constructor(txParams = {}) { - this.txParams = txParams - } - - async deploy(klass) { - await this._createPackage() - return new klass(this.package, this.txParams) - } - - async _createPackage() { - log.info('Deploying new Package...') - const Package = Contracts.getFromLib('Package') - this.package = await deploy(Package, [], this.txParams) - log.info(`Deployed Package ${this.package.address}`) - } -} diff --git a/packages/lib/src/package/PackageProvider.js b/packages/lib/src/package/PackageProvider.js deleted file mode 100644 index 08dd9cbfd..000000000 --- a/packages/lib/src/package/PackageProvider.js +++ /dev/null @@ -1,18 +0,0 @@ -import Package from './Package' -import Contracts from '../utils/Contracts' - -export default class PackageProvider { - constructor(txParams = {}) { - this.txParams = txParams - } - - fetch(address, klass) { - this._fetchPackage(address); - return new klass(this.package, this.txParams) - } - - _fetchPackage(address) { - const Package = Contracts.getFromLib('Package') - this.package = new Package(address) - } -} diff --git a/packages/lib/src/package/PackageWithAppDirectories.js b/packages/lib/src/package/PackageWithAppDirectories.js deleted file mode 100644 index 6a1ad51ca..000000000 --- a/packages/lib/src/package/PackageWithAppDirectories.js +++ /dev/null @@ -1,12 +0,0 @@ -import Package from './Package' -import AppDirectory from '../directory/AppDirectory' - -export default class PackageWithAppDirectories extends Package { - wrapImplementationDirectory(directoryAddress) { - return AppDirectory.fetch(directoryAddress, this.txParams) - } - - async newDirectory(stdlibAddress) { - return AppDirectory.deploy(stdlibAddress, this.txParams) - } -} diff --git a/packages/lib/src/package/PackageWithFreezableDirectories.js b/packages/lib/src/package/PackageWithFreezableDirectories.js deleted file mode 100644 index fc4a0bc51..000000000 --- a/packages/lib/src/package/PackageWithFreezableDirectories.js +++ /dev/null @@ -1,23 +0,0 @@ -import Package from './Package' -import ImplementationDirectoryProvider from '../directory/ImplementationDirectoryProvider' -import FreezableImplementationDirectory from '../directory/FreezableImplementationDirectory' - -export default class PackageWithFreezableDirectories extends Package { - wrapImplementationDirectory(directoryAddress) { - return ImplementationDirectoryProvider.freezable(directoryAddress, this.txParams) - } - - async newDirectory() { - return FreezableImplementationDirectory.deployLocal([], this.txParams) - } - - async isFrozen(version) { - const directory = await this.getDirectory(version) - return await directory.isFrozen() - } - - async freeze(version) { - const directory = await this.getDirectory(version) - await directory.freeze(this.txParams) - } -} diff --git a/packages/lib/src/package/PackageWithNonFreezableDirectories.js b/packages/lib/src/package/PackageWithNonFreezableDirectories.js deleted file mode 100644 index 9d5032063..000000000 --- a/packages/lib/src/package/PackageWithNonFreezableDirectories.js +++ /dev/null @@ -1,13 +0,0 @@ -import Package from './Package' -import ImplementationDirectory from '../directory/ImplementationDirectory' -import ImplementationDirectoryProvider from '../directory/ImplementationDirectoryProvider' - -export default class PackageWithNonFreezableDirectories extends Package { - wrapImplementationDirectory(directoryAddress) { - return ImplementationDirectoryProvider.nonFreezable(directoryAddress, this.txParams) - } - - async newDirectory() { - return ImplementationDirectory.deployLocal([], this.txParams) - } -} diff --git a/packages/lib/src/project/AppProject.js b/packages/lib/src/project/AppProject.js new file mode 100644 index 000000000..2fae4571f --- /dev/null +++ b/packages/lib/src/project/AppProject.js @@ -0,0 +1,78 @@ +import Project from "./Project"; +import VersionedApp from "../app/VersionedApp"; +import Package from "../package/Package"; + +export default class AppProject extends Project { + static async fetch(appAddress, name, txParams) { + const app = await VersionedApp.fetch(appAddress, txParams) + const packageInfo = await app.getPackage(name) + const project = new this(app, name, packageInfo.version, txParams) + project.package = packageInfo.package + return project + } + + static async deploy(name = 'main', version = '0.1.0', txParams = {}) { + const thepackage = await Package.deploy(txParams) + const directory = await thepackage.newVersion(version) + const app = await VersionedApp.deploy(txParams) + await app.setPackage(name, thepackage.address, version) + const project = new this(app, name, version, txParams) + project.directory = directory + project.package = thepackage + return project + } + + constructor(app, name = 'main', version = '0.1.0', txParams = {}) { + super(txParams) + this.app = app + this.name = name + this.version = version + } + + async newVersion(version) { + const directory = await super.newVersion(version) + const thepackage = await this.getProjectPackage() + await this.app.setPackage(this.name, thepackage.address, version) + return directory + } + + async getApp() { + return this.app + } + + async getProjectPackage() { + if (!this.package) { + const packageInfo = await this.app.getPackage(this.name) + this.package = packageInfo.package + } + return this.package + } + + async getCurrentDirectory() { + if (!this.directory) { + this.directory = await this.app.getProvider(this.name) + } + return this.directory + } + + // TODO: Testme + async createContract(contractClass, { packageName, contractName, initMethod, initArgs }) { + if (!contractName) contractName = contractClass.contractName + if (!packageName) packageName = this.name + return this.app.createContract(contractClass, packageName, contractName, initMethod, initArgs) + } + + // TODO: Testme + async createProxy(contractClass, { packageName, contractName, initMethod, initArgs }) { + if (!contractName) contractName = contractClass.contractName + if (!packageName) packageName = this.name + return this.app.createProxy(contractClass, packageName, contractName, initMethod, initArgs) + } + + // TODO: Testme + async upgradeProxy(proxyAddress, contractClass, { packageName, contractName, initMethod, initArgs }) { + if (!contractName) contractName = contractClass.contractName + if (!packageName) packageName = this.name + return this.app.upgradeProxy(proxyAddress, contractClass, packageName, contractName, initMethod, initArgs) + } +} diff --git a/packages/lib/src/project/LibProject.js b/packages/lib/src/project/LibProject.js new file mode 100644 index 000000000..0e8b2697f --- /dev/null +++ b/packages/lib/src/project/LibProject.js @@ -0,0 +1,35 @@ +import Project from "./Project"; +import Package from "../package/Package"; + +export default class LibProject extends Project { + static async fetch(packageAddress, version = '0.1.0', txParams) { + const thepackage = await Package.fetch(packageAddress, txParams) + return new this(thepackage, version, txParams) + } + + static async deploy(version = '0.1.0', txParams = {}) { + const thepackage = await Package.deploy(txParams) + const directory = await thepackage.newVersion(version) + const project = new this(thepackage, txParams) + project.directory = directory + return project + } + + constructor(thepackage, version = '0.1.0', txParams = {}) { + super(txParams) + this.package = thepackage + this.version = version + } + + async getProjectPackage() { + return this.package + } + + async getCurrentDirectory() { + if (!this.directory) { + const thepackage = await this.getProjectPackage() + this.directory = await thepackage.getDirectory(this.version) + } + return this.directory + } +} diff --git a/packages/lib/src/project/Project.js b/packages/lib/src/project/Project.js new file mode 100644 index 000000000..6db71ea20 --- /dev/null +++ b/packages/lib/src/project/Project.js @@ -0,0 +1,42 @@ +import { deploy } from "../utils/Transactions"; +import Logger from "../utils/Logger"; + +const log = new Logger('Project') + +export default class Project { + + constructor(txParams) { + this.txParams = txParams + } + + async newVersion(version) { + const thepackage = await this.getProjectPackage() + const directory = await thepackage.newVersion(version) + this.directory = directory + this.version = version + return directory + } + + async setImplementation(contractClass, contractName) { + log.info(`Setting implementation of ${contractName} in directory...`) + const implementation = await deploy(contractClass, [], this.txParams) + const directory = await this.getCurrentDirectory() + await directory.setImplementation(contractName, implementation.address) + log.info(`Implementation set: ${implementation.address}`) + return implementation + } + + async unsetImplementation(contractName) { + log.info(`Unsetting implementation of ${contractName}...`) + const directory = await this.getCurrentDirectory() + await directory.unsetImplementation(contractName) + } + + async getCurrentDirectory() { + throw Error("Unimplemented") + } + + async getProjectPackage() { + throw Error("Unimplemented") + } +} \ No newline at end of file diff --git a/packages/lib/src/test/behaviors/BaseApp.js b/packages/lib/src/test/behaviors/BaseApp.js new file mode 100644 index 000000000..d910a4ae7 --- /dev/null +++ b/packages/lib/src/test/behaviors/BaseApp.js @@ -0,0 +1,223 @@ +'use strict'; + +import Proxy from '../../../src/utils/Proxy' +import Contracts from '../../../src/utils/Contracts' +import decodeLogs from '../../../src/helpers/decodeLogs' +import encodeCall from '../../../src/helpers/encodeCall' +import assertRevert from '../../../src/test/helpers/assertRevert' +import shouldBehaveLikeOwnable from '../../../src/test/behaviors/Ownable' + +const DummyImplementation = Contracts.getFromLocal('DummyImplementation') +const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') +const UpgradeabilityProxyFactory = Contracts.getFromLocal('UpgradeabilityProxyFactory') + +export default function shouldBehaveLikeApp([_, appOwner, directoryOwner, anotherAccount]) { + describe('ownership', function () { + beforeEach("setting ownable", function () { + this.ownable = this.app + }) + shouldBehaveLikeOwnable(appOwner, anotherAccount) + }) + + const shouldCreateProxy = function () { + it('sets proxy implementation', async function () { + const implementation = await this.app.getProxyImplementation(this.proxyAddress) + implementation.should.be.equal(this.implementation_v0) + }); + + it('sets proxy admin', async function () { + const admin = await this.app.getProxyAdmin(this.proxyAddress) + admin.should.be.equal(this.app.address) + }); + + it('delegates to implementation', async function () { + const version = await DummyImplementation.at(this.proxyAddress).version(); + version.should.be.equal("V1"); + }); + }; + + describe('create', function () { + describe('successful', function () { + beforeEach("creating proxy", async function () { + const { receipt } = await this.app.create(this.packageName, this.contractName) + this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + }) + + shouldCreateProxy(); + }); + + it('fails to create a proxy for unregistered package', async function () { + await assertRevert(this.app.create("NOTEXISTS", this.contractName)) + }); + + it('fails to create a proxy for unregistered contract', async function () { + await assertRevert(this.app.create(this.packageName, "NOTEXISTS")) + }); + }); + + describe('createAndCall', function () { + const value = 1e5 + const initializeData = encodeCall('initialize', ['uint256'], [42]) + const incorrectData = encodeCall('wrong', ['uint256'], [42]) + + describe('successful', function () { + beforeEach("creating proxy", async function () { + const { receipt } = await this.app.createAndCall(this.packageName, this.contractName, initializeData, { value }) + this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + }) + + shouldCreateProxy(); + + it('initializes the proxy', async function() { + const value = await DummyImplementation.at(this.proxyAddress).value() + value.should.be.bignumber.eq(42) + }) + + it('sends given value to the proxy', async function() { + const balance = await web3.eth.getBalance(this.proxyAddress) + balance.should.be.bignumber.eq(value) + }) + }); + + it('fails to create a proxy for unregistered package', async function () { + await assertRevert(this.app.createAndCall("NOTEXISTS", this.contractName, initializeData, { value })) + }); + + it('fails to create a proxy for unregistered contract', async function () { + await assertRevert(this.app.createAndCall(this.packageName, "NOTEXISTS", initializeData, { value })) + }); + + it('fails to create a proxy with invalid initialize data', async function () { + await assertRevert(this.app.createAndCall(this.packageName, this.contractName, incorrectData, { value })) + }); + }); + + const shouldUpgradeProxy = function () { + it('upgrades to the requested implementation', async function () { + const implementation = await this.app.getProxyImplementation(this.proxyAddress) + implementation.should.be.equal(this.implementation_v1) + }) + + it('delegates to new implementation', async function () { + const version = await DummyImplementationV2.at(this.proxyAddress).version(); + version.should.be.equal("V2"); + }); + }; + + describe('upgrade', function () { + beforeEach("creating proxy", async function () { + const { receipt } = await this.app.create(this.packageName, this.contractName, { from: appOwner }) + this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + }) + + describe('successful', async function () { + beforeEach("upgrading proxy", async function () { + await this.app.upgrade(this.proxyAddress, this.packageName, this.contractNameUpdated, { from: appOwner }) + }); + + shouldUpgradeProxy(); + }); + + it('fails to upgrade a proxy for unregistered package', async function () { + await assertRevert(this.app.upgrade(this.proxyAddress, "NOTEXISTS", this.contractNameUpdated)) + }); + + it('fails to upgrade a proxy for unregistered contract', async function () { + await assertRevert(this.app.upgrade(this.proxyAddress, this.packageName, "NOTEXISTS")) + }); + + it('fails to upgrade a non-proxy contract', async function () { + await assertRevert(this.app.upgrade(this.implementation_v0, this.packageName, this.contractNameUpdated)) + }); + + it('fails to upgrade from another account', async function () { + await assertRevert(this.app.upgrade(this.proxyAddress, this.packageName, this.contractNameUpdated, { from: anotherAccount })) + }); + }) + + describe('upgradeAndCall', function () { + const value = 1e5 + const initializeData = encodeCall('initialize', ['uint256'], [42]) + const migrateData = encodeCall('migrate', ['uint256'], [84]) + const incorrectData = encodeCall('wrong', ['uint256'], [42]) + + beforeEach("creating proxy", async function () { + const { receipt } = await this.app.createAndCall(this.packageName, this.contractName, initializeData, { from: appOwner }) + this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + }) + + describe('successful', async function () { + beforeEach("upgrading proxy", async function () { + await this.app.upgradeAndCall(this.proxyAddress, this.packageName, this.contractNameUpdated, migrateData, { value, from: appOwner }) + }); + + shouldUpgradeProxy() + + it('migrates the proxy', async function() { + const value = await DummyImplementationV2.at(this.proxyAddress).value() + value.should.be.bignumber.eq(84) + }) + + it('sends given value to the proxy', async function() { + const balance = await web3.eth.getBalance(this.proxyAddress) + balance.should.be.bignumber.eq(value) + }) + }); + + it('fails to upgrade a proxy for unregistered package', async function () { + await assertRevert(this.app.upgradeAndCall(this.proxyAddress, "NOTEXISTS", this.contractNameUpdated, migrateData, { from: appOwner })) + }); + + it('fails to upgrade a proxy for unregistered contract', async function () { + await assertRevert(this.app.upgradeAndCall(this.proxyAddress, this.packageName, "NOTEXISTS", migrateData, { from: appOwner })) + }); + + it('fails to upgrade a non-proxy contract', async function () { + await assertRevert(this.app.upgradeAndCall(this.implementation_v0, this.packageName, this.contractNameUpdated, migrateData, { from: appOwner })) + }); + + it('fails to upgrade from another account', async function () { + await assertRevert(this.app.upgradeAndCall(this.proxyAddress, this.packageName, this.contractNameUpdated, migrateData, { from: anotherAccount })) + }); + + it('fails to upgrade with incorrect migrate data', async function () { + await assertRevert(this.app.upgradeAndCall(this.proxyAddress, this.packageName, this.contractNameUpdated, incorrectData, { from: appOwner })) + }); + }) + + describe('getImplementation', function () { + it('fetches the requested implementation from the directory', async function () { + const implementation = await this.app.getImplementation(this.packageName, this.contractName) + implementation.should.be.equal(this.implementation_v0) + }) + + it('returns zero if implementation does not exist', async function () { + const implementation = await this.app.getImplementation(this.packageName, "NOTEXISTS") + implementation.should.be.zeroAddress + }) + + it('returns zero if package name does not exist', async function () { + const implementation = await this.app.getImplementation("NOTEXISTS", this.contractName) + implementation.should.be.zeroAddress + }) + }) + + describe('changeAdmin', function () { + beforeEach("creating proxy", async function () { + const { receipt } = await this.app.create(this.packageName, this.contractName) + this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) + this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + }) + + it('changes admin of the proxy', async function () { + await this.app.changeProxyAdmin(this.proxyAddress, anotherAccount, { from: appOwner }); + const proxy = Proxy.at(this.proxyAddress); + const admin = await proxy.admin(); + admin.should.be.equal(anotherAccount); + }); + }) +} \ No newline at end of file diff --git a/packages/lib/src/test/helpers/assertions.js b/packages/lib/src/test/helpers/assertions.js index 9b0844862..91fcd5142 100644 --- a/packages/lib/src/test/helpers/assertions.js +++ b/packages/lib/src/test/helpers/assertions.js @@ -1,3 +1,7 @@ +import _ from 'lodash'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + module.exports = function (chai, utils) { const Assertion = chai.Assertion @@ -6,7 +10,7 @@ module.exports = function (chai, utils) { this._obj && this._obj.length === 42 && this._obj.startsWith('0x') - && this._obj !== '0x0000000000000000000000000000000000000000' + && this._obj !== ZERO_ADDRESS , 'expected #{this} to be a non-zero address' , 'expected #{this} to not be a non-zero address' ); @@ -17,7 +21,7 @@ module.exports = function (chai, utils) { this._obj && this._obj.length === 42 && this._obj.startsWith('0x') - && this._obj === '0x0000000000000000000000000000000000000000' + && this._obj === ZERO_ADDRESS , 'expected #{this} to be a zero address' , 'expected #{this} to not be a zero address' ); diff --git a/packages/lib/src/utils/Addresses.js b/packages/lib/src/utils/Addresses.js new file mode 100644 index 000000000..4e98a6e15 --- /dev/null +++ b/packages/lib/src/utils/Addresses.js @@ -0,0 +1,17 @@ +import _ from 'lodash' + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export function toAddress(contractOrAddress) { + if (_.isEmpty(contractOrAddress)) { + throw Error(`Contract or address expected`) + } else if (_.isString(contractOrAddress)) { + return contractOrAddress + } else { + return contractOrAddress.address + } +} + +export function isZeroAddress(address) { + return !address || address === ZERO_ADDRESS +} \ No newline at end of file diff --git a/packages/lib/test/contracts/application/AppDirectory.test.js b/packages/lib/test/contracts/application/AppDirectory.test.js deleted file mode 100644 index cc9dd3942..000000000 --- a/packages/lib/test/contracts/application/AppDirectory.test.js +++ /dev/null @@ -1,134 +0,0 @@ -'use strict'; -require('../../setup') - -import Contracts from '../../../src/utils/Contracts' -import assertRevert from '../../../src/test/helpers/assertRevert' -import shouldBehaveLikeImplementationDirectory from '../../../src/test/behaviors/ImplementationDirectory' - -const AppDirectory = Contracts.getFromLocal('AppDirectory') -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') -const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory') - -contract('AppDirectory', ([_, appOwner, stdlibOwner, anotherAddress]) => { - before(async function () { - this.implementation_v0 = (await DummyImplementation.new()).address - this.implementation_v1 = (await DummyImplementation.new()).address - this.stdlibImplementation = (await DummyImplementation.new()).address - }) - - beforeEach(async function () { - this.directory = await AppDirectory.new(0x0, { from: appOwner }) - this.stdlib = await ImplementationDirectory.new({ from: stdlibOwner }) - }) - - shouldBehaveLikeImplementationDirectory(appOwner, anotherAddress) - - describe('getImplementation', function () { - const contractName = 'ERC721' - - describe('when no stdlib was set', function () { - describe('when the requested contract was registered in the directory', function () { - beforeEach(async function () { - await this.directory.setImplementation(contractName, this.implementation_v0, { from: appOwner }) - }) - - it('returns the directory implementation', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.equal(this.implementation_v0) - }) - }) - - describe('when the requested contract was not registered in the directory', function () { - it('returns the zero address', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.zeroAddress - }) - }) - }) - - describe('when a stdlib was set', function () { - beforeEach(async function () { - await this.directory.setStdlib(this.stdlib.address, { from: appOwner }) - }) - - describe('when the requested contract was registered in the directory', function () { - beforeEach(async function () { - await this.directory.setImplementation(contractName, this.implementation_v0, { from: appOwner }) - }) - - describe('when the requested contract was registered in the stdlib', function () { - beforeEach(async function () { - await this.stdlib.setImplementation(contractName, this.stdlibImplementation, { from: stdlibOwner }) - }) - - it('returns the directory implementation', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.equal(this.implementation_v0) - }) - }) - - describe('when the requested contract was not registered in the stdlib', function () { - it('returns the directory implementation', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.equal(this.implementation_v0) - }) - }) - }) - - describe('when the requested contract was not registered in the directory', function () { - describe('when the requested contract was registered in the stdlib', function () { - beforeEach(async function () { - await this.stdlib.setImplementation(contractName, this.stdlibImplementation, { from: stdlibOwner }) - }) - - it('returns the stdlib implementation', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.equal(this.stdlibImplementation) - }) - }) - - describe('when the requested contract was not registered in the stdlib', function () { - it('returns the zero address', async function () { - const implementation = await this.directory.getImplementation(contractName) - implementation.should.be.zeroAddress - }) - }) - }) - }) - }) - - describe('setStdlib', function () { - describe('when the sender is the owner', function () { - const from = appOwner - - beforeEach(async function () { - await this.directory.setStdlib(this.stdlib.address, { from }) - }) - - it('can set a new stdlib', async function () { - const stdlib = await this.directory.stdlib() - stdlib.should.be.equal(this.stdlib.address) - }) - - it('can reset a stdlib', async function () { - const anotherStdlib = await ImplementationDirectory.new({ from: stdlibOwner }) - await this.directory.setStdlib(anotherStdlib.address, { from }) - - const stdlib = await this.directory.stdlib() - stdlib.should.be.equal(anotherStdlib.address) - }) - - it('can unset a stdlib', async function () { - await this.directory.setStdlib(0, { from }) - const stdlib = await this.directory.stdlib() - stdlib.should.be.zeroAddress - }) - }) - - describe('when the sender is not the owner', function () { - it('reverts', async function () { - await assertRevert(this.directory.setStdlib(this.stdlib.address, { from: anotherAddress })) - }) - }) - }) -}) diff --git a/packages/lib/test/contracts/application/PackagedApp.test.js b/packages/lib/test/contracts/application/PackagedApp.test.js deleted file mode 100644 index ee1405a1a..000000000 --- a/packages/lib/test/contracts/application/PackagedApp.test.js +++ /dev/null @@ -1,339 +0,0 @@ -'use strict'; -require('../../setup') - -import Proxy from '../../../src/utils/Proxy' -import Contracts from '../../../src/utils/Contracts' -import encodeCall from '../../../src/helpers/encodeCall' -import decodeLogs from '../../../src/helpers/decodeLogs' -import assertRevert from '../../../src/test/helpers/assertRevert' -import shouldBehaveLikeOwnable from '../../../src/test/behaviors/Ownable' - -const Package = Contracts.getFromLocal('Package') -const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory') -const MigratableMock = Contracts.getFromLocal('MigratableMock') -const PackagedApp = Contracts.getFromLocal('PackagedApp') -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') -const AdminUpgradeabilityProxy = Contracts.getFromLocal('AdminUpgradeabilityProxy') -const UpgradeabilityProxyFactory = Contracts.getFromLocal('UpgradeabilityProxyFactory') - -contract('PackagedApp', ([_, appOwner, packageOwner, directoryOwner, anotherAccount]) => { - const contract = 'ERC721' - const version_0 = 'version_0' - const version_1 = 'version_1' - - before(async function () { - this.implementation_v0 = (await DummyImplementation.new()).address - this.implementation_v1 = (await DummyImplementation.new()).address - }) - - beforeEach(async function () { - this.factory = await UpgradeabilityProxyFactory.new() - this.package = await Package.new({ from: packageOwner }) - this.zeroVersionDirectory = await ImplementationDirectory.new({ from: directoryOwner }) - this.firstVersionDirectory = await ImplementationDirectory.new({ from: directoryOwner }) - }) - - describe('when the package address is null', function () { - it('reverts', async function () { - await assertRevert(PackagedApp.new(0, 'dummy', this.factory.address)) - }) - }) - - describe('when the given package does not support the required version', function () { - it('reverts', async function () { - await assertRevert(PackagedApp.new(this.package.address, version_0, this.factory.address, { from: appOwner })) - }) - }) - - describe('when the given package supports the required version', function () { - beforeEach(async function () { - await this.package.addVersion(version_0, this.zeroVersionDirectory.address, { from: packageOwner }) - this.app = await PackagedApp.new(this.package.address, version_0, this.factory.address, { from: appOwner }) - }) - - describe('ownership', function () { - beforeEach(function () { - this.ownable = this.app - }) - - shouldBehaveLikeOwnable(appOwner, anotherAccount) - }) - - describe('factory', function () { - it('returns the proxy factory being used by the app', async function () { - const factory = await this.app.factory() - - factory.should.be.equal(this.factory.address) - }) - }) - - describe('setVersion', function () { - const version = version_1 - - describe('when the sender is the app owner', function () { - const from = appOwner - - describe('whern the requested version is registered in the package', function () { - beforeEach(async function () { - await this.package.addVersion(version_1, this.firstVersionDirectory.address, { from: packageOwner }) - }) - - it('sets a new version', async function () { - await this.app.setVersion(version, { from }) - - const newVersion = await this.app.version() - newVersion.should.be.equal(version_1) - }) - }) - - describe('when the requested version is registered in the package', function () { - it('reverts', async function () { - await assertRevert(this.app.setVersion(version, { from })) - }) - }) - }) - - describe('when the sender is the app owner', function () { - it('reverts', async function () { - await assertRevert(this.app.setVersion(version_1, { from: anotherAccount })) - }) - }) - }) - - describe('create', function () { - describe('when the requested contract was registered for the current version', function () { - beforeEach(async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.proxy = await AdminUpgradeabilityProxy.at(this.proxyAddress) - }) - - it('creates a proxy pointing to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxy.address) - implementation.should.be.equal(this.implementation_v0) - }) - - it('transfers the ownership to the app', async function () { - const admin = await this.app.getProxyAdmin(this.proxy.address) - admin.should.be.equal(this.app.address) - }) - }) - - describe('when the requested contract was not registered for the current version', function () { - it('reverts', async function () { - await assertRevert(this.app.create(contract)) - }) - }) - }) - - describe('createAndCall', function () { - const value = 1e5 - const initializeData = encodeCall('initialize', ['uint256'], [42]) - - beforeEach(async function () { - this.behavior = await MigratableMock.new() - }) - - describe('when the requested contract was registered for the current version', function () { - beforeEach(async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - - const { receipt } = await this.app.createAndCall(contract, initializeData, { value }) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.proxy = await AdminUpgradeabilityProxy.at(this.proxyAddress) - }) - - it('creates a proxy pointing to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxy.address) - implementation.should.be.equal(this.behavior.address) - }) - - it('transfers the ownership to the app', async function () { - const admin = await this.app.getProxyAdmin(this.proxy.address) - admin.should.be.equal(this.app.address) - }) - - it('calls "initialize" function', async function() { - const initializable = MigratableMock.at(this.proxyAddress) - const x = await initializable.x() - x.should.be.bignumber.eq(42) - }) - - it('sends given value to the delegated implementation', async function() { - const balance = await web3.eth.getBalance(this.proxyAddress) - assert(balance.eq(value)) - }) - - it('uses the storage of the proxy', async function () { - // fetch the x value of Migratable at position 0 of the storage - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(1) - storedValue.should.be.bignumber.eq(42) - }) - }) - - describe('when the requested contract was not registered for the current version', function () { - it('reverts', async function () { - await assertRevert(this.app.createAndCall(contract, initializeData, { value })) - }) - }) - }) - - describe('upgrade', function () { - beforeEach(async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.proxy = await AdminUpgradeabilityProxy.at(this.proxyAddress) - - // set new version - await this.package.addVersion(version_1, this.firstVersionDirectory.address, { from: packageOwner }) - await this.app.setVersion(version_1, { from: appOwner }) - }) - - describe('when the sender is the app owner', function () { - const from = appOwner - - describe('when the requested contract was registered for the new version', function () { - beforeEach(async function () { - await this.firstVersionDirectory.setImplementation(contract, this.implementation_v1, { from: directoryOwner }) - }) - - it('upgrades to the requested implementation', async function () { - await this.app.upgrade(this.proxyAddress, contract, { from }) - - const implementation = await this.app.getProxyImplementation(this.proxy.address) - implementation.should.be.equal(this.implementation_v1) - }) - }) - - describe('when the requested contract was not registered for the new version', function () { - it('reverts', async function () { - await assertRevert(this.app.upgrade(this.proxyAddress, contract, { from })) - }) - }) - }) - - describe('when the sender is not the app owner', function () { - const from = anotherAccount - - it('reverts', async function () { - await this.firstVersionDirectory.setImplementation(contract, this.implementation_v1, { from: directoryOwner }) - await assertRevert(this.app.upgrade(this.proxyAddress, contract, { from })) - }) - }) - }) - - describe('upgradeAndCall', function () { - const initializeData = encodeCall('initialize', ['uint256'], [42]) - - beforeEach(async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.proxy = await AdminUpgradeabilityProxy.at(this.proxyAddress) - this.behavior = await MigratableMock.new() - - // set new version - await this.package.addVersion(version_1, this.firstVersionDirectory.address, { from: packageOwner }) - await this.app.setVersion(version_1, { from: appOwner }) - }) - - describe('when the sender is the app owner', function () { - const from = appOwner - const value = 1e5 - - describe('when the requested contract was registered for the new version', function () { - beforeEach(async function () { - await this.firstVersionDirectory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - await this.app.upgradeAndCall(this.proxyAddress, contract, initializeData, { from, value }) - }) - - it('upgrades to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxy.address) - implementation.should.be.equal(this.behavior.address) - }) - - it('calls the "initialize" function', async function() { - const initializable = MigratableMock.at(this.proxyAddress) - const x = await initializable.x() - x.should.be.bignumber.eq(42) - }) - - it('sends given value to the delegated implementation', async function() { - const balance = await web3.eth.getBalance(this.proxyAddress) - assert(balance.eq(value)) - }) - - it('uses the storage of the proxy', async function () { - // fetch the x value of Migratable at position 0 of the storage - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(1) - storedValue.should.be.bignumber.eq(42) - }) - }) - - describe('when the requested contract was not registered for the new version', function () { - it('reverts', async function () { - await assertRevert(this.app.upgradeAndCall(this.proxyAddress, contract, initializeData, { from, value })) - }) - }) - }) - - describe('when the sender is not the app owner', function () { - const from = anotherAccount - - it('reverts', async function () { - await this.firstVersionDirectory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - await assertRevert(this.app.upgradeAndCall(this.proxyAddress, contract, initializeData, { from })) - }) - }) - }) - - describe('getImplementation', function () { - describe('when using the directory of the first version', function () { - it('fetches the implementation of the contract registered in the zero directory', async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - - const implementation = await this.app.getImplementation(contract) - implementation.should.be.equal(this.implementation_v0) - }) - }) - - describe('when using the directory of the first version', function () { - beforeEach(async function () { - await this.package.addVersion(version_1, this.firstVersionDirectory.address, { from: packageOwner }) - await this.app.setVersion(version_1, { from: appOwner }) - }) - - it('fetches the implementation of the contract registered in the first directory', async function () { - await this.firstVersionDirectory.setImplementation(contract, this.implementation_v1, { from: directoryOwner }) - - const implementation = await this.app.getImplementation(contract) - implementation.should.be.equal(this.implementation_v1) - }) - }) - }) - - describe('changeAdmin', function () { - beforeEach(async function () { - await this.zeroVersionDirectory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.proxy = await AdminUpgradeabilityProxy.at(this.proxyAddress) - }) - - it('changes admin of the proxy', async function () { - await this.app.changeProxyAdmin(this.proxyAddress, anotherAccount, { from: appOwner }); - const proxy = Proxy.at(this.proxyAddress); - const admin = await proxy.admin(); - admin.should.be.equal(anotherAccount); - }); - }) - }) -}) diff --git a/packages/lib/test/contracts/application/UnversionedApp.test.js b/packages/lib/test/contracts/application/UnversionedApp.test.js index c93a1e775..e810e25f4 100644 --- a/packages/lib/test/contracts/application/UnversionedApp.test.js +++ b/packages/lib/test/contracts/application/UnversionedApp.test.js @@ -1,251 +1,81 @@ 'use strict'; require('../../setup') -import Proxy from '../../../src/utils/Proxy' import Contracts from '../../../src/utils/Contracts' -import decodeLogs from '../../../src/helpers/decodeLogs' -import encodeCall from '../../../src/helpers/encodeCall' -import assertRevert from '../../../src/test/helpers/assertRevert' -import shouldBehaveLikeOwnable from '../../../src/test/behaviors/Ownable' +import shouldBehaveLikeApp from '../../../src/test/behaviors/BaseApp'; +import assertRevert from '../../../src/test/helpers/assertRevert'; -const MigratableMock = Contracts.getFromLocal('MigratableMock') const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory') const DummyImplementation = Contracts.getFromLocal('DummyImplementation') +const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') const UnversionedApp = Contracts.getFromLocal('UnversionedApp') const UpgradeabilityProxyFactory = Contracts.getFromLocal('UpgradeabilityProxyFactory') -contract('UnversionedApp', ([_, appOwner, directoryOwner, anotherAccount]) => { - const contract = 'ERC721' +contract('UnversionedApp', (accounts) => { + const [_, appOwner, directoryOwner, anotherAccount] = accounts; - before(async function () { + before("initializing dummy implementations", async function () { this.implementation_v0 = (await DummyImplementation.new()).address - this.implementation_v1 = (await DummyImplementation.new()).address + this.implementation_v1 = (await DummyImplementationV2.new()).address }) - beforeEach(async function () { + beforeEach("initializing unversioned app", async function () { + this.contractName = 'ERC721'; + this.contractNameUpdated = 'ERC721Updated'; + this.packageName = 'MyProject'; + this.factory = await UpgradeabilityProxyFactory.new() this.directory = await ImplementationDirectory.new({ from: directoryOwner }) - this.app = await UnversionedApp.new(this.directory.address, this.factory.address, { from: appOwner }) - }) - - it('must receive an implementation directory and a factory', async function () { - await assertRevert(UnversionedApp.new(0x0, this.factory.address)) - await assertRevert(UnversionedApp.new(this.directory.address, 0x0)) - }) - - describe('ownership', function () { - beforeEach(function () { - this.ownable = this.app - }) + await this.directory.setImplementation(this.contractName, this.implementation_v0, { from: directoryOwner }) + await this.directory.setImplementation(this.contractNameUpdated, this.implementation_v1, { from: directoryOwner }) - shouldBehaveLikeOwnable(appOwner, anotherAccount) + this.app = await UnversionedApp.new(this.factory.address, { from: appOwner }) + await this.app.setProvider(this.packageName, this.directory.address, { from: appOwner }); }) - describe('factory', function () { - it('returns the proxy factory being used by the app', async function () { - const factory = await this.app.factory() + shouldBehaveLikeApp(accounts); - factory.should.be.equal(this.factory.address) - }) - }) - - describe('create', function () { - describe('when the requested contract was registered in the implementation provider', function () { - beforeEach(async function () { - await this.directory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - }) - - it('creates a proxy pointing to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxyAddress) - implementation.should.be.equal(this.implementation_v0) - }) - - it('transfers the ownership to the app', async function () { - const admin = await this.app.getProxyAdmin(this.proxyAddress) - admin.should.be.equal(this.app.address) - }) - }) + describe('setProvider', function () { + const anotherPackageName = 'AnotherProject'; - describe('when the requested contract was not registered', function () { - it('reverts', async function () { - await assertRevert(this.app.create(contract)) - }) - }) - }) - - describe('createAndCall', function () { - const value = 1e5 - const initializeData = encodeCall('initialize', ['uint256'], [42]) - - beforeEach(async function () { - this.behavior = await MigratableMock.new() - }) - - describe('when the requested contract was registered in the implementation provider', function () { - beforeEach(async function () { - await this.directory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - - const { receipt } = await this.app.createAndCall(contract, initializeData, { value }) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - }) - - it('creates a proxy pointing to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxyAddress) - implementation.should.be.equal(this.behavior.address) - }) - - it('transfers the ownership to the app', async function () { - const admin = await this.app.getProxyAdmin(this.proxyAddress) - admin.should.be.equal(this.app.address) - }) - - it('calls "initialize" function', async function() { - const migratable = MigratableMock.at(this.proxyAddress) - const x = await migratable.x() - x.should.be.bignumber.eq(42) - }) - - it('sends given value to the delegated implementation', async function() { - const balance = await web3.eth.getBalance(this.proxyAddress) - balance.should.be.bignumber.eq(value) - }) - - it('uses the storage of the proxy', async function () { - // fetch the x value of Migratable at position 0 of the storage - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(1) - storedValue.should.be.bignumber.eq(42) - }) - }) - - describe('when the requested contract was not registered', function () { - it('reverts', async function () { - await assertRevert(this.app.createAndCall(contract, initializeData, { value })) - }) - }) - }) - - describe('upgrade', function () { - beforeEach(async function () { - await this.directory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - - await this.directory.setImplementation(contract, this.implementation_v1, { from: directoryOwner }) - }) - - describe('when the sender is the app owner', function () { - const from = appOwner - - it('upgrades to the requested implementation', async function () { - await this.app.upgrade(this.proxyAddress, contract, { from }) - - const implementation = await this.app.getProxyImplementation(this.proxyAddress) - implementation.should.be.equal(this.implementation_v1) - }) - }) - - describe('when the sender is not the app owner', function () { - const from = anotherAccount + beforeEach('initializing another directory', async function () { + this.anotherDirectory = await ImplementationDirectory.new({ from: directoryOwner }); + }); - it('reverts', async function () { - await assertRevert(this.app.upgrade(this.proxyAddress, contract, { from })) - }) + it('sets a provider with a different name', async function () { + await this.app.setProvider(anotherPackageName, this.anotherDirectory.address, { from: appOwner }); + const provider = await this.app.getProvider(anotherPackageName); + provider.should.eq(this.anotherDirectory.address); }) - }) - - describe('upgradeAndCall', function () { - const initializeData = encodeCall('initialize', ['uint256'], [42]) - beforeEach(async function () { - await this.directory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy - this.behavior = await MigratableMock.new() + it('overwrites existing provider', async function () { + await this.app.setProvider(this.packageName, this.anotherDirectory.address, { from: appOwner }); + const provider = await this.app.getProvider(this.packageName); + provider.should.eq(this.anotherDirectory.address); }) - describe('when the sender is the app owner', function () { - const from = appOwner - const value = 1e5 - - beforeEach(async function () { - await this.directory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - await this.app.upgradeAndCall(this.proxyAddress, contract, initializeData, { from, value }) - }) - - it('upgrades to the requested implementation', async function () { - const implementation = await this.app.getProxyImplementation(this.proxyAddress) - implementation.should.be.equal(this.behavior.address) - }) - - it('calls the "initialize" function', async function() { - const migratable = MigratableMock.at(this.proxyAddress) - const x = await migratable.x() - x.should.be.bignumber.eq(42) - }) - - it('sends given value to the delegated implementation', async function() { - const balance = await web3.eth.getBalance(this.proxyAddress) - balance.should.be.bignumber.eq(value) - }) - - it('uses the storage of the proxy', async function () { - // fetch the x value of Initializable at position 0 of the storage - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(1) - storedValue.should.be.bignumber.eq(42) - }) + it('fails to set provider from another account', async function () { + await assertRevert(this.app.setProvider(anotherPackageName, this.anotherDirectory.address, { from: anotherAccount })); }) - describe('when the sender is not the app owner', function () { - const from = anotherAccount - - it('reverts', async function () { - await this.directory.setImplementation(contract, this.behavior.address, { from: directoryOwner }) - await assertRevert(this.app.upgradeAndCall(this.proxyAddress, contract, initializeData, { from })) - }) + it('fails to set a provider to zero', async function () { + await assertRevert(this.app.setProvider(anotherPackageName, "0x0000000000000000000000000000000000000000", { from: appOwner })); }) }) - describe('getImplementation', function () { - - describe('when the requested contract was registered in the directory', function () { - beforeEach(async function () { - await this.directory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - }) - - it('fetches the requested implementation from the directory', async function () { - const implementation = await this.app.getImplementation(contract) - implementation.should.be.equal(this.implementation_v0) - }) + describe('unsetProvider', function () { + it('unsets provider', async function () { + await this.app.unsetProvider(this.packageName, { from: appOwner }); + const provider = await this.app.getProvider(this.packageName); + provider.should.be.zeroAddress; }) - describe('when the requested contract was not registered in the directory', function () { - it('returns a zero address', async function () { - const implementation = await this.app.getImplementation(contract) - implementation.should.be.zeroAddress - }) + it('fails to unset non-existing provider', async function () { + await assertRevert(this.app.unsetProvider("NOTEXISTS", { from: appOwner })); }) - }) - describe('changeAdmin', function () { - beforeEach(async function () { - await this.directory.setImplementation(contract, this.implementation_v0, { from: directoryOwner }) - const { receipt } = await this.app.create(contract) - this.logs = decodeLogs(receipt.logs, UpgradeabilityProxyFactory) - this.proxyAddress = this.logs.find(l => l.event === 'ProxyCreated').args.proxy + it('fails to unset provider from another account', async function () { + await assertRevert(this.app.unsetProvider(this.packageName, { from: anotherAccount })); }) - - it('changes admin of the proxy', async function () { - await this.app.changeProxyAdmin(this.proxyAddress, anotherAccount, { from: appOwner }); - const proxy = Proxy.at(this.proxyAddress); - const admin = await proxy.admin(); - admin.should.be.equal(anotherAccount); - }); }) }) diff --git a/packages/lib/test/contracts/application/VersionedApp.test.js b/packages/lib/test/contracts/application/VersionedApp.test.js new file mode 100644 index 000000000..17929e1d2 --- /dev/null +++ b/packages/lib/test/contracts/application/VersionedApp.test.js @@ -0,0 +1,145 @@ +'use strict'; +require('../../setup') + +import Contracts from '../../../src/utils/Contracts' +import assertRevert from '../../../src/test/helpers/assertRevert' +import shouldBehaveLikeApp from '../../../src/test/behaviors/BaseApp'; + +const Package = Contracts.getFromLocal('Package') +const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory') +const VersionedApp = Contracts.getFromLocal('VersionedApp') +const DummyImplementation = Contracts.getFromLocal('DummyImplementation') +const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') +const UpgradeabilityProxyFactory = Contracts.getFromLocal('UpgradeabilityProxyFactory') + +contract('VersionedApp', (accounts) => { + const [_, appOwner, packageOwner, directoryOwner, anotherAccount] = accounts; + + const version_0 = 'version_0' + const version_1 = 'version_1' + + before("initializing dummy implementations", async function () { + this.implementation_v0 = (await DummyImplementation.new()).address + this.implementation_v1 = (await DummyImplementationV2.new()).address + }) + + beforeEach("initializing versioned app", async function () { + this.contractName = 'ERC721'; + this.contractNameUpdated = 'ERC721Updated'; + this.packageName = 'MyProject'; + + this.directory = await ImplementationDirectory.new({ from: directoryOwner }) + await this.directory.setImplementation(this.contractName, this.implementation_v0, { from: directoryOwner }) + await this.directory.setImplementation(this.contractNameUpdated, this.implementation_v1, { from: directoryOwner }) + + this.package = await Package.new({ from: packageOwner }) + await this.package.addVersion(version_0, this.directory.address, { from: packageOwner }); + + this.factory = await UpgradeabilityProxyFactory.new() + this.app = await VersionedApp.new(this.factory.address, { from: appOwner }) + await this.app.setPackage(this.packageName, this.package.address, version_0, { from: appOwner }); + }) + + shouldBehaveLikeApp(accounts); + + const assertPackage = async function(packageName, expectedAddress, expectedVersion) { + const [address, version] = await this.app.getPackage(packageName); + version.should.eq(expectedVersion); + address.should.eq(expectedAddress); + } + + describe('setPackage', function () { + beforeEach(async function () { + this.directoryV1 = await ImplementationDirectory.new({ from: directoryOwner }) + await this.package.addVersion(version_1, this.directoryV1.address, { from: packageOwner }); + this.anotherPackage = await Package.new({ from: packageOwner }) + await this.anotherPackage.addVersion(version_0, this.directory.address, { from: packageOwner }); + this.anotherPackageName = 'AnotherPackage'; + + this.assertPackage = assertPackage; + }); + + it('registers a new package and version', async function () { + await this.app.setPackage(this.anotherPackageName, this.anotherPackage.address, version_0, { from: appOwner }); + await this.assertPackage(this.anotherPackageName, this.anotherPackage.address, version_0); + await this.assertPackage(this.packageName, this.package.address, version_0); + }); + + it('overwrites registered package with the same name', async function () { + await this.app.setPackage(this.packageName, this.anotherPackage.address, version_0, { from: appOwner }); + await this.assertPackage(this.packageName, this.anotherPackage.address, version_0); + }); + + it('updates existing package version', async function () { + await this.app.setPackage(this.packageName, this.package.address, version_1, { from: appOwner }); + await this.assertPackage(this.packageName, this.package.address, version_1); + }); + + it('fails if package does not have the required version', async function () { + await assertRevert(this.app.setPackage(this.anotherPackageName, this.anotherPackage.address, "NOTEXISTS", { from: appOwner })); + }); + + it('fails if called from non-owner account', async function () { + await assertRevert(this.app.setPackage(this.anotherPackageName, this.anotherPackage.address, version_0, { from: anotherAccount })); + }); + }) + + describe('unsetPackage', function () { + beforeEach(async function () { + this.assertPackage = assertPackage; + }); + + it('unsets a package', async function () { + await this.app.unsetPackage(this.packageName, { from: appOwner }); + const [address, version] = await this.app.getPackage(this.packageName); + version.should.be.empty; + address.should.be.zeroAddress; + }) + + it('fails to unset a package from non-owner account', async function () { + await assertRevert(this.app.unsetPackage(this.packageName, { from: anotherAccount })); + }); + + it('fails to unset a non-existing package', async function () { + await assertRevert(this.app.unsetPackage("NOTEXISTS", { from: appOwner })); + }); + }); + + describe('getProvider', function () { + beforeEach(async function () { + this.anotherDirectory = await ImplementationDirectory.new({ from: directoryOwner }); + this.anotherPackage = await Package.new({ from: packageOwner }) + await this.anotherPackage.addVersion(version_1, this.anotherDirectory.address, { from: packageOwner }); + this.anotherPackageName = 'AnotherPackage'; + await this.app.setPackage(this.anotherPackageName, this.anotherPackage.address, version_1, { from: appOwner }); + }); + + it('returns provider from package', async function () { + const provider = await this.app.getProvider(this.packageName); + provider.should.eq(this.directory.address); + }); + + it('returns provider from another package', async function () { + const provider = await this.app.getProvider(this.anotherPackageName); + provider.should.eq(this.anotherDirectory.address); + }); + + it('returns provider from updated package', async function () { + await this.app.setPackage(this.packageName, this.anotherPackage.address, version_1, { from: appOwner }); + const provider = await this.app.getProvider(this.packageName); + provider.should.eq(this.anotherDirectory.address); + }); + + it('returns provider from updated package version', async function () { + await this.package.addVersion(version_1, this.anotherDirectory.address, { from: packageOwner }); + await this.app.setPackage(this.packageName, this.package.address, version_1, { from: appOwner }); + const provider = await this.app.getProvider(this.packageName); + provider.should.eq(this.anotherDirectory.address); + }); + + it('returns zero when requested non-existing package name', async function () { + const provider = await this.app.getProvider("NOTEXISTS"); + provider.should.be.zeroAddress; + }) + }); +}) diff --git a/packages/lib/test/src/app/App.test.js b/packages/lib/test/src/app/App.test.js deleted file mode 100644 index e3704ed93..000000000 --- a/packages/lib/test/src/app/App.test.js +++ /dev/null @@ -1,330 +0,0 @@ -'use strict'; -require('../../setup') - -import App from '../../../src/app/App'; -import Contracts from '../../../src/utils/Contracts' -import Proxy from '../../../src/utils/Proxy'; - -const Impl = Contracts.getFromLocal('Impl'); -const ImplV1 = Contracts.getFromLocal('DummyImplementation'); -const ImplV2 = Contracts.getFromLocal('DummyImplementationV2'); -const AppDirectory = Contracts.getFromLocal('AppDirectory'); -const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory'); - -contract('App', function ([_, owner, otherAdmin]) { - const txParams = { from: owner } - const initialVersion = '1.0'; - const contractName = 'Impl'; - - const shouldInitialize = function () { - it('deploys all contracts', async function() { - this.app.address.should.not.be.null; - this.app.factory.address.should.not.be.null; - this.app.package.address.should.not.be.null; - }); - - it('sets initial version', async function () { - this.app.version.should.eq(initialVersion); - }); - - it('registers initial version in package', async function () { - (await this.app.package.hasVersion(initialVersion)).should.be.true; - }); - - it('initializes all app properties', async function () { - this.app.version.should.eq(initialVersion); - this.app.directory.should.not.be.null - }); - - it('returns the current directory', async function () { - this.app.directory.address.should.be.not.null; - }); - }; - - const shouldConnectToStdlib = function () { - it('should connect current directory to stdlib', async function () { - const appDirectory = await this.app.package.getDirectory(this.app.version) - const currentStdlib = await appDirectory.stdlib() - - const stdlib = await this.app.currentStdlib(); - stdlib.should.be.eq(currentStdlib) - }); - - it('should tell whether current stdlib is zero', async function () { - (await this.app.hasStdlib()).should.be.true - }) - }; - - beforeEach('deploying stdlib', async function () { - this.stdlib = await ImplementationDirectory.new({ from: owner }) - }); - - describe('without stdlib', function () { - beforeEach('deploying', async function () { - this.app = await App.deploy(initialVersion, txParams) - }); - - describe('deploy', function () { - shouldInitialize(); - - it('should not have an stdlib initially', async function () { - (await this.app.hasStdlib()).should.be.false - }) - }); - - describe('fetch', function () { - beforeEach('connecting to existing instance', async function () { - this.app = await App.fetch(this.app.address, txParams); - }); - - shouldInitialize(); - - it('should not have an stdlib initially', async function () { - (await this.app.hasStdlib()).should.be.false - }) - }); - - const newVersion = '2.0'; - const createVersion = async function () { - await this.app.newVersion(newVersion); - }; - - describe('newVersion', function () { - beforeEach('creating a new version', createVersion); - - it('updates own properties', async function () { - this.app.version.should.eq(newVersion); - this.app.directory.should.not.be.null - }); - - it('registers new version on package', async function () { - (await this.app.package.hasVersion(newVersion)).should.be.true; - }); - - it('returns the current directory', async function () { - const appDirectory = await this.app.package.getDirectory(this.app.version) - - this.app.directory.address.should.eq(appDirectory.address) - }); - }); - - const setImplementation = async function () { - this.implementation_v1 = await this.app.setImplementation(ImplV1, contractName); - }; - - describe('setImplementation', function () { - beforeEach('setting implementation', setImplementation); - - it('should return implementation', async function () { - this.implementation_v1.address.should.be.not.null; - }); - - it('should register implementation on directory', async function () { - const implementation = await this.app.directory.getImplementation(contractName); - implementation.should.eq(this.implementation_v1.address); - }); - }); - - describe('unsetImplementation', function () { - beforeEach('setting implementation', setImplementation) - - it('should unset implementation on directory', async function () { - await this.app.unsetImplementation(contractName) - const implementation = await this.app.directory.getImplementation(contractName) - implementation.should.be.zeroAddress - }) - }) - - describe('createContract', function () { - beforeEach('setting implementation', setImplementation); - - const shouldReturnANonUpgradeableInstance = function () { - it('should return a non-upgradeable instance', async function () { - this.instance.address.should.be.not.null; - (await this.instance.version()).should.be.eq('V1'); - (await web3.eth.getCode(this.instance.address)).should.be.eq(ImplV1.deployedBytecode) - }); - }; - - describe('without initializer', function () { - beforeEach('creating a non-upgradeable instance', async function () { - this.instance = await this.app.createContract(ImplV1, contractName); - }); - - shouldReturnANonUpgradeableInstance(); - }); - - describe('with initializer', function () { - beforeEach('creating a proxy', async function () { - this.instance = await this.app.createContract(ImplV1, contractName, 'initialize', [10]); - }); - - shouldReturnANonUpgradeableInstance(); - - it('should have initialized the instance', async function () { - (await this.instance.value()).toNumber().should.eq(10); - }); - }); - - describe('with complex initializer', function () { - beforeEach('creating a non-upgradeable instance', async function () { - this.instance = await this.app.createContract(ImplV1, contractName, 'initialize', [10, "foo", []]); - }); - - shouldReturnANonUpgradeableInstance(); - - it('should have initialized the proxy', async function () { - (await this.instance.value()).toNumber().should.eq(10); - (await this.instance.text()).should.eq("foo"); - await this.instance.values(0).should.be.rejected; - }); - }); - - describe('with implicit contract name', async function () { - beforeEach('creating a non-upgradeable instance', async function () { - this.instance = await this.app.createContract(Impl); - }); - - shouldReturnANonUpgradeableInstance(); - }); - }); - - const createProxy = async function () { - this.proxy = await this.app.createProxy(ImplV1, contractName); - }; - - describe('createProxy', function () { - beforeEach('setting implementation', setImplementation); - - const shouldReturnProxy = function () { - it('should return a proxy', async function () { - this.proxy.address.should.be.not.null; - (await this.proxy.version()).should.be.eq('V1'); - (await this.app.getProxyImplementation(this.proxy.address)).should.be.eq(this.implementation_v1.address) - }); - }; - - describe('without initializer', function () { - beforeEach('creating a proxy', createProxy); - shouldReturnProxy(); - }); - - describe('with initializer', function () { - beforeEach('creating a proxy', async function () { - this.proxy = await this.app.createProxy(ImplV1, contractName, 'initialize', [10]); - }); - - shouldReturnProxy(); - - it('should have initialized the proxy', async function () { - (await this.proxy.value()).toNumber().should.eq(10); - }); - }); - - describe('with complex initializer', function () { - beforeEach('creating a proxy', async function () { - this.proxy = await this.app.createProxy(ImplV1, contractName, 'initialize', [10, "foo", []]); - }); - - shouldReturnProxy(); - - it('should have initialized the proxy', async function () { - (await this.proxy.value()).toNumber().should.eq(10); - (await this.proxy.text()).should.eq("foo"); - await this.proxy.values(0).should.be.rejected; - }); - }); - - describe('with implicit contract name', async function () { - beforeEach('creating a proxy', async function () { - this.proxy = await this.app.createProxy(Impl); - }); - shouldReturnProxy(); - }); - }); - - describe('upgradeProxy', function () { - beforeEach('setting implementation', setImplementation); - beforeEach('create proxy', createProxy); - beforeEach('creating new version', createVersion); - beforeEach('setting new implementation', async function () { - this.implementation_v2 = await this.app.setImplementation(ImplV2, contractName); - }); - - const shouldUpgradeProxy = function () { - it('should upgrade proxy to ImplV2', async function () { - (await this.proxy.version()).should.be.eq('V2'); - (await this.app.getProxyImplementation(this.proxy.address)).should.be.eq(this.implementation_v2.address) - }); - }; - - describe('without call', function () { - beforeEach('upgrading the proxy', async function () { - await this.app.upgradeProxy(this.proxy.address, ImplV2, contractName); - }); - - shouldUpgradeProxy(); - }); - - describe('with call', function () { - beforeEach('upgrading the proxy', async function () { - await this.app.upgradeProxy(this.proxy.address, ImplV2, contractName, 'migrate', [20]); - }); - - shouldUpgradeProxy(); - - it('should run migration', async function () { - (await this.proxy.value()).toNumber().should.eq(20); - }); - }); - - describe('with implicit contract name', async function () { - beforeEach('upgrading the proxy', async function () { - await this.app.upgradeProxy(this.proxy.address, Impl); - }); - - shouldUpgradeProxy(); - }); - }); - - describe('setStdlib', function () { - beforeEach('setting stdlib from name', async function () { - await this.app.setStdlib(this.stdlib.address); - }); - - shouldConnectToStdlib(); - }); - - describe('changeProxyAdmin', function () { - beforeEach('setting implementation', setImplementation); - beforeEach('create proxy', createProxy); - - it('should change proxy admin', async function () { - await this.app.changeProxyAdmin(this.proxy.address, otherAdmin); - const proxyWrapper = Proxy.at(this.proxy.address); - const actualAdmin = await proxyWrapper.admin(); - actualAdmin.should.be.eq(otherAdmin); - }); - }); - }); - - describe('with stdlib', function () { - beforeEach('deploying', async function () { - this.app = await App.deployWithStdlib(initialVersion, this.stdlib.address, txParams); - }); - - describe('deploy', function () { - shouldInitialize(); - shouldConnectToStdlib(); - }); - - describe('fetch', function () { - beforeEach('connecting to existing instance', async function () { - this.app = await App.fetch(this.app.address, txParams); - }); - - shouldInitialize(); - shouldConnectToStdlib(); - }); - }); -}); diff --git a/packages/lib/test/src/app/BaseApp.behavior.js b/packages/lib/test/src/app/BaseApp.behavior.js new file mode 100644 index 000000000..56adc15c1 --- /dev/null +++ b/packages/lib/test/src/app/BaseApp.behavior.js @@ -0,0 +1,218 @@ +'use strict'; +require('../../setup') + +import Contracts from '../../../src/utils/Contracts' +import Proxy from '../../../src/utils/Proxy'; +import FreezableImplementationDirectory from '../../../src/directory/FreezableImplementationDirectory'; + +const ImplV1 = Contracts.getFromLocal('DummyImplementation'); +const ImplV2 = Contracts.getFromLocal('DummyImplementationV2'); + +export default function shouldBehaveLikeApp(appClass, accounts, { setImplementation, setNewImplementation }) { + const [_unused, owner, otherAdmin] = accounts; + const txParams = { from: owner }; + const contractName = 'Impl'; + const packageName = 'MyPackage'; + + function shouldInitialize () { + it('initializes the app', async function () { + this.app.contract.should.be.not.null + this.app.address.should.be.nonzeroAddress + }) + } + + describe('deploy', function () { + shouldInitialize() + }) + + describe('fetch', function () { + beforeEach('fetching', async function () { + const address = this.app.address + this.app = await appClass.fetch(address, txParams) + }) + + shouldInitialize() + }) + + describe('createContract', function () { + beforeEach('setting implementation', setImplementation); + + const shouldReturnANonUpgradeableInstance = function () { + it('should return a non-upgradeable instance', async function () { + this.instance.address.should.be.not.null; + (await this.instance.version()).should.be.eq('V1'); + (await web3.eth.getCode(this.instance.address)).should.be.eq(ImplV1.deployedBytecode) + }); + }; + + describe('without initializer', function () { + beforeEach('creating a non-upgradeable instance', async function () { + this.instance = await this.app.createContract(ImplV1, packageName, contractName); + }); + + shouldReturnANonUpgradeableInstance(); + }); + + describe('with initializer', function () { + beforeEach('creating a proxy', async function () { + this.instance = await this.app.createContract(ImplV1, packageName, contractName, 'initialize', [10]); + }); + + shouldReturnANonUpgradeableInstance(); + + it('should have initialized the instance', async function () { + (await this.instance.value()).toNumber().should.eq(10); + }); + }); + + describe('with complex initializer', function () { + beforeEach('creating a non-upgradeable instance', async function () { + this.instance = await this.app.createContract(ImplV1, packageName, contractName, 'initialize', [10, "foo", []]); + }); + + shouldReturnANonUpgradeableInstance(); + + it('should have initialized the proxy', async function () { + (await this.instance.value()).toNumber().should.eq(10); + (await this.instance.text()).should.eq("foo"); + await this.instance.values(0).should.be.rejected; + }); + }); + }); + + describe('getImplementation', async function () { + beforeEach('setting implementation', setImplementation); + + it('returns implementation address', async function () { + const implementation = await this.app.getImplementation(packageName, contractName) + implementation.should.eq(this.implV1.address) + }) + + it('returns zero if not exists', async function () { + const implementation = await this.app.getImplementation(packageName, 'NOTEXISTS') + implementation.should.be.zeroAddress + }) + }) + + describe('getProvider', async function () { + beforeEach('setting implementation', setImplementation); + + it('returns provider', async function () { + const provider = await this.app.getProvider(packageName) + provider.address.should.eq(this.directory.address) + provider.should.be.instanceof(FreezableImplementationDirectory) + }) + + it('returns null if not exists', async function () { + const provider = await this.app.getProvider('NOTEXISTS') + const isNull = (provider === null) + isNull.should.be.true + }) + }) + + describe('hasProvider', async function () { + beforeEach('setting implementation', setImplementation); + + it('returns true', async function () { + const hasProvider = await this.app.hasProvider(packageName) + hasProvider.should.be.true + }) + + it('returns false if not exists', async function () { + const hasProvider = await this.app.hasProvider('NOTEXISTS') + hasProvider.should.be.false + }) + }) + + describe('createProxy', function () { + beforeEach('setting implementation', setImplementation); + + const shouldReturnProxy = function () { + it('should return a proxy', async function () { + this.proxy.address.should.be.not.null; + (await this.proxy.version()).should.be.eq('V1'); + (await this.app.getProxyImplementation(this.proxy.address)).should.be.eq(this.implV1.address) + }); + }; + + describe('without initializer', function () { + beforeEach('creating a proxy', createProxy); + shouldReturnProxy(); + }); + + describe('with initializer', function () { + beforeEach('creating a proxy', async function () { + this.proxy = await this.app.createProxy(ImplV1, packageName, contractName, 'initialize', [10]); + }); + + shouldReturnProxy(); + + it('should have initialized the proxy', async function () { + (await this.proxy.value()).toNumber().should.eq(10); + }); + }); + + describe('with complex initializer', function () { + beforeEach('creating a proxy', async function () { + this.proxy = await this.app.createProxy(ImplV1, packageName, contractName, 'initialize', [10, "foo", []]); + }); + + shouldReturnProxy(); + + it('should have initialized the proxy', async function () { + (await this.proxy.value()).toNumber().should.eq(10); + (await this.proxy.text()).should.eq("foo"); + await this.proxy.values(0).should.be.rejected; + }); + }); + }); + + describe('upgradeProxy', function () { + beforeEach('setting implementation', setImplementation); + beforeEach('create proxy', createProxy); + beforeEach('setting new implementation', setNewImplementation) + + const shouldUpgradeProxy = function () { + it('should upgrade proxy to ImplV2', async function () { + (await this.proxy.version()).should.be.eq('V2'); + (await this.app.getProxyImplementation(this.proxy.address)).should.be.eq(this.implV2.address) + }); + }; + + describe('without call', function () { + beforeEach('upgrading the proxy', async function () { + await this.app.upgradeProxy(this.proxy.address, ImplV2, packageName, contractName); + }); + + shouldUpgradeProxy(); + }); + + describe('with call', function () { + beforeEach('upgrading the proxy', async function () { + await this.app.upgradeProxy(this.proxy.address, ImplV2, packageName, contractName, 'migrate', [20]); + }); + + shouldUpgradeProxy(); + + it('should run migration', async function () { + (await this.proxy.value()).toNumber().should.eq(20); + }); + }); + }); + + describe('changeProxyAdmin', function () { + beforeEach('setting implementation', setImplementation); + beforeEach('create proxy', createProxy); + + it('should change proxy admin', async function () { + await this.app.changeProxyAdmin(this.proxy.address, otherAdmin); + const proxyWrapper = Proxy.at(this.proxy.address); + const actualAdmin = await proxyWrapper.admin(); + actualAdmin.should.be.eq(otherAdmin); + }); + }); + + async function createProxy () { + this.proxy = await this.app.createProxy(ImplV1, packageName, contractName); + }; +}; diff --git a/packages/lib/test/src/app/UnversionedApp.test.js b/packages/lib/test/src/app/UnversionedApp.test.js new file mode 100644 index 000000000..77236e52d --- /dev/null +++ b/packages/lib/test/src/app/UnversionedApp.test.js @@ -0,0 +1,68 @@ +'use strict'; +require('../../setup') + +import UnversionedApp from '../../../src/app/UnversionedApp'; +import shouldBehaveLikeApp from './BaseApp.behavior'; +import FreezableImplementationDirectory from '../../../src/directory/FreezableImplementationDirectory'; +import { deploy as deployContract } from '../../../src/utils/Transactions'; +import Contracts from '../../../src/utils/Contracts' + +const ImplV1 = Contracts.getFromLocal('DummyImplementation'); +const ImplV2 = Contracts.getFromLocal('DummyImplementationV2'); + +contract('UnversionedApp', function (accounts) { + const [_, owner] = accounts; + const txParams = { from: owner }; + const contractName = 'Impl'; + const packageName = 'MyPackage'; + const anotherPackageName = 'AnotherPackage'; + + beforeEach('deploying', async function deploy () { + this.app = await UnversionedApp.deploy(txParams) + }) + + shouldBehaveLikeApp(UnversionedApp, accounts, { + setImplementation: async function () { + this.directory = await FreezableImplementationDirectory.deployLocal([], txParams) + this.implV1 = await deployContract(ImplV1) + await this.directory.setImplementation(contractName, this.implV1.address) + await this.app.setProvider(packageName, this.directory.address) + }, + + setNewImplementation: async function () { + this.directory = await FreezableImplementationDirectory.deployLocal([], txParams) + this.implV2 = await deployContract(ImplV2) + await this.directory.setImplementation(contractName, this.implV2.address) + await this.app.setProvider(packageName, this.directory.address) + } + }) + + describe('setProvider', function () { + it('can set multiple providers', async function () { + const directory1 = await FreezableImplementationDirectory.deployLocal([], txParams); + const directory2 = await FreezableImplementationDirectory.deployLocal([], txParams); + + await this.app.setProvider(packageName, directory1.address); + await this.app.setProvider(anotherPackageName, directory2.address); + + const provider1 = await this.app.getProvider(packageName); + provider1.address.should.eq(directory1.address); + + const provider2 = await this.app.getProvider(anotherPackageName); + provider2.address.should.eq(directory2.address); + }) + }) + + describe('unsetProvider', function () { + beforeEach('setting provider', async function () { + this.directory = await FreezableImplementationDirectory.deployLocal([], txParams) + await this.app.setProvider(packageName, this.directory.address) + }) + + it('unsets a provider', async function () { + await this.app.unsetProvider(packageName) + const hasProvider = await this.app.hasProvider(packageName) + hasProvider.should.be.false + }) + }) +}); diff --git a/packages/lib/test/src/app/VersionedApp.test.js b/packages/lib/test/src/app/VersionedApp.test.js new file mode 100644 index 000000000..4e48b3ce3 --- /dev/null +++ b/packages/lib/test/src/app/VersionedApp.test.js @@ -0,0 +1,116 @@ +'use strict'; +require('../../setup') + +import VersionedApp from '../../../src/app/VersionedApp'; +import shouldBehaveLikeApp from './BaseApp.behavior'; +import FreezableImplementationDirectory from '../../../src/directory/FreezableImplementationDirectory'; +import { deploy as deployContract } from '../../../src/utils/Transactions'; +import Contracts from '../../../src/utils/Contracts' +import Package from '../../../src/package/Package'; + +const ImplV1 = Contracts.getFromLocal('DummyImplementation'); +const ImplV2 = Contracts.getFromLocal('DummyImplementationV2'); + +contract('VersionedApp', function (accounts) { + const [_, owner] = accounts; + const txParams = { from: owner }; + const contractName = 'Impl'; + const packageName = 'MyPackage'; + const anotherPackageName = 'AnotherPackage'; + const version = '1.0'; + const anotherVersion = '2.0'; + + beforeEach('deploying', async function deploy () { + this.app = await VersionedApp.deploy(txParams) + }) + + shouldBehaveLikeApp(VersionedApp, accounts, { + setImplementation: async function () { + this.package = await Package.deploy(txParams) + this.directory = await this.package.newVersion(version) + this.implV1 = await deployContract(ImplV1) + await this.directory.setImplementation(contractName, this.implV1.address) + await this.app.setPackage(packageName, this.package.address, version) + }, + + setNewImplementation: async function () { + this.directory = await this.package.newVersion(anotherVersion) + this.implV2 = await deployContract(ImplV2) + await this.directory.setImplementation(contractName, this.implV2.address) + await this.app.setPackage(packageName, this.package.address, anotherVersion) + } + }) + + async function setPackage() { + this.package = await Package.deploy(txParams) + this.directory = await this.package.newVersion(version) + await this.app.setPackage(packageName, this.package.address, version) + } + + describe('getPackage', function () { + beforeEach('setting package', setPackage) + + it('returns package info', async function () { + const packageInfo = await this.app.getPackage(packageName); + packageInfo.package.address.should.eq(this.package.address) + packageInfo.package.should.be.instanceof(Package) + packageInfo.version.should.eq(version) + }) + + it('returns empty info if not exists', async function () { + const packageInfo = await this.app.getPackage('NOTEXISTS'); + (packageInfo.package === null).should.be.true + }) + }) + + describe('hasPackage', function () { + beforeEach('setting package', setPackage) + + it('returns true if exists', async function () { + const hasPackage = await this.app.hasPackage(packageName) + hasPackage.should.be.true + }) + + it('returns false if not exists', async function () { + const hasPackage = await this.app.hasPackage('NOTEXISTS') + hasPackage.should.be.false + }) + }) + + describe('setPackage', function () { + it('can set multiple packages', async function () { + const package1 = await Package.deploy(txParams) + const package2 = await Package.deploy(txParams) + + const directory1 = await package1.newVersion(version) + const directory2 = await package2.newVersion(anotherVersion) + + await this.app.setPackage(packageName, package1.address, version); + await this.app.setPackage(anotherPackageName, package2.address, anotherVersion); + + const provider1 = await this.app.getProvider(packageName); + provider1.address.should.eq(directory1.address); + + const provider2 = await this.app.getProvider(anotherPackageName); + provider2.address.should.eq(directory2.address); + }) + + it('can overwrite package version', async function () { + await setPackage.apply(this) + await this.package.newVersion(anotherVersion) + await this.app.setPackage(packageName, this.package.address, anotherVersion) + const packageInfo = await this.app.getPackage(packageName) + packageInfo.version.should.eq(anotherVersion) + }) + }) + + describe('unsetPackage', function () { + beforeEach('setting package', setPackage) + + it('unsets a provider', async function () { + await this.app.unsetPackage(packageName) + const hasProvider = await this.app.hasProvider(packageName) + hasProvider.should.be.false + }) + }) +}); diff --git a/packages/lib/test/src/directory/AppDirectory.test.js b/packages/lib/test/src/directory/AppDirectory.test.js deleted file mode 100644 index c3e9ef46a..000000000 --- a/packages/lib/test/src/directory/AppDirectory.test.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict' -require('../../setup') - -import Contracts from '../../../src/utils/Contracts' -import AppDirectory from '../../../src/directory/AppDirectory' - -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') -const ImplementationDirectory = Contracts.getFromLocal('ImplementationDirectory') - -contract('AppDirectory', ([_, appOwner, stdlibOwner, anotherAddress]) => { - const txParams = { from: appOwner } - - beforeEach('deploying app directory', async function () { - this.stdlib = await ImplementationDirectory.new({ from: stdlibOwner }) - this.directory = await AppDirectory.deploy(this.stdlib.address, txParams) - }) - - it('has an address', async function () { - (await this.directory.address).should.not.be.null - }) - - it('has an stdlib', async function () { - (await this.directory.stdlib()).should.be.eq(this.stdlib.address) - }) - - it('has an owner', async function () { - (await this.directory.owner()).should.be.equal(appOwner) - }) - - it('can set new implementations', async function () { - const implementation = await DummyImplementation.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.eq(implementation.address) - }) - - it('can unset implementations', async function () { - const implementation = await DummyImplementation.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - await this.directory.unsetImplementation('DummyImplementation') - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.zeroAddress - }) - - it('can retrieve an implementation from the stdlib if not registered', async function () { - let currentImplementation = await this.directory.getImplementation('DummyImplementation'); - currentImplementation.should.be.zeroAddress - - const implementation = await DummyImplementation.new() - await this.stdlib.setImplementation('DummyImplementation', implementation.address, { from: stdlibOwner }) - - currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.eq(implementation.address) - }) - - it('can set another stdlib', async function () { - const anotherStdlib = await ImplementationDirectory.new({ from: stdlibOwner }) - - await this.directory.setStdlib(anotherStdlib.address) - - const currentStdlib = await this.directory.stdlib(); - currentStdlib.should.be.eq(anotherStdlib.address) - }) -}) diff --git a/packages/lib/test/src/directory/BaseImplementationDirectory.behavior.js b/packages/lib/test/src/directory/BaseImplementationDirectory.behavior.js new file mode 100644 index 000000000..8c1941dd3 --- /dev/null +++ b/packages/lib/test/src/directory/BaseImplementationDirectory.behavior.js @@ -0,0 +1,66 @@ +'use strict' +require('../../setup') + +import Contracts from '../../../src/utils/Contracts' + +const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') + +export default function shouldBehaveLikeImplementationDirectory(directoryClass, accounts, { onDeployed } = {}) { + const [_, owner] = accounts + const txParams = { from: owner } + + describe('deployLocal', function () { + const contractName = 'DummyImplementation' + const contracts = [{ alias: contractName, name: contractName }] + + beforeEach('deploying implementation directory', async function () { + this.directory = await directoryClass.deployLocal(contracts, txParams) + }) + + shouldBeDeployed(contractName) + if (onDeployed) onDeployed() + }) + + describe('deployDependency', function () { + const contractName = 'Greeter' + const contracts = [{ alias: contractName, name: contractName }] + + beforeEach('deploying implementation directory', async function () { + this.directory = await directoryClass.deployDependency('mock-dependency', contracts, txParams) + }) + + shouldBeDeployed(contractName) + if (onDeployed) onDeployed() + }) + + function shouldBeDeployed(expectedContractName) { + it('has an address', async function () { + (await this.directory.address).should.not.be.null + }) + + it('has an owner', async function () { + (await this.directory.owner()).should.be.equal(owner) + }) + + it('includes the given contracts', async function () { + (await this.directory.getImplementation(expectedContractName)).should.not.be.zero + }) + + it('can set new implementations', async function () { + const implementation = await DummyImplementationV2.new() + await this.directory.setImplementation('DummyImplementation', implementation.address) + + const currentImplementation = await this.directory.getImplementation('DummyImplementation'); + currentImplementation.should.be.eq(implementation.address) + }) + + it('can unset implementations', async function () { + const implementation = await DummyImplementationV2.new() + await this.directory.setImplementation('DummyImplementation', implementation.address) + await this.directory.unsetImplementation('DummyImplementation') + + const currentImplementation = await this.directory.getImplementation('DummyImplementation') + currentImplementation.should.be.zeroAddress + }) + } +} diff --git a/packages/lib/test/src/directory/FreezableImplementationDirectory.test.js b/packages/lib/test/src/directory/FreezableImplementationDirectory.test.js index 5d249987d..4cc0f59c0 100644 --- a/packages/lib/test/src/directory/FreezableImplementationDirectory.test.js +++ b/packages/lib/test/src/directory/FreezableImplementationDirectory.test.js @@ -1,29 +1,11 @@ 'use strict' require('../../setup') -import Contracts from '../../../src/utils/Contracts' import FreezableImplementationDirectory from '../../../src/directory/FreezableImplementationDirectory' +import shouldBehaveLikeImplementationDirectory from './BaseImplementationDirectory.behavior'; -const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') - -contract('FreezableImplementationDirectory', ([_, owner]) => { - const txParams = { from: owner } - - describe('deployLocal', function () { - const contracts = [{ alias: 'DummyImplementation', name: 'DummyImplementation' }] - - beforeEach('deploying freezable implementation directory', async function () { - this.directory = await FreezableImplementationDirectory.deployLocal(contracts, txParams) - }) - - it('has an address', async function () { - (await this.directory.address).should.not.be.null - }) - - it('has an owner', async function () { - (await this.directory.owner()).should.be.equal(owner) - }) - +contract('FreezableImplementationDirectory', function(accounts) { + shouldBehaveLikeImplementationDirectory(FreezableImplementationDirectory, accounts, { onDeployed: function () { it('can be frozen', async function () { let frozen = await this.directory.isFrozen(); frozen.should.be.false @@ -33,73 +15,5 @@ contract('FreezableImplementationDirectory', ([_, owner]) => { frozen = await this.directory.isFrozen() frozen.should.be.true }) - - it('includes the given contracts', async function () { - (await this.directory.getImplementation('DummyImplementation')).should.not.be.zero - }) - - it('can set new implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - - const currentImplementation = await this.directory.getImplementation('DummyImplementation'); - currentImplementation.should.be.eq(implementation.address) - }) - - it('can unset implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - await this.directory.unsetImplementation('DummyImplementation') - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.zeroAddress - }) - }) - - describe('deployDependency', function () { - const contracts = [{ alias: 'Greeter', name: 'Greeter' }] - - beforeEach('deploying freezable implementation directory', async function () { - this.directory = await FreezableImplementationDirectory.deployDependency('mock-dependency', contracts, txParams) - }) - - it('has an address', async function () { - (await this.directory.address).should.not.be.null - }) - - it('has an owner', async function () { - (await this.directory.owner()).should.be.equal(owner) - }) - - it('can be frozen', async function () { - let frozen = await this.directory.isFrozen(); - frozen.should.be.false - - await this.directory.freeze().should.eventually.be.fulfilled - - frozen = await this.directory.isFrozen() - frozen.should.be.true - }) - - it('includes the given contracts', async function () { - (await this.directory.getImplementation('Greeter')).should.not.be.zero - }) - - it('can set new implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.eq(implementation.address) - }) - - it('can unset implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - await this.directory.unsetImplementation('DummyImplementation') - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.zeroAddress - }) - }) -}) + }}) +}) \ No newline at end of file diff --git a/packages/lib/test/src/directory/ImplementationDirectory.test.js b/packages/lib/test/src/directory/ImplementationDirectory.test.js index 38f9201fb..84659fbce 100644 --- a/packages/lib/test/src/directory/ImplementationDirectory.test.js +++ b/packages/lib/test/src/directory/ImplementationDirectory.test.js @@ -1,85 +1,9 @@ 'use strict' require('../../setup') -import Contracts from '../../../src/utils/Contracts' import ImplementationDirectory from '../../../src/directory/ImplementationDirectory' +import shouldBehaveLikeImplementationDirectory from './BaseImplementationDirectory.behavior'; -const DummyImplementationV2 = Contracts.getFromLocal('DummyImplementationV2') - -contract('ImplementationDirectory', ([_, owner, anotherAddress]) => { - const txParams = { from: owner } - - describe('deployLocal', function () { - const contracts = [{ alias: 'DummyImplementation', name: 'DummyImplementation' }] - - beforeEach('deploying implementation directory', async function () { - this.directory = await ImplementationDirectory.deployLocal(contracts, txParams) - }) - - it('has an address', async function () { - (await this.directory.address).should.not.be.null - }) - - it('has an owner', async function () { - (await this.directory.owner()).should.be.equal(owner) - }) - - it('includes the given contracts', async function () { - (await this.directory.getImplementation('DummyImplementation')).should.not.be.zero - }) - - it('can set new implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - - const currentImplementation = await this.directory.getImplementation('DummyImplementation'); - currentImplementation.should.be.eq(implementation.address) - }) - - it('can unset implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - await this.directory.unsetImplementation('DummyImplementation') - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.zeroAddress - }) - }) - - describe('deployDependency', function () { - const contracts = [{ alias: 'Greeter', name: 'Greeter' }] - - beforeEach('deploying implementation directory', async function () { - this.directory = await ImplementationDirectory.deployDependency('mock-dependency', contracts, txParams) - }) - - it('has an address', async function () { - (await this.directory.address).should.not.be.null - }) - - it('has an owner', async function () { - (await this.directory.owner()).should.be.equal(owner) - }) - - it('includes the given contracts', async function () { - (await this.directory.getImplementation('Greeter')).should.not.be.zero - }) - - it('can set new implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - - const currentImplementation = await this.directory.getImplementation('DummyImplementation'); - currentImplementation.should.be.eq(implementation.address) - }) - - it('can unset implementations', async function () { - const implementation = await DummyImplementationV2.new() - await this.directory.setImplementation('DummyImplementation', implementation.address) - await this.directory.unsetImplementation('DummyImplementation') - - const currentImplementation = await this.directory.getImplementation('DummyImplementation') - currentImplementation.should.be.zeroAddress - }) - }) +contract('ImplementationDirectory', function(accounts) { + shouldBehaveLikeImplementationDirectory(ImplementationDirectory, accounts) }) diff --git a/packages/lib/test/src/factory/UpgradeabilityProxyFactory.test.js b/packages/lib/test/src/factory/UpgradeabilityProxyFactory.test.js new file mode 100644 index 000000000..5170cc9aa --- /dev/null +++ b/packages/lib/test/src/factory/UpgradeabilityProxyFactory.test.js @@ -0,0 +1,34 @@ +'use strict' +require('../../setup') + +import UpgradeabilityProxyFactory from '../../../src/factory/UpgradeabilityProxyFactory'; + +contract('UpgradeabilityProxyFactory', function ([_, owner]) { + + const txParams = { from: owner } + + beforeEach('deploying', async function () { + this.factory = await UpgradeabilityProxyFactory.deploy(txParams) + }) + + describe('deploy', function () { + shouldInitialize() + }) + + describe('fetch', function () { + beforeEach('fetching', async function () { + const address = this.factory.address + this.factory = await UpgradeabilityProxyFactory.fetch(address, txParams) + }) + + shouldInitialize() + }) + + function shouldInitialize() { + it('initializes the factory', async function () { + this.factory.contract.should.be.not.null + this.factory.address.should.be.nonzeroAddress + }) + } + +}) \ No newline at end of file diff --git a/packages/lib/test/src/package/Package.test.js b/packages/lib/test/src/package/Package.test.js new file mode 100644 index 000000000..943f18cdd --- /dev/null +++ b/packages/lib/test/src/package/Package.test.js @@ -0,0 +1,117 @@ +'use strict' +require('../../setup') + +import Package from '../../../src/package/Package' +import Contracts from '../../../src/utils/Contracts' +import { deploy as deployContract } from '../../../src/utils/Transactions' +import FreezableImplementationDirectory from '../../../src/directory/FreezableImplementationDirectory'; +import ImplementationDirectory from '../../../src/directory/ImplementationDirectory'; + +const DummyImplementation = Contracts.getFromLocal('DummyImplementation') + +contract('Package', function ([_, owner]) { + const txParams = { from: owner } + const contractName = 'DummyImplementation' + const version = "1.0" + const version2 = "2.0" + + shouldBehaveLikePackage(ImplementationDirectory) + + shouldBehaveLikePackage(FreezableImplementationDirectory, { onNewVersion: function () { + it('is freezable', async function () { + await this.package.freeze(version) + const frozen = await this.package.isFrozen(version) + frozen.should.be.true + }) + }}) + + function shouldBehaveLikePackage(directoryClass, { onNewVersion } = {}) { + describe(`with ${directoryClass.name}`, function () { + const shouldInitialize = function () { + it('instantiates the package', async function() { + this.package.contract.should.not.be.null + this.package.address.should.be.nonzeroAddress + }) + } + + const deploy = async function () { + this.package = await Package.deploy(txParams, directoryClass) + } + + describe('deploy', function () { + beforeEach('deploying package', deploy) + shouldInitialize() + }) + + describe('fetch', function () { + beforeEach('deploying package', deploy) + beforeEach("connecting to existing instance", async function () { + this.package = await Package.fetch(this.package.address, txParams, directoryClass) + }) + shouldInitialize() + }) + + describe('newVersion', function () { + beforeEach('deploying package', deploy) + beforeEach('adding a new version', async function () { + await this.package.newVersion(version) + }) + + it('returns version directory', async function () { + const directory = await this.package.getDirectory(version) + directory.address.should.be.nonzeroAddress + directory.should.be.instanceof(directoryClass) + }) + + it('registers new version on package', async function () { + const hasVersion = await this.package.hasVersion(version) + hasVersion.should.be.true + }) + + it('is not frozen by default', async function () { + const frozen = await this.package.isFrozen(version) + frozen.should.be.false + }) + + if (onNewVersion) { + onNewVersion() + } + }) + + describe('setImplementation', function () { + beforeEach('deploying package', deploy) + + beforeEach('setting versions', async function () { + await this.package.newVersion(version) + await this.package.newVersion(version2) + }) + + beforeEach('setting an implementation', async function () { + this.implementation = await deployContract(DummyImplementation) + await this.package.setImplementation(version, contractName, this.implementation) + }) + + it('gets the implementation from the correct version', async function () { + const implementation = await this.package.getImplementation(version, contractName) + implementation.should.eq(this.implementation.address) + }) + + it('returns zero when requesting from another version', async function () { + const implementation = await this.package.getImplementation(version2, contractName) + implementation.should.be.zeroAddress + }) + + it('returns zero when requesting another contract name', async function () { + const implementation = await this.package.getImplementation(version, 'NOTEXISTS') + implementation.should.be.zeroAddress + }) + + it('unsets the implementation', async function () { + await this.package.unsetImplementation(version, contractName) + const implementation = await this.package.getImplementation(version, contractName) + implementation.should.be.zeroAddress + }) + }) + }) + } +}) diff --git a/packages/lib/test/src/package/PackageWithAppDirectories.test.js b/packages/lib/test/src/package/PackageWithAppDirectories.test.js deleted file mode 100644 index 878352fa2..000000000 --- a/packages/lib/test/src/package/PackageWithAppDirectories.test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' -require('../../setup') - -import Package from '../../../src/package/Package' -import Contracts from '../../../src/utils/Contracts' - -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') - -contract('PackageWithAppDirectories', function ([_, owner]) { - const txParams = { from: owner } - const contractName = 'DummyImplementation' - const version = "1.0" - - const shouldInitialize = function () { - it('instantiates the package', async function() { - this.package.address.should.not.be.null - }) - } - - beforeEach('deploying package with app directories', async function () { - this.package = await Package.deploy(txParams) - }) - - describe('deploy', function () { - shouldInitialize() - }) - - describe('fetch', function () { - beforeEach('connecting to existing instance', async function () { - this.package = Package.fetch(this.package.address, txParams) - }) - - shouldInitialize() - }) - - const addNewVersion = async function () { - await this.package.newVersion(version) - } - - describe('newVersion', function () { - beforeEach('adding a new version', addNewVersion) - - it('registers new version on package', async function () { - const hasVersion = await this.package.hasVersion(version) - hasVersion.should.be.true - }) - }) - - describe('get and set implementation', function () { - beforeEach('adding a new version', addNewVersion) - - it('allows to register new implementations', async function() { - const newImplementation = await this.package.setImplementation(version, DummyImplementation, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.eq(newImplementation.address) - }) - - it('allows to register the same implementations twice', async function() { - await this.package.setImplementation(version, DummyImplementation, contractName) - const newImplementation = await this.package.setImplementation(version, DummyImplementation, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.eq(newImplementation.address) - }) - - it('allows to unset an implementation', async function () { - await this.package.setImplementation(version, DummyImplementation, contractName) - await this.package.unsetImplementation(version, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.be.zeroAddress - }) - }) -}) diff --git a/packages/lib/test/src/package/PackageWithFreezableDirectories.test.js b/packages/lib/test/src/package/PackageWithFreezableDirectories.test.js deleted file mode 100644 index 5a42c1230..000000000 --- a/packages/lib/test/src/package/PackageWithFreezableDirectories.test.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' -require('../../setup') - -import Package from '../../../src/package/Package' -import Contracts from '../../../src/utils/Contracts' -import assertRevert from '../../../src/test/helpers/assertRevert' - -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') - -contract('PackageWithFreezableDirectories', function ([_, owner]) { - const txParams = { from: owner } - const contractName = 'DummyImplementation' - const version = "1.0" - - const shouldInitialize = function () { - it('instantiates the package', async function() { - this.package.address.should.not.be.null - }) - } - - beforeEach('deploying package with freezable directories', async function () { - this.package = await Package.deployWithFreezableDirectories(txParams) - }) - - describe('deploy', function () { - shouldInitialize() - }) - - describe('fetch', function () { - beforeEach("connecting to existing instance", async function () { - this.package = Package.fetchWithFreezableDirectories(this.package.address, txParams) - }) - - shouldInitialize() - }) - - const addNewVersion = async function () { - await this.package.newVersion(version) - } - - describe('newVersion', function () { - beforeEach('adding a new version', addNewVersion) - - it('registers new version on package', async function () { - const hasVersion = await this.package.hasVersion(version) - hasVersion.should.be.true - }) - }) - - describe('freeze', function() { - beforeEach('adding a new version', addNewVersion) - - it('should not be frozen by default', async function () { - const frozen = await this.package.isFrozen(version) - frozen.should.be.false - }) - - it('should be freezable', async function () { - await this.package.freeze(version) - const frozen = await this.package.isFrozen(version) - frozen.should.be.true - }) - }) - - describe('get and set implementation', function () { - beforeEach('adding a new version', addNewVersion) - - describe('when current version is not frozen', async function() { - - it('allows to register new implementations', async function() { - const newImplementation = await this.package.setImplementation(version, DummyImplementation, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.eq(newImplementation.address) - }) - - it('allows to unset an implementation', async function () { - await this.package.setImplementation(version, DummyImplementation, contractName) - await this.package.unsetImplementation(version, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.be.zeroAddress - }) - }) - - describe('when current version is frozen', async function() { - beforeEach('freezing', async function() { - await this.package.freeze(version) - }) - - it('does not allow to register new implementations', async function() { - await assertRevert(this.package.setImplementation(version, DummyImplementation, contractName)) - }) - - // TODO: uncomment this test once the patch has been merged - xit('does not allow to unset an implementations', async function() { - await assertRevert(this.package.unsetImplementation(version, contractName)) - }) - }) - }) -}) diff --git a/packages/lib/test/src/package/PackageWithNonFreezableDirectories.test.js b/packages/lib/test/src/package/PackageWithNonFreezableDirectories.test.js deleted file mode 100644 index 196415f58..000000000 --- a/packages/lib/test/src/package/PackageWithNonFreezableDirectories.test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' -require('../../setup') - -import Package from '../../../src/package/Package' -import Contracts from '../../../src/utils/Contracts' - -const DummyImplementation = Contracts.getFromLocal('DummyImplementation') - -contract('PackageWithNonFreezableDirectories', function ([_, owner]) { - const txParams = { from: owner } - const contractName = 'DummyImplementation' - const version = "1.0" - - const shouldInitialize = function () { - it('instantiates the package', async function() { - this.package.address.should.not.be.null - }) - } - - beforeEach('deploying package with non-freezable directories', async function () { - this.package = await Package.deployWithNonFreezableDirectories(txParams) - }) - - describe('deploy', function () { - shouldInitialize() - }) - - describe('fetch', function () { - beforeEach("connecting to existing instance", async function () { - this.package = Package.fetchWithNonFreezableDirectories(this.package.address, txParams) - }) - - shouldInitialize() - }) - - const addNewVersion = async function () { - await this.package.newVersion(version) - } - - describe('newVersion', function () { - beforeEach('adding a new version', addNewVersion) - - it('registers new version on package', async function () { - const hasVersion = await this.package.hasVersion(version) - hasVersion.should.be.true - }) - }) - - describe('get and set implementation', function () { - beforeEach('adding a new version', addNewVersion) - - it('allows to register new implementations', async function() { - const newImplementation = await this.package.setImplementation(version, DummyImplementation, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.eq(newImplementation.address) - }) - - it('allows to register the same implementations twice', async function() { - await this.package.setImplementation(version, DummyImplementation, contractName) - const newImplementation = await this.package.setImplementation(version, DummyImplementation, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.eq(newImplementation.address) - }) - - it('allows to unset an implementation', async function () { - await this.package.setImplementation(version, DummyImplementation, contractName) - await this.package.unsetImplementation(version, contractName) - - const implementation = await this.package.getImplementation(version, contractName) - implementation.should.be.zeroAddress - }) - }) -}) diff --git a/packages/lib/test/src/project/AppProject.test.js b/packages/lib/test/src/project/AppProject.test.js new file mode 100644 index 000000000..e75d29cae --- /dev/null +++ b/packages/lib/test/src/project/AppProject.test.js @@ -0,0 +1,33 @@ +'use strict' +require('../../setup') + +import AppProject from '../../../src/project/AppProject' +import shouldBehaveLikeProject from './Project.behavior'; + +contract('AppProject', function (accounts) { + const [_, owner] = accounts + const name = 'MyProject' + const version = '0.2.0' + const newVersion = '0.3.0' + + const deploy = async function () { + this.project = await AppProject.deploy(name, version, { from: owner }) + } + + const fetch = async function () { + const app = await this.project.getApp() + this.project = await AppProject.fetch(app.address, name, { from: owner }) + } + + const onNewVersion = function () { + it('registers the new package version in the app', async function () { + const app = await this.project.getApp() + const thepackage = await this.project.getProjectPackage() + const packageInfo = await app.getPackage(name) + packageInfo.version.should.eq(newVersion) + packageInfo.package.should.eq(thepackage.address) + }) + } + + shouldBehaveLikeProject(deploy, fetch, onNewVersion) +}) \ No newline at end of file diff --git a/packages/lib/test/src/project/LibProject.test.js b/packages/lib/test/src/project/LibProject.test.js new file mode 100644 index 000000000..54d802842 --- /dev/null +++ b/packages/lib/test/src/project/LibProject.test.js @@ -0,0 +1,21 @@ +'use strict' +require('../../setup') + +import LibProject from '../../../src/project/LibProject' +import shouldBehaveLikeProject from './Project.behavior'; + +contract('LibProject', function (accounts) { + const [_, owner] = accounts + const version = '0.2.0' + + const deploy = async function () { + this.project = await LibProject.deploy(version, { from: owner }) + } + + const fetch = async function () { + const thepackage = await this.project.getProjectPackage() + this.project = await LibProject.fetch(thepackage.address, version, { from: owner }) + } + + shouldBehaveLikeProject(deploy, fetch) +}) \ No newline at end of file diff --git a/packages/lib/test/src/project/Project.behavior.js b/packages/lib/test/src/project/Project.behavior.js new file mode 100644 index 000000000..eb1e298bf --- /dev/null +++ b/packages/lib/test/src/project/Project.behavior.js @@ -0,0 +1,89 @@ +'use strict' +require('../../setup') + +import Contracts from '../../../src/utils/Contracts'; + +const DummyImplementation = Contracts.getFromLocal('DummyImplementation') + +export default function shouldBehaveLikeProject(deploy, fetch, { onNewVersion } = {}) { + const version = '0.2.0' + const newVersion = '0.3.0' + const contractName = 'Dummy' + + const shouldInitializePackage = function () { + it('creates a package', async function () { + const thepackage = await this.project.getProjectPackage() + thepackage.address.should.be.nonzeroAddress + }) + + it('creates a directory', async function () { + const directory = await this.project.getCurrentDirectory() + directory.address.should.be.nonzeroAddress + }) + + it('registers directory in the package', async function () { + const thepackage = await this.project.getProjectPackage() + const packageDirectory = await thepackage.getDirectory(version) + const projectDirectory = await this.project.getCurrentDirectory() + packageDirectory.address.should.eq(projectDirectory.address) + }) + } + + beforeEach('deploying project', deploy) + + describe('deploy', function () { + shouldInitializePackage() + }) + + describe('fetch', function () { + beforeEach('fetching project', fetch) + shouldInitializePackage() + }) + + describe('newVersion', function () { + beforeEach('creating new version', async function () { + await this.project.newVersion(newVersion) + }) + + it('sets a new version', async function () { + this.project.version.should.eq(newVersion) + }) + + it('registers a new directory', async function () { + const thepackage = await this.project.getProjectPackage() + const packageDirectory = await thepackage.getDirectory(newVersion) + const projectDirectory = await this.project.getCurrentDirectory() + packageDirectory.address.should.eq(projectDirectory.address) + }) + + if (onNewVersion) onNewVersion() + }) + + describe('setImplementation', function () { + it('registers a new implementation', async function() { + const newImplementation = await this.project.setImplementation(DummyImplementation, contractName) + const thepackage = await this.project.getProjectPackage() + const implementation = await thepackage.getImplementation(version, contractName) + implementation.should.eq(newImplementation.address) + }) + + it('registers a new implementation on a new version', async function() { + await this.project.newVersion(newVersion) + const newImplementation = await this.project.setImplementation(DummyImplementation, contractName) + const thepackage = await this.project.getProjectPackage() + const implementation = await thepackage.getImplementation(newVersion, contractName) + implementation.should.eq(newImplementation.address) + }) + }) + + describe('unsetImplementation', function () { + it('unsets an implementation', async function () { + await this.project.setImplementation(DummyImplementation, contractName) + await this.project.unsetImplementation(contractName) + + const thepackage = await this.project.getCurrentDirectory() + const implementation = await thepackage.getImplementation(version, contractName) + implementation.should.be.zeroAddress + }) + }) +}; \ No newline at end of file