From 164e2b63cad3d2443502e16c2d0bd5b1849e9576 Mon Sep 17 00:00:00 2001 From: ryley-o <30364988+ryley-o@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:46:19 -0700 Subject: [PATCH 1/3] Update dependency registy with three new script fields --- .../contracts/DependencyRegistryV0.sol | 151 +++++++++++------- .../generator/GenArt721GeneratorV0.sol | 28 ++-- .../v0.8.x/IDependencyRegistryV0.sol | 81 +++++++--- .../v0.8.x/DependencyRegistryStorageLib.sol | 8 + 4 files changed, 170 insertions(+), 98 deletions(-) diff --git a/packages/contracts/contracts/DependencyRegistryV0.sol b/packages/contracts/contracts/DependencyRegistryV0.sol index 14ff01d6d..eb62403ea 100644 --- a/packages/contracts/contracts/DependencyRegistryV0.sol +++ b/packages/contracts/contracts/DependencyRegistryV0.sol @@ -642,6 +642,76 @@ contract DependencyRegistryV0 is ); } + /** + * @notice Updates the canvas tag for dependency `dependencyNameAndVersion`. + * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. + * @param canvasTag The new canvas tag for the dependency. + */ + function updateDependencyCanvasTag( + bytes32 dependencyNameAndVersion, + IDependencyRegistryV0.CanvasTag canvasTag + ) external { + _onlyAdminACL(this.updateDependencyCanvasTag.selector); + _onlyExistingDependency(dependencyNameAndVersion); + + DependencyRegistryStorageLib.Dependency + storage dependency = DependencyRegistryStorageLib + .s() + .dependencyRecords[dependencyNameAndVersion]; + dependency.canvasTag = canvasTag; + + emit DependencyCanvasTagUpdated(dependencyNameAndVersion, canvasTag); + } + + /** + * @notice Updates the loadAsModule flag for dependency `dependencyNameAndVersion`. + * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. + * @param loadAsModule The new loadAsModule flag for the dependency. + */ + function updateDependencyLoadAsModule( + bytes32 dependencyNameAndVersion, + bool loadAsModule + ) external { + _onlyAdminACL(this.updateDependencyLoadAsModule.selector); + _onlyExistingDependency(dependencyNameAndVersion); + + DependencyRegistryStorageLib.Dependency + storage dependency = DependencyRegistryStorageLib + .s() + .dependencyRecords[dependencyNameAndVersion]; + dependency.loadAsModule = loadAsModule; + + emit DependencyLoadAsModuleUpdated( + dependencyNameAndVersion, + loadAsModule + ); + } + + /** + * @notice Updates the project script special type for dependency `dependencyNameAndVersion`. + * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. + * @param projectScriptSpecialType The new project script special type for the dependency. + * For example, "type=‘application/processing" for processing.js + */ + function updateDependencyProjectScriptSpecialType( + bytes32 dependencyNameAndVersion, + string memory projectScriptSpecialType + ) external { + _onlyAdminACL(this.updateDependencyProjectScriptSpecialType.selector); + _onlyExistingDependency(dependencyNameAndVersion); + + DependencyRegistryStorageLib.Dependency + storage dependency = DependencyRegistryStorageLib + .s() + .dependencyRecords[dependencyNameAndVersion]; + dependency.projectScriptSpecialType = projectScriptSpecialType; + + emit DependencyProjectScriptSpecialTypeUpdated( + dependencyNameAndVersion, + projectScriptSpecialType + ); + } + /** * @notice These functions were removed in an upgrade, registering and unregistering * contracts are handled by the core registry. @@ -1001,34 +1071,11 @@ contract DependencyRegistryV0 is * @notice Returns details for a given dependency type `dependencyNameAndVersion` input as string. * Reverts if input string does not fit within 32 bytes. * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. - * @return nameAndVersion String representation of `dependencyNameAndVersion`. - * (e.g. "p5js(atSymbol)1.0.0") - * @return licenseType License type for dependency - * @return preferredCDN Preferred CDN URL for dependency - * @return additionalCDNCount Count of additional CDN URLs for dependency - * @return preferredRepository Preferred repository URL for dependency - * @return additionalRepositoryCount Count of additional repository URLs for dependency - * @return dependencyWebsite Project website URL for dependency - * @return availableOnChain Whether dependency is available on chain - * @return scriptCount Count of on-chain scripts for dependency + * @return dependencyDetails Details for a given dependency type. */ function getDependencyDetailsFromString( string memory dependencyNameAndVersion - ) - external - view - returns ( - string memory nameAndVersion, - string memory licenseType, - string memory preferredCDN, - uint24 additionalCDNCount, - string memory preferredRepository, - uint24 additionalRepositoryCount, - string memory dependencyWebsite, - bool availableOnChain, - uint24 scriptCount - ) - { + ) external view returns (DependencyDetails memory dependencyDetails) { _onlyBytes32String(dependencyNameAndVersion); return getDependencyDetails(dependencyNameAndVersion.stringToBytes32()); } @@ -1377,50 +1424,32 @@ contract DependencyRegistryV0 is /** * @notice Returns details for a given dependency type `dependencyNameAndVersion`. * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. - * @return nameAndVersion String representation of `dependencyNameAndVersion`. - * (e.g. "p5js(atSymbol)1.0.0") - * @return licenseType License type for dependency - * @return preferredCDN Preferred CDN URL for dependency - * @return additionalCDNCount Count of additional CDN URLs for dependency - * @return preferredRepository Preferred repository URL for dependency - * @return additionalRepositoryCount Count of additional repository URLs for dependency - * @return dependencyWebsite Project website URL for dependency - * @return availableOnChain Whether dependency is available on chain - * @return scriptCount Count of on-chain scripts for dependency + * @return dependencyDetails Details for a given dependency type. */ function getDependencyDetails( bytes32 dependencyNameAndVersion - ) - public - view - returns ( - string memory nameAndVersion, - string memory licenseType, - string memory preferredCDN, - uint24 additionalCDNCount, - string memory preferredRepository, - uint24 additionalRepositoryCount, - string memory dependencyWebsite, - bool availableOnChain, - uint24 scriptCount - ) - { + ) public view returns (DependencyDetails memory dependencyDetails) { DependencyRegistryStorageLib.Dependency storage dependency = DependencyRegistryStorageLib .s() .dependencyRecords[dependencyNameAndVersion]; - return ( - dependencyNameAndVersion.toString(), - dependency.licenseType.toString(), - dependency.preferredCDN, - dependency.additionalCDNCount, - dependency.preferredRepository, - dependency.additionalRepositoryCount, - dependency.website, - dependency.scriptCount > 0, - dependency.scriptCount - ); + dependencyDetails.nameAndVersion = dependencyNameAndVersion.toString(); + dependencyDetails.licenseType = dependency.licenseType.toString(); + dependencyDetails.preferredCDN = dependency.preferredCDN; + dependencyDetails.additionalCDNCount = dependency.additionalCDNCount; + dependencyDetails.preferredRepository = dependency.preferredRepository; + dependencyDetails.additionalRepositoryCount = dependency + .additionalRepositoryCount; + dependencyDetails.dependencyWebsite = dependency.website; + dependencyDetails.availableOnChain = dependency.scriptCount > 0; + dependencyDetails.scriptCount = dependency.scriptCount; + dependencyDetails.canvasTag = dependency.canvasTag; + dependencyDetails.loadAsModule = dependency.loadAsModule; + dependencyDetails.projectScriptSpecialType = dependency + .projectScriptSpecialType; + + // @dev implicit return of dependencyDetails } /** diff --git a/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol b/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol index fae392bcd..1bc0ab363 100644 --- a/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol +++ b/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol @@ -821,17 +821,16 @@ contract GenArt721GeneratorV0 is Initializable, IGenArt721GeneratorV0 { HTMLTag memory htmlTag ) internal view { // Get script count and preferred CDN for the dependency. - ( - , - , - string memory preferredCDN, - , - , - , - , - , - uint24 scriptCount - ) = dependencyRegistry.getDependencyDetails(dependencyNameAndVersion); + string memory preferredCDN; + uint24 scriptCount; + // block scope to avoid stack too deep error + { + IDependencyRegistryV0.DependencyDetails + memory dependencyDetails = dependencyRegistry + .getDependencyDetails(dependencyNameAndVersion); + preferredCDN = dependencyDetails.preferredCDN; + scriptCount = dependencyDetails.scriptCount; + } // If no scripts on-chain, load the script from the preferred CDN. if (scriptCount == 0) { @@ -1021,8 +1020,11 @@ contract GenArt721GeneratorV0 is Initializable, IGenArt721GeneratorV0 { return true; } // query and return result of dependency registry for on-chain status - (, , , , , , , availableOnChain, ) = dependencyRegistry - .getDependencyDetails(dependencyNameAndVersion); + IDependencyRegistryV0.DependencyDetails + memory dependencyDetails = dependencyRegistry.getDependencyDetails( + dependencyNameAndVersion + ); + return dependencyDetails.availableOnChain; } /** diff --git a/packages/contracts/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol b/packages/contracts/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol index 977a645c0..2996758b3 100644 --- a/packages/contracts/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol +++ b/packages/contracts/contracts/interfaces/v0.8.x/IDependencyRegistryV0.sol @@ -87,6 +87,61 @@ interface IDependencyRegistryV0 { event UniversalBytecodeStorageReaderUpdated(address indexed newReader); + event DependencyCanvasTagUpdated( + bytes32 indexed dependencyNameAndVersion, + CanvasTag canvasTag + ); + + event DependencyLoadAsModuleUpdated( + bytes32 indexed dependencyNameAndVersion, + bool loadAsModule + ); + + event DependencyProjectScriptSpecialTypeUpdated( + bytes32 indexed dependencyNameAndVersion, + string projectScriptSpecialType + ); + + /** + * @notice Enum representing the canvas tag requirements for a dependency. + * @dev This enum is used to convey the canvas tag for a dependency. + * @dev NoCanvasTag is the default value. No canvas tag is required to be added to the html when generating the project outputs. + * @dev CanvasBeforeProjectScript is used for dependencies that require a canvas tag to be added before the project script. + * @dev CanvasAfterProjectScript is used for dependencies that require a canvas tag to be added after the project script. + */ + enum CanvasTag { + NoCanvasTag, // default + CanvasBeforeProjectScript, + CanvasAfterProjectScript + } + + struct DependencyDetails { + // name and version of dependency (i.e. "name@version") used to identify dependency + string nameAndVersion; + // type of license, MIT, GPL, etc. + string licenseType; + // preferred CDN URL for dependency + string preferredCDN; + // count of additional CDN URLs for dependency + uint24 additionalCDNCount; + // preferred code repository URL for dependency + string preferredRepository; + // count of additional repository URLs for dependency + uint24 additionalRepositoryCount; + // project website URL for dependency + string dependencyWebsite; + // whether the dependency is available on chain + bool availableOnChain; + // count of on-chain scripts for dependency + uint24 scriptCount; + // canvas tag for the dependency + CanvasTag canvasTag; + // whether the dependency should be loaded as a module + bool loadAsModule; + // project script special type for the dependency + string projectScriptSpecialType; + } + /** * @notice Returns the count of scripts for dependency `dependencyNameAndVersion`. * @param dependencyNameAndVersion Dependency type to be queried. @@ -117,33 +172,11 @@ interface IDependencyRegistryV0 { /** * @notice Returns details for a given dependency type `dependencyNameAndVersion`. * @param dependencyNameAndVersion Name and version of dependency (i.e. "name@version") used to identify dependency. - * @return nameAndVersion String representation of `dependencyNameAndVersion`. - * (e.g. "p5js(atSymbol)1.0.0") - * @return licenseType License type for dependency - * @return preferredCDN Preferred CDN URL for dependency - * @return additionalCDNCount Count of additional CDN URLs for dependency - * @return preferredRepository Preferred repository URL for dependency - * @return additionalRepositoryCount Count of additional repository URLs for dependency - * @return dependencyWebsite Project website URL for dependency - * @return availableOnChain Whether dependency is available on chain - * @return scriptCount Count of on-chain scripts for dependency + * @return dependencyDetails Details for a given dependency type. */ function getDependencyDetails( bytes32 dependencyNameAndVersion - ) - external - view - returns ( - string memory nameAndVersion, - string memory licenseType, - string memory preferredCDN, - uint24 additionalCDNCount, - string memory preferredRepository, - uint24 additionalRepositoryCount, - string memory dependencyWebsite, - bool availableOnChain, - uint24 scriptCount - ); + ) external view returns (DependencyDetails memory dependencyDetails); /** * @notice Returns the dependency name and version for a given project (`projectId`) diff --git a/packages/contracts/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol b/packages/contracts/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol index 058fe5aa8..081eb9a3c 100644 --- a/packages/contracts/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol +++ b/packages/contracts/contracts/libs/v0.8.x/DependencyRegistryStorageLib.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.19; import "../../interfaces/v0.8.x/IAdminACLV0.sol"; import "../../interfaces/v0.8.x/ICoreRegistryV1.sol"; import "../../interfaces/v0.8.x/IUniversalBytecodeStorageReader.sol"; +import {IDependencyRegistryV0} from "../../interfaces/v0.8.x/IDependencyRegistryV0.sol"; + import "@openzeppelin-4.7/contracts/utils/structs/EnumerableSet.sol"; /** @@ -51,6 +53,12 @@ library DependencyRegistryStorageLib { uint24 additionalRepositoryCount; // count of scripts that make up the dependency, if the dependency is available on-chain uint24 scriptCount; + // canvas tag for the dependency + IDependencyRegistryV0.CanvasTag canvasTag; + // whether the dependency should be loaded as a module + bool loadAsModule; + // project script special type for the dependency + string projectScriptSpecialType; } /** From 9796d43c722e3b0e61ae6584819ec64c48dde861 Mon Sep 17 00:00:00 2001 From: ryley-o <30364988+ryley-o@users.noreply.github.com> Date: Tue, 13 May 2025 16:04:17 -0700 Subject: [PATCH 2/3] update existing test for updates to pass --- .../test/dependency-registry/DependencyRegistryV0.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts b/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts index 3d84866dc..f11a952b0 100644 --- a/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts +++ b/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts @@ -374,6 +374,9 @@ describe(`DependencyRegistryV0`, async function () { dependencyWebsite, // dependencyWebsite false, // availableOnChain 0, // scriptCount + 0, // canvas tag enum + false, // loadAsModule + "", // projectScriptSpecialType ]); }); }); From e3d72b1a83c9309e2591447f430dd397cfaa4726 Mon Sep 17 00:00:00 2001 From: ryley-o <30364988+ryley-o@users.noreply.github.com> Date: Tue, 13 May 2025 16:22:02 -0700 Subject: [PATCH 3/3] add tests for new logic coverage --- .../DependencyRegistryV0.test.ts | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts b/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts index f11a952b0..f8654595b 100644 --- a/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts +++ b/packages/contracts/test/dependency-registry/DependencyRegistryV0.test.ts @@ -32,6 +32,13 @@ const ONLY_NON_EMPTY_STRING_ERROR = "Must input non-empty string"; const ONLY_NON_ZERO_ADDRESS_ERROR = "Must input non-zero address"; const INDEX_OUT_OF_RANGE_ERROR = "Index out of range"; +const CANVAS_TAG = { + NoCanvasTag: 0, + CanvasBeforeProjectScript: 1, + CanvasAfterProjectScript: 2, + InvalidCanvasTag: 3, // this is not a valid enum value +}; + interface DependencyRegistryV0TestContext extends Mocha.Context { dependencyRegistry: DependencyRegistryV0; genArt721Core: GenArt721CoreV3; @@ -1956,6 +1963,215 @@ describe(`DependencyRegistryV0`, async function () { }); }); + describe("updateDependencyCanvasTag", function () { + it("does not allow non-admins to update canvas tag", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry + .connect(config.accounts.user) + .updateDependencyCanvasTag( + dependencyNameAndVersionBytes, + CANVAS_TAG.CanvasAfterProjectScript + ), + ONLY_ADMIN_ACL_ERROR + ); + }); + + it("does not allow updating canvas tag for a dependency that does not exist", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry.updateDependencyCanvasTag( + ethers.utils.formatBytes32String("nonExistentDependencyType"), + CANVAS_TAG.CanvasAfterProjectScript + ), + ONLY_EXISTING_DEPENDENCY_TYPE_ERROR + ); + }); + + it("does not allow updating canvas tag to an invalid enum value", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert.unspecified( + config.dependencyRegistry.updateDependencyCanvasTag( + dependencyNameAndVersionBytes, + CANVAS_TAG.InvalidCanvasTag + ) + ); + }); + + it("allows admin to update canvas tag", async function () { + // get config from beforeEach + const config = this.config; + await config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyCanvasTag( + dependencyNameAndVersionBytes, + CANVAS_TAG.CanvasAfterProjectScript + ); + + const dependencyDetails = + await config.dependencyRegistry.getDependencyDetails( + dependencyNameAndVersionBytes + ); + expect(dependencyDetails.canvasTag).to.eq( + CANVAS_TAG.CanvasAfterProjectScript + ); + }); + + it("emits the correct event", async function () { + // get config from beforeEach + const config = this.config; + await expect( + config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyCanvasTag( + dependencyNameAndVersionBytes, + CANVAS_TAG.CanvasBeforeProjectScript + ) + ) + .to.emit(config.dependencyRegistry, "DependencyCanvasTagUpdated") + .withArgs( + dependencyNameAndVersionBytes, + CANVAS_TAG.CanvasBeforeProjectScript + ); + }); + }); + + describe("updateDependencyLoadAsModule", function () { + it("does not allow non-admins to update loadAsModule", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry + .connect(config.accounts.user) + .updateDependencyLoadAsModule(dependencyNameAndVersionBytes, true), + ONLY_ADMIN_ACL_ERROR + ); + }); + + it("does not allow updating loadAsModule for a dependency that does not exist", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry.updateDependencyLoadAsModule( + ethers.utils.formatBytes32String("nonExistentDependencyType"), + true + ), + ONLY_EXISTING_DEPENDENCY_TYPE_ERROR + ); + }); + + it("allows admin to update loadAsModule", async function () { + // get config from beforeEach + const config = this.config; + await config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyLoadAsModule(dependencyNameAndVersionBytes, true); + + const dependencyDetails = + await config.dependencyRegistry.getDependencyDetails( + dependencyNameAndVersionBytes + ); + expect(dependencyDetails.loadAsModule).to.eq(true); + }); + + it("emits the correct event", async function () { + // get config from beforeEach + const config = this.config; + await expect( + config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyLoadAsModule(dependencyNameAndVersionBytes, true) + ) + .to.emit(config.dependencyRegistry, "DependencyLoadAsModuleUpdated") + .withArgs(dependencyNameAndVersionBytes, true); + }); + }); + + describe("updateDependencyProjectScriptSpecialType", function () { + it("does not allow non-admins to update project script special type", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry + .connect(config.accounts.user) + .updateDependencyProjectScriptSpecialType( + dependencyNameAndVersionBytes, + "newSpecialType" + ), + ONLY_ADMIN_ACL_ERROR + ); + }); + + it("does not allow updating project script special type for a dependency that does not exist", async function () { + // get config from beforeEach + const config = this.config; + await expectRevert( + config.dependencyRegistry.updateDependencyProjectScriptSpecialType( + ethers.utils.formatBytes32String("nonExistentDependencyType"), + "newSpecialType" + ), + ONLY_EXISTING_DEPENDENCY_TYPE_ERROR + ); + }); + + it("allows admin to update project script special type", async function () { + // get config from beforeEach + const config = this.config; + await config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyProjectScriptSpecialType( + dependencyNameAndVersionBytes, + "newSpecialType" + ); + + const dependencyDetails = + await config.dependencyRegistry.getDependencyDetails( + dependencyNameAndVersionBytes + ); + expect(dependencyDetails.projectScriptSpecialType).to.eq( + "newSpecialType" + ); + }); + + it("emits the correct event", async function () { + // get config from beforeEach + const config = this.config; + await expect( + config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyProjectScriptSpecialType( + dependencyNameAndVersionBytes, + "newSpecialType" + ) + ) + .to.emit( + config.dependencyRegistry, + "DependencyProjectScriptSpecialTypeUpdated" + ) + .withArgs(dependencyNameAndVersionBytes, "newSpecialType"); + }); + + it("allows updating project script special type to empty string", async function () { + // get config from beforeEach + const config = this.config; + await config.dependencyRegistry + .connect(config.accounts.deployer) + .updateDependencyProjectScriptSpecialType( + dependencyNameAndVersionBytes, + "" + ); + + const dependencyDetails = + await config.dependencyRegistry.getDependencyDetails( + dependencyNameAndVersionBytes + ); + expect(dependencyDetails.projectScriptSpecialType).to.eq(""); + }); + }); + describe("views", function () { it("getdependencyDetails", async function () { // get config from beforeEach