diff --git a/.changeset/add-cli-package.md b/.changeset/add-cli-package.md new file mode 100644 index 000000000..256d77b3c --- /dev/null +++ b/.changeset/add-cli-package.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/contracts-cli': minor +--- + +Initial release. diff --git a/.changeset/mcp-use-common-schemas.md b/.changeset/mcp-use-common-schemas.md new file mode 100644 index 000000000..1c835443d --- /dev/null +++ b/.changeset/mcp-use-common-schemas.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/contracts-mcp': patch +--- + +Use shared Zod schemas from `@openzeppelin/wizard-common/schemas` instead of local schema definitions. diff --git a/.changeset/move-schemas-to-common.md b/.changeset/move-schemas-to-common.md new file mode 100644 index 000000000..2e0a83290 --- /dev/null +++ b/.changeset/move-schemas-to-common.md @@ -0,0 +1,10 @@ +--- +'@openzeppelin/wizard-common': minor +--- + +Move Zod schemas from MCP to common package, add `@openzeppelin/wizard-common/schemas` subpath export. +- Uniswap hooks: shorten prompt, move hook descriptions to field-level `describe()` on `--hook` parameter. +- Cairo `access` schema field changed from required to optional (loosens validation). +- Added `zod` as a dependency. +- Add format examples and defaults to duration and date descriptions. +- **Breaking change**: Added `exports` field to package.json, restricting imports to declared subpaths (`.` and `./schemas`). diff --git a/.claude/skills/changeset/SKILL.md b/.claude/skills/changeset/SKILL.md new file mode 100644 index 000000000..8180602a1 --- /dev/null +++ b/.claude/skills/changeset/SKILL.md @@ -0,0 +1,38 @@ +--- +name: changeset +description: Add a changeset file for a package version bump +user_invocable: true +--- + +# Add Changeset + +Create a changeset file in `.changeset/` for the specified package(s). + +## Format + +```markdown +--- +'package-name': patch|minor|major +--- + +First line is a high-level summary without leading dash (PR number gets appended automatically). +- Additional detail line. +- Another detail line. +- **Breaking change**: Description of what breaks. +``` + +## Rules + +1. **First line**: Write as a plain sentence, no leading dash. The release tooling appends the PR link to this line. +2. **Subsequent lines**: Start each line with a dash. These lines get 2 spaces prepended automatically during the release process, so they appear indented under the first line in CHANGELOG.md. +3. **Breaking changes**: Mark with **Breaking change** or **Breaking changes** (see format example above). +4. **File naming**: Use a short kebab-case description, e.g., `add-cli-package.md`, `move-schemas-to-common.md`. +5. **Multiple packages**: Multiple packages can share a changeset file with different bump levels in the frontmatter. Use separate files only when packages need unrelated descriptions. +6. **Bump levels**: Follow semver based on current package version. `x.y.z` (>=1.0.0): major for breaking, minor for features, patch for fixes. `0.x.y`: minor for breaking, patch for features/fixes. `0.0.x`: patch for everything. +7. **New unpublished packages**: Still need a changeset to bump the initial version in package.json and for the changes to appear in the resulting changelog. + +## Steps + +1. Determine which packages changed and what bump level each needs. +2. Review existing CHANGELOG.md for the package(s) to match tone and style. +3. Write the changeset file(s) to `.changeset/.md`. \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 847ee3f0c..bc2e04358 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: pull_request: {} jobs: - format-lint: + static-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -16,6 +16,8 @@ jobs: run: yarn format:check - name: Run linter run: yarn lint + - name: Check circular dependencies + run: yarn check:circular-deps deno-check: runs-on: ubuntu-latest @@ -30,6 +32,16 @@ jobs: - name: Deno check API run: yarn type:check:api + cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Set up environment + uses: ./.github/actions/setup + - name: Run tests + run: yarn test + working-directory: packages/cli + mcp: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 8c478a614..5f02f3745 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ node_modules .vscode/settings.json fhevmTemp + +.claude/* +!.claude/skills/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d84bccc1..4c9829099 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Contributions to OpenZeppelin Contracts Wizard are welcome. Please review the in - `packages/common` contains common code used internally by some of the below packages. - `packages/core` contains the code generation logic for each language under separately named subfolders. +- `packages/cli` contains the CLI. - `packages/mcp` contains the MCP server. - `packages/ui` is the interface built in Svelte. diff --git a/README.md b/README.md index 268ab9db6..8f3555a5e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Stellar NPM Package](https://img.shields.io/npm/v/@openzeppelin/wizard-stellar?color=%23e55233&label=%40openzeppelin%2Fwizard-stellar)](https://www.npmjs.com/package/@openzeppelin/wizard-stellar) [![Stylus NPM Package](https://img.shields.io/npm/v/@openzeppelin/wizard-stylus?color=%23e55233&label=%40openzeppelin%2Fwizard-stylus)](https://www.npmjs.com/package/@openzeppelin/wizard-stylus) [![Uniswap Hooks NPM Package](https://img.shields.io/npm/v/@openzeppelin/wizard-uniswap-hooks?color=%23e55233&label=%40openzeppelin%2Fwizard-uniswap-hooks)](https://www.npmjs.com/package/@openzeppelin/wizard-uniswap-hooks) +[![Contracts CLI NPM Package](https://img.shields.io/npm/v/@openzeppelin/contracts-cli?label=%40openzeppelin%2Fcontracts-cli)](https://www.npmjs.com/package/@openzeppelin/contracts-cli) [![Contracts MCP NPM Package](https://img.shields.io/npm/v/@openzeppelin/contracts-mcp?label=%40openzeppelin%2Fcontracts-mcp)](https://www.npmjs.com/package/@openzeppelin/contracts-mcp) [![Netlify Status](https://api.netlify.com/api/v1/badges/ca9b53e1-44eb-410d-aac7-31b2f5399b68/deploy-status)](https://app.netlify.com/sites/openzeppelin-contracts-wizard/deploys) @@ -16,16 +17,17 @@ Contracts Wizard is a web application to interactively build a contract out of c Use the Contracts Wizard at https://wizard.openzeppelin.com -## MCP Servers +## CLI + +Generate contracts from the command line. See the [CLI package](packages/cli/README.md). -MCP Servers allow AI agents to generate smart contracts with the same options as the Contracts Wizard. +## MCP Servers -For local installation, see the [Contracts MCP Server](packages/mcp/README.md) package. -For a hosted version, see [OpenZeppelin MCP Servers](https://mcp.openzeppelin.com). +Allow AI agents to generate contracts. See the [MCP package](packages/mcp/README.md) for local installation, or [OpenZeppelin MCP Servers](https://mcp.openzeppelin.com) for a hosted version. ## TypeScript API -You can use the programmatic TypeScript API to generate contracts from your own applications. +Generate contracts programmatically from your own applications. View the API documentation for each smart contract language: - [Solidity](packages/core/solidity/README.md) diff --git a/package.json b/package.json index 55e517cd8..7ce24e546 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "prepare": "yarn workspaces run prepare", + "check:circular-deps": "node scripts/check-circular-deps.mjs", "lint": "eslint", "format:write": "prettier --write \"**/*\"", "format:check": "prettier --check \"**/*\"", @@ -22,6 +23,7 @@ "packages/core/solidity", "packages/core/*", "packages/mcp", + "packages/cli", "packages/ui" ], "nohoist": [ diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/cli/NOTICE b/packages/cli/NOTICE new file mode 100644 index 000000000..784d7052f --- /dev/null +++ b/packages/cli/NOTICE @@ -0,0 +1,8 @@ +OpenZeppelin Contracts Wizard +Copyright (C) 2021-2026 Zeppelin Group Ltd + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000..e023ab99a --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,28 @@ +# OpenZeppelin Contracts CLI + +[![NPM Package](https://img.shields.io/npm/v/@openzeppelin/contracts-cli)](https://www.npmjs.com/package/@openzeppelin/contracts-cli) + +CLI to generate secure, ready-to-compile smart contracts using OpenZeppelin Contracts libraries for various languages and contract kinds. + +## Usage + +```sh +npx @openzeppelin/contracts-cli [options] +``` + +Run `--help` to see available languages, contracts and options: + +```sh +npx @openzeppelin/contracts-cli --help +npx @openzeppelin/contracts-cli solidity-erc20 --help +``` + +### Examples + +```sh +npx @openzeppelin/contracts-cli solidity-erc20 --name MyToken --symbol MTK --mintable --burnable --access ownable +``` + +```sh +npx @openzeppelin/contracts-cli cairo-erc20 --name MyToken --symbol MTK --mintable --pausable --upgradeable +``` diff --git a/packages/cli/ava.config.js b/packages/cli/ava.config.js new file mode 100644 index 000000000..5858bdf5b --- /dev/null +++ b/packages/cli/ava.config.js @@ -0,0 +1,7 @@ +module.exports = { + extensions: ['ts'], + require: ['ts-node/register'], + timeout: '10m', + workerThreads: false, + files: ['src/**/*.test.ts'], +}; diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..5bb42c498 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,44 @@ +{ + "name": "@openzeppelin/contracts-cli", + "version": "0.0.1", + "description": "CLI for generating smart contracts using OpenZeppelin Contracts Wizard", + "license": "AGPL-3.0-only", + "repository": "https://github.com/OpenZeppelin/contracts-wizard", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "contracts-cli": "dist/index.js" + }, + "ts:main": "src/index.ts", + "files": [ + "LICENSE", + "NOTICE", + "/dist", + "/src", + "!**/*.test.*" + ], + "scripts": { + "prepare": "tsc", + "watch": "tsc --watch", + "pretest": "tsc", + "test": "ava", + "test:update-snapshots": "ava --update-snapshots", + "test:watch": "ava --watch" + }, + "dependencies": { + "zod": "^4.0", + "@openzeppelin/wizard-common": "^0.4.4", + "@openzeppelin/wizard": "^0.10.7", + "@openzeppelin/wizard-cairo": "^3.0.0", + "@openzeppelin/wizard-stellar": "^0.5.0", + "@openzeppelin/wizard-stylus": "^0.3.0", + "@openzeppelin/wizard-confidential": "^0.1.0", + "@openzeppelin/wizard-uniswap-hooks": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "ava": "^6.0.0", + "ts-node": "^10.4.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/cli/src/cli-adapter.ts b/packages/cli/src/cli-adapter.ts new file mode 100644 index 000000000..e4128b134 --- /dev/null +++ b/packages/cli/src/cli-adapter.ts @@ -0,0 +1,253 @@ +import { z } from 'zod'; + +type PrimitiveType = 'string' | 'boolean' | 'number'; + +interface BaseTypeInfo { + type: PrimitiveType | 'object'; + enumValues?: string[]; + hasLiteralFalse?: boolean; +} + +interface FlagSpec { + name: string; + path: string[]; + type: PrimitiveType; + enumValues?: string[]; + required: boolean; + description?: string; + hasLiteralFalse: boolean; +} + +function unwrapSchema(schema: z.ZodType): z.ZodType { + let current: z.ZodType = schema; + + while (true) { + if (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault) { + current = current._def.innerType as z.ZodType; + continue; + } + return current; + } +} + +function getBaseType(schema: z.ZodType): BaseTypeInfo { + const unwrapped = unwrapSchema(schema); + + if (unwrapped instanceof z.ZodString) return { type: 'string' }; + if (unwrapped instanceof z.ZodBoolean) return { type: 'boolean' }; + if (unwrapped instanceof z.ZodNumber) return { type: 'number' }; + if (unwrapped instanceof z.ZodLiteral) { + const val = unwrapped._def.values?.[0]; + if (val === false) return { type: 'boolean', hasLiteralFalse: true }; + if (typeof val === 'boolean') return { type: 'boolean' }; + if (typeof val === 'string') return { type: 'string', enumValues: [val] }; + return { type: 'string' }; + } + if (unwrapped instanceof z.ZodEnum) return { type: 'string', enumValues: unwrapped.options as string[] }; + if (unwrapped instanceof z.ZodUnion) { + const literals: string[] = []; + let hasFalse = false; + for (const option of unwrapped._def.options) { + const base = getBaseType(option as z.ZodType); + if (base.enumValues) literals.push(...base.enumValues); + if (base.hasLiteralFalse) hasFalse = true; + } + if (literals.length > 0) return { type: 'string', enumValues: hasFalse ? ['false', ...literals] : literals }; + return { type: 'string' }; + } + if (unwrapped instanceof z.ZodObject) return { type: 'object' }; + return { type: 'string' }; +} + +function getDescription(schema: z.ZodType): string | undefined { + if (schema.description) return schema.description; + if (schema instanceof z.ZodOptional) { + return (schema.unwrap() as z.ZodType).description; + } + return undefined; +} + +function isRequired(schema: z.ZodType): boolean { + return !(schema instanceof z.ZodOptional) && !(schema instanceof z.ZodDefault); +} + +function collectFlagSpecs(shape: z.ZodRawShape, pathPrefix: string[] = [], parentRequired = true): FlagSpec[] { + const specs: FlagSpec[] = []; + + for (const key of Object.keys(shape)) { + const schema = shape[key] as z.ZodType; + const base = getBaseType(schema); + const path = [...pathPrefix, key]; + const required = parentRequired && isRequired(schema); + + if (base.type === 'object') { + const unwrapped = unwrapSchema(schema); + if (unwrapped instanceof z.ZodObject) { + specs.push(...collectFlagSpecs(unwrapped.shape, path, required)); + } + continue; + } + + specs.push({ + name: path.join('.'), + path, + type: base.type, + enumValues: base.enumValues, + required, + description: getDescription(schema), + hasLiteralFalse: base.enumValues?.includes('false') ?? false, + }); + } + + return specs; +} + +function formatFlag(spec: FlagSpec): string { + if (spec.type === 'boolean') { + return ` --${spec.name}`; + } + const typeStr = spec.enumValues ? spec.enumValues.join('|') : spec.type; + return ` --${spec.name} <${typeStr}>`; +} + +function parseFlagValue(spec: FlagSpec, value: string): string | number | boolean { + if (spec.type === 'number') return Number(value); + if (spec.hasLiteralFalse && value === 'false') return false; + return value; +} + +function setPathValue(target: Record, path: string[], value: string | number | boolean): void { + let current = target; + + for (const segment of path.slice(0, -1)) { + const existing = current[segment]; + if (existing === undefined) { + current[segment] = {}; + } else if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) { + throw new Error(`Invalid option nesting: --${path.join('.')}`); + } + current = current[segment] as Record; + } + + current[path.at(-1)!] = value; +} + +export function generateHelp(commandName: string, shape: z.ZodRawShape, description?: string): string { + const lines: string[] = []; + lines.push(description ? `${commandName}: ${description}` : commandName); + lines.push(''); + + const required: string[] = []; + const optional: string[] = []; + + for (const spec of collectFlagSpecs(shape)) { + const flag = formatFlag(spec); + const desc = spec.description ?? ''; + const padding = flag.length >= 40 ? ' ' : ' '.repeat(40 - flag.length); + const indentedDesc = desc.replace(/\n/g, '\n '); + const line = desc ? `${flag}${padding}${indentedDesc}` : flag; + + if (spec.required) { + required.push(line); + } else { + optional.push(line); + } + } + + if (required.length > 0) { + lines.push('Required:'); + lines.push(...required); + lines.push(''); + } + if (optional.length > 0) { + lines.push('Options:'); + lines.push(...optional); + } + + return lines.join('\n'); +} + +export function parseArgsFromSchema(shape: T, argv: string[]): z.infer> { + const flagSpecs = collectFlagSpecs(shape); + const flagSpecsByName = new Map(flagSpecs.map(spec => [spec.name, spec])); + + const result: Record = {}; + const seen = new Set(); + + let i = 0; + while (i < argv.length) { + const arg = argv[i]!; + if (!arg.startsWith('--')) { + throw new Error(`Unexpected argument: ${arg}`); + } + + let flagName: string; + let inlineValue: string | undefined; + + const eqIndex = arg.indexOf('='); + if (eqIndex !== -1) { + flagName = arg.slice(2, eqIndex); + inlineValue = arg.slice(eqIndex + 1); + } else { + flagName = arg.slice(2); + } + + const spec = flagSpecsByName.get(flagName); + if (!spec) { + throw new Error(`Unknown option: --${flagName}`); + } + + if (seen.has(flagName)) { + throw new Error(`Duplicate option: --${flagName}`); + } + seen.add(flagName); + + if (spec.type === 'boolean') { + const nextArg = inlineValue ?? argv[i + 1]; + if (nextArg === 'true' || nextArg === 'false') { + setPathValue(result, spec.path, nextArg === 'true'); + i += inlineValue ? 1 : 2; + } else { + setPathValue(result, spec.path, true); + i++; + } + } else { + const value = inlineValue ?? argv[i + 1]; + if (value === undefined || value.startsWith('--')) { + throw new Error(`Missing value for option: --${flagName}`); + } + setPathValue(result, spec.path, parseFlagValue(spec, value)); + i += inlineValue ? 1 : 2; + } + } + + const parsed = z.object(shape).safeParse(result); + if (!parsed.success) { + const missing: string[] = []; + const invalid: string[] = []; + + for (const issue of parsed.error.issues) { + const name = issue.path.join('.'); + const spec = flagSpecsByName.get(name); + const isMissing = issue.code === 'invalid_type' && issue.message.endsWith('received undefined'); + + const line = spec ? ` ${formatFlag(spec)} ${spec.description ?? ''}` : ` --${name}: ${issue.message}`; + + if (isMissing) { + missing.push(line); + } else { + invalid.push(line); + } + } + + const sections: string[] = []; + if (invalid.length > 0) { + sections.push(`Invalid values for options:\n${invalid.join('\n')}`); + } + if (missing.length > 0) { + sections.push(`Missing required options:\n${missing.join('\n')}`); + } + throw new Error(sections.join('\n\n')); + } + return parsed.data; +} diff --git a/packages/cli/src/cli-options.test.ts b/packages/cli/src/cli-options.test.ts new file mode 100644 index 000000000..f869c933f --- /dev/null +++ b/packages/cli/src/cli-options.test.ts @@ -0,0 +1,820 @@ +import test from 'ava'; +import { execFileSync } from 'node:child_process'; +import { join } from 'node:path'; + +import { erc20, erc721, erc1155, stablecoin, realWorldAsset, governor, account, custom } from '@openzeppelin/wizard'; +import { + erc20 as cairoErc20, + erc721 as cairoErc721, + erc1155 as cairoErc1155, + account as cairoAccount, + multisig as cairoMultisig, + governor as cairoGovernor, + vesting as cairoVesting, + custom as cairoCustom, +} from '@openzeppelin/wizard-cairo'; +import { fungible, nonFungible, stablecoin as stellarStablecoin } from '@openzeppelin/wizard-stellar'; +import { erc20 as stylusErc20, erc721 as stylusErc721, erc1155 as stylusErc1155 } from '@openzeppelin/wizard-stylus'; +import { erc7984 } from '@openzeppelin/wizard-confidential'; +import { hooks } from '@openzeppelin/wizard-uniswap-hooks'; + +const CLI = join(__dirname, '..', 'dist', 'index.js'); + +function run(...args: string[]): string { + return execFileSync('node', [CLI, ...args], { encoding: 'utf-8' }); +} + +// --- Solidity --- + +test('solidity-erc20: basic', t => { + const output = run('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST'); + t.is(output, erc20.print({ name: 'TestToken', symbol: 'TST' })); +}); + +test('solidity-erc20: most options', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + premint: '1000', + access: 'roles' as const, + burnable: true, + mintable: true, + pausable: true, + permit: true, + votes: 'blocknumber' as const, + flashmint: true, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-erc20', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--access', + opts.access, + '--burnable', + '--mintable', + '--pausable', + '--permit', + '--flashmint', + '--votes', + opts.votes, + '--upgradeable', + opts.upgradeable, + ); + t.is(output, erc20.print(opts)); +}); + +test('solidity-erc721: most options', t => { + const opts = { + name: 'TestNFT', + symbol: 'TNFT', + baseUri: 'https://example.com/', + enumerable: true, + uriStorage: true, + burnable: true, + pausable: true, + mintable: true, + incremental: true, + votes: 'blocknumber' as const, + access: 'roles' as const, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-erc721', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--baseUri', + opts.baseUri, + '--enumerable', + '--uriStorage', + '--burnable', + '--pausable', + '--mintable', + '--incremental', + '--votes', + opts.votes, + '--access', + opts.access, + '--upgradeable', + opts.upgradeable, + ); + t.is(output, erc721.print(opts)); +}); + +test('solidity-erc1155: most options', t => { + const opts = { + name: 'TestMulti', + uri: 'https://example.com/{id}', + burnable: true, + pausable: true, + mintable: true, + supply: true, + updatableUri: true, + access: 'roles' as const, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-erc1155', + '--name', + opts.name, + '--uri', + opts.uri, + '--burnable', + '--pausable', + '--mintable', + '--supply', + '--updatableUri', + '--access', + opts.access, + '--upgradeable', + opts.upgradeable, + ); + t.is(output, erc1155.print(opts)); +}); + +test('solidity-stablecoin: most options', t => { + const opts = { + name: 'TestStable', + symbol: 'TSTB', + premint: '1000000', + access: 'roles' as const, + burnable: true, + mintable: true, + pausable: true, + permit: true, + flashmint: true, + restrictions: 'allowlist' as const, + freezable: true, + }; + const output = run( + 'solidity-stablecoin', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--access', + opts.access, + '--burnable', + '--mintable', + '--pausable', + '--permit', + '--flashmint', + '--restrictions', + opts.restrictions, + '--freezable', + ); + t.is(output, stablecoin.print(opts)); +}); + +test('solidity-governor: most options', t => { + const opts = { + name: 'TestGovernor', + delay: '1 day', + period: '1 week', + votes: 'erc20votes' as const, + clockMode: 'timestamp' as const, + timelock: 'openzeppelin' as const, + proposalThreshold: '1000', + quorumMode: 'percent' as const, + quorumPercent: 10, + storage: true, + settings: true, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-governor', + '--name', + opts.name, + '--delay', + opts.delay, + '--period', + opts.period, + '--votes', + opts.votes, + '--clockMode', + opts.clockMode, + '--timelock', + opts.timelock, + '--proposalThreshold', + opts.proposalThreshold, + '--quorumMode', + opts.quorumMode, + '--quorumPercent', + String(opts.quorumPercent), + '--storage', + '--settings', + '--upgradeable', + opts.upgradeable, + ); + t.is(output, governor.print(opts)); +}); + +test('solidity-account: most options', t => { + const opts = { + name: 'TestAccount', + signatureValidation: 'ERC7739' as const, + ERC721Holder: true, + ERC1155Holder: true, + signer: 'P256' as const, + batchedExecution: true, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-account', + '--name', + opts.name, + '--signatureValidation', + opts.signatureValidation, + '--ERC721Holder', + '--ERC1155Holder', + '--signer', + opts.signer, + '--batchedExecution', + '--upgradeable', + opts.upgradeable, + ); + t.is(output, account.print(opts)); +}); + +test('solidity-rwa: most options', t => { + const opts = { + name: 'TestRWA', + symbol: 'TRWA', + premint: '1000000', + burnable: true, + mintable: true, + pausable: true, + permit: true, + access: 'roles' as const, + restrictions: 'allowlist' as const, + freezable: true, + }; + const output = run( + 'solidity-rwa', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--burnable', + '--mintable', + '--pausable', + '--permit', + '--access', + opts.access, + '--restrictions', + opts.restrictions, + '--freezable', + ); + t.is(output, realWorldAsset.print(opts)); +}); + +test('solidity-custom: most options', t => { + const opts = { + name: 'TestCustom', + pausable: true, + access: 'roles' as const, + upgradeable: 'uups' as const, + }; + const output = run( + 'solidity-custom', + '--name', + opts.name, + '--pausable', + '--access', + opts.access, + '--upgradeable', + opts.upgradeable, + ); + t.is(output, custom.print(opts)); +}); + +// --- Cairo --- + +test('cairo-erc20: basic', t => { + const output = run('cairo-erc20', '--name', 'TestToken', '--symbol', 'TST'); + t.is(output, cairoErc20.print({ name: 'TestToken', symbol: 'TST' })); +}); + +test('cairo-erc20: most options', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + premint: '1000', + burnable: true, + mintable: true, + pausable: true, + votes: true, + appName: 'TestApp', + appVersion: 'v1', + upgradeable: true, + }; + const output = run( + 'cairo-erc20', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--burnable', + '--mintable', + '--pausable', + '--votes', + '--appName', + opts.appName, + '--appVersion', + opts.appVersion, + '--upgradeable', + ); + t.is(output, cairoErc20.print(opts)); +}); + +test('cairo-erc20: access roles-dar', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + access: { + type: 'roles-dar' as const, + darInitialDelay: '1 day', + darDefaultDelayIncrease: '1 day', + darMaxTransferDelay: '2 day', + }, + }; + const output = run( + 'cairo-erc20', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--access.type', + 'roles-dar', + '--access.darInitialDelay', + '1 day', + '--access.darDefaultDelayIncrease', + '1 day', + '--access.darMaxTransferDelay', + '2 day', + ); + t.is(output, cairoErc20.print(opts)); +}); + +test('cairo-erc721: most options', t => { + const opts = { + name: 'TestNFT', + symbol: 'TNFT', + baseUri: 'https://example.com/', + burnable: true, + mintable: true, + pausable: true, + enumerable: true, + votes: true, + appName: 'TestApp', + appVersion: 'v1', + upgradeable: true, + }; + const output = run( + 'cairo-erc721', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--baseUri', + opts.baseUri, + '--burnable', + '--mintable', + '--pausable', + '--enumerable', + '--votes', + '--appName', + opts.appName, + '--appVersion', + opts.appVersion, + '--upgradeable', + ); + t.is(output, cairoErc721.print(opts)); +}); + +test('cairo-erc1155: most options', t => { + const opts = { + name: 'TestMulti', + baseUri: 'https://example.com/{id}', + burnable: true, + mintable: true, + pausable: true, + updatableUri: true, + upgradeable: true, + }; + const output = run( + 'cairo-erc1155', + '--name', + opts.name, + '--baseUri', + opts.baseUri, + '--burnable', + '--mintable', + '--pausable', + '--updatableUri', + '--upgradeable', + ); + t.is(output, cairoErc1155.print(opts)); +}); + +test('cairo-account: most options', t => { + const opts = { + name: 'TestAccount', + type: 'stark' as const, + declare: true, + deploy: true, + pubkey: true, + outsideExecution: true, + upgradeable: true, + }; + const output = run( + 'cairo-account', + '--name', + opts.name, + '--type', + opts.type, + '--declare', + '--deploy', + '--pubkey', + '--outsideExecution', + '--upgradeable', + ); + t.is(output, cairoAccount.print(opts)); +}); + +test('cairo-governor: most options', t => { + const opts = { + name: 'TestGovernor', + delay: '1 day', + period: '1 week', + votes: 'erc20votes' as const, + clockMode: 'timestamp' as const, + timelock: 'openzeppelin' as const, + proposalThreshold: '100', + quorumMode: 'percent' as const, + quorumPercent: 10, + settings: true, + upgradeable: true, + appName: 'TestApp', + appVersion: 'v1', + }; + const output = run( + 'cairo-governor', + '--name', + opts.name, + '--delay', + opts.delay, + '--period', + opts.period, + '--votes', + opts.votes, + '--clockMode', + opts.clockMode, + '--timelock', + opts.timelock, + '--proposalThreshold', + opts.proposalThreshold, + '--quorumMode', + opts.quorumMode, + '--quorumPercent', + String(opts.quorumPercent), + '--settings', + '--upgradeable', + '--appName', + opts.appName, + '--appVersion', + opts.appVersion, + ); + t.is(output, cairoGovernor.print(opts)); +}); + +test('cairo-multisig: most options', t => { + const opts = { + name: 'TestMultisig', + quorum: '3', + upgradeable: true, + }; + const output = run('cairo-multisig', '--name', opts.name, '--quorum', opts.quorum, '--upgradeable'); + t.is(output, cairoMultisig.print(opts)); +}); + +test('cairo-vesting: most options', t => { + const opts = { + name: 'TestVesting', + startDate: '2025-01-01', + duration: '30 day', + cliffDuration: '7 day', + schedule: 'linear' as const, + }; + const output = run( + 'cairo-vesting', + '--name', + opts.name, + '--startDate', + opts.startDate, + '--duration', + opts.duration, + '--cliffDuration', + opts.cliffDuration, + '--schedule', + opts.schedule, + ); + t.is(output, cairoVesting.print(opts)); +}); + +test('cairo-custom: most options', t => { + const opts = { + name: 'TestCustom', + pausable: true, + upgradeable: true, + }; + const output = run('cairo-custom', '--name', opts.name, '--pausable', '--upgradeable'); + t.is(output, cairoCustom.print(opts)); +}); + +// --- Stellar --- + +test('stellar-fungible: basic', t => { + const output = run('stellar-fungible', '--name', 'TestToken', '--symbol', 'TST'); + t.is(output, fungible.print({ name: 'TestToken', symbol: 'TST' })); +}); + +test('stellar-fungible: most options', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + premint: '1000', + burnable: true, + mintable: true, + pausable: true, + access: 'roles' as const, + upgradeable: true, + }; + const output = run( + 'stellar-fungible', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--burnable', + '--mintable', + '--pausable', + '--access', + opts.access, + '--upgradeable', + ); + t.is(output, fungible.print(opts)); +}); + +test('stellar-stablecoin: most options', t => { + const opts = { + name: 'TestStable', + symbol: 'TSTB', + premint: '1000000', + burnable: true, + mintable: true, + pausable: true, + access: 'roles' as const, + upgradeable: true, + limitations: 'allowlist' as const, + }; + const output = run( + 'stellar-stablecoin', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--premint', + opts.premint, + '--burnable', + '--mintable', + '--pausable', + '--access', + opts.access, + '--upgradeable', + '--limitations', + opts.limitations, + ); + t.is(output, stellarStablecoin.print(opts)); +}); + +test('stellar-non-fungible: most options', t => { + const opts = { + name: 'TestNFT', + symbol: 'TNFT', + tokenUri: 'https://example.com/', + burnable: true, + enumerable: true, + pausable: true, + mintable: true, + sequential: true, + access: 'roles' as const, + upgradeable: true, + }; + const output = run( + 'stellar-non-fungible', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--tokenUri', + opts.tokenUri, + '--burnable', + '--enumerable', + '--pausable', + '--mintable', + '--sequential', + '--access', + opts.access, + '--upgradeable', + ); + t.is(output, nonFungible.print(opts)); +}); + +// --- Stylus --- + +test('stylus-erc20: basic', t => { + const output = run('stylus-erc20', '--name', 'TestToken'); + t.is(output, stylusErc20.print({ name: 'TestToken' })); +}); + +test('stylus-erc20: most options', t => { + const opts = { + name: 'TestToken', + burnable: true, + permit: true, + flashmint: true, + }; + const output = run('stylus-erc20', '--name', opts.name, '--burnable', '--permit', '--flashmint'); + t.is(output, stylusErc20.print(opts)); +}); + +test('stylus-erc721: most options', t => { + const opts = { + name: 'TestNFT', + burnable: true, + enumerable: true, + }; + const output = run('stylus-erc721', '--name', opts.name, '--burnable', '--enumerable'); + t.is(output, stylusErc721.print(opts)); +}); + +test('stylus-erc1155: most options', t => { + const opts = { + name: 'TestMulti', + burnable: true, + supply: true, + }; + const output = run('stylus-erc1155', '--name', opts.name, '--burnable', '--supply'); + t.is(output, stylusErc1155.print(opts)); +}); + +// --- Confidential --- + +test('confidential-erc7984: basic', t => { + const output = run( + 'confidential-erc7984', + '--name', + 'TestToken', + '--symbol', + 'TST', + '--contractURI', + 'https://example.com', + '--networkConfig', + 'zama-ethereum', + ); + t.is( + output, + erc7984.print({ + name: 'TestToken', + symbol: 'TST', + contractURI: 'https://example.com', + networkConfig: 'zama-ethereum', + }), + ); +}); + +test('confidential-erc7984: most options', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + contractURI: 'https://example.com', + networkConfig: 'zama-ethereum' as const, + premint: '1000', + wrappable: true, + votes: 'blocknumber' as const, + }; + const output = run( + 'confidential-erc7984', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--contractURI', + opts.contractURI, + '--networkConfig', + opts.networkConfig, + '--premint', + opts.premint, + '--wrappable', + '--votes', + opts.votes, + ); + t.is(output, erc7984.print(opts)); +}); + +// --- Uniswap Hooks --- + +test('uniswap-hooks: most options', t => { + const opts = { + hook: 'BaseHook' as const, + name: 'TestHook', + pausable: true, + currencySettler: true, + safeCast: true, + transientStorage: true, + shares: { options: 'ERC20' as const, name: 'TestShare', symbol: 'TS' }, + permissions: { + beforeInitialize: true, + afterInitialize: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false, + }, + inputs: { blockNumberOffset: 0, maxAbsTickDelta: 0 }, + access: 'roles' as const, + }; + const output = run( + 'uniswap-hooks', + '--hook', + opts.hook, + '--name', + opts.name, + '--pausable', + '--currencySettler', + '--safeCast', + '--transientStorage', + '--shares.options', + 'ERC20', + '--shares.name', + opts.shares.name, + '--shares.symbol', + opts.shares.symbol, + '--permissions.beforeInitialize', + '--permissions.afterInitialize', + 'false', + '--permissions.beforeAddLiquidity', + 'false', + '--permissions.beforeRemoveLiquidity', + 'false', + '--permissions.afterAddLiquidity', + 'false', + '--permissions.afterRemoveLiquidity', + 'false', + '--permissions.beforeSwap', + '--permissions.afterSwap', + 'false', + '--permissions.beforeDonate', + 'false', + '--permissions.afterDonate', + 'false', + '--permissions.beforeSwapReturnDelta', + 'false', + '--permissions.afterSwapReturnDelta', + 'false', + '--permissions.afterAddLiquidityReturnDelta', + 'false', + '--permissions.afterRemoveLiquidityReturnDelta', + 'false', + '--inputs.blockNumberOffset', + '0', + '--inputs.maxAbsTickDelta', + '0', + '--access', + opts.access, + ); + t.is(output, hooks.print(opts)); +}); diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts new file mode 100644 index 000000000..9606cafc8 --- /dev/null +++ b/packages/cli/src/cli.test.ts @@ -0,0 +1,197 @@ +import test from 'ava'; +import { execFileSync } from 'node:child_process'; +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { erc20, stablecoin } from '@openzeppelin/wizard'; +import { registry } from './registry'; + +const CLI = join(__dirname, '..', 'dist', 'index.js'); +const PACKAGES_CORE_PATH = join(__dirname, '../../core'); +const CLI_EXCLUDED_LANGUAGES = ['cairo_alpha']; + +function run(...args: string[]): string { + return execFileSync('node', [CLI, ...args], { encoding: 'utf-8' }); +} + +function runError(...args: string[]): string { + try { + execFileSync('node', [CLI, ...args], { encoding: 'utf-8' }); + throw new Error('Expected command to fail'); + } catch (e: unknown) { + const error = e as { stderr?: string }; + return error.stderr ?? ''; + } +} + +function toKebabCase(value: string): string { + return value + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .toLowerCase(); +} + +function kindsFromSource(source: string): string[] { + return [...source.matchAll(/case '([^']+)'/g)] + .map(match => match[1]) + .filter((kind): kind is string => kind !== undefined); +} + +function coreKindToCommand(language: string, kind: string): string { + if (language === 'uniswap-hooks' && kind === 'Hooks') { + return language; + } + + if (language === 'solidity' && kind === 'RealWorldAsset') { + return 'solidity-rwa'; + } + + return `${language}-${toKebabCase(kind)}`; +} + +// --- Registry completeness --- + +test('each core kind has cli registry entry', async t => { + const coreEntries = await readdir(PACKAGES_CORE_PATH, { withFileTypes: true }); + const coreDirs = coreEntries + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .filter(name => !CLI_EXCLUDED_LANGUAGES.includes(name)); + + for (const coreDir of coreDirs) { + const kindSource = await readFile(join(PACKAGES_CORE_PATH, coreDir, 'src', 'kind.ts'), 'utf-8'); + + for (const kind of kindsFromSource(kindSource)) { + const expectedCommand = coreKindToCommand(coreDir, kind); + + t.true( + expectedCommand in registry, + `Expected '${expectedCommand}' not found in registry for core language '${coreDir}' and kind '${kind}'`, + ); + } + } +}); + +// --- Help snapshots --- + +test('no args', t => { + t.snapshot(run()); +}); + +test('--help', t => { + t.snapshot(run('--help')); +}); + +for (const command of Object.keys(registry)) { + test(`${command} --help`, t => { + t.snapshot(run(command, '--help')); + }); +} + +// --- Error snapshots --- + +test('unknown command', t => { + t.snapshot(runError('nonexistent-command')); +}); + +test('unknown option', t => { + t.snapshot(runError('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST', '--notreal')); +}); + +test('missing required option', t => { + t.snapshot(runError('solidity-erc20', '--name', 'TestToken')); +}); + +test('missing multiple required options', t => { + t.snapshot(runError('solidity-erc20')); +}); + +test('unexpected argument without --', t => { + t.snapshot(runError('solidity-erc20', 'foo')); +}); + +test('missing value for string option (followed by another flag)', t => { + t.snapshot(runError('solidity-erc20', '--name', '--symbol', 'TST')); +}); + +test('missing value for string option (at end of args)', t => { + t.snapshot(runError('solidity-erc20', '--symbol', 'TST', '--name')); +}); + +test('invalid enum value', t => { + t.snapshot(runError('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST', '--votes', 'invalidvalue')); +}); + +test('invalid number value', t => { + t.snapshot( + runError( + 'solidity-governor', + '--name', + 'TestGov', + '--delay', + '1 day', + '--period', + '1 week', + '--quorumPercent', + 'notanumber', + ), + ); +}); + +test('duplicate flag', t => { + t.snapshot(runError('solidity-erc20', '--name', 'First', '--symbol', 'TST', '--name', 'Second')); +}); + +// --- Parsing --- + +test('--flag=value syntax', t => { + const output = run('solidity-erc20', '--name=TestToken', '--symbol=TST', '--votes=blocknumber'); + t.is(output, erc20.print({ name: 'TestToken', symbol: 'TST', votes: 'blocknumber' })); +}); + +test('--bool=true and --bool=false syntax', t => { + const output = run('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST', '--mintable=true', '--pausable=false'); + t.is(output, erc20.print({ name: 'TestToken', symbol: 'TST', mintable: true, pausable: false })); +}); + +test('bare --bool at end of args', t => { + const output = run('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST', '--mintable'); + t.is(output, erc20.print({ name: 'TestToken', symbol: 'TST', mintable: true })); +}); + +test('enum string value', t => { + const output = run('solidity-erc20', '--name', 'TestToken', '--symbol', 'TST', '--access', 'ownable'); + t.is(output, erc20.print({ name: 'TestToken', symbol: 'TST', access: 'ownable' })); +}); + +test('false literal enum value', t => { + const output = run('solidity-stablecoin', '--name', 'TestStable', '--symbol', 'TSTB', '--restrictions', 'false'); + t.is(output, stablecoin.print({ name: 'TestStable', symbol: 'TSTB', restrictions: false })); +}); + +test('string values with spaces', t => { + const output = run('solidity-erc20', '--name', 'My Token', '--symbol', 'TST'); + t.is(output, erc20.print({ name: 'My Token', symbol: 'TST' })); +}); + +test('nested dot options with multiple fields', t => { + const output = run( + 'solidity-erc20', + '--name', + 'TestToken', + '--symbol', + 'TST', + '--info.license', + 'Apache-2.0', + '--info.securityContact', + 'test@test.com', + ); + t.is( + output, + erc20.print({ + name: 'TestToken', + symbol: 'TST', + info: { license: 'Apache-2.0', securityContact: 'test@test.com' }, + }), + ); +}); diff --git a/packages/cli/src/cli.test.ts.md b/packages/cli/src/cli.test.ts.md new file mode 100644 index 000000000..70b883d91 --- /dev/null +++ b/packages/cli/src/cli.test.ts.md @@ -0,0 +1,709 @@ +# Snapshot report for `src/cli.test.ts` + +The actual snapshot is saved in `cli.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## no args + +> Snapshot 1 + + `Usage: npx @openzeppelin/contracts-cli [options]␊ + ␊ + Commands:␊ + solidity-erc20, solidity-erc721, solidity-erc1155, solidity-stablecoin, solidity-rwa, solidity-account, solidity-governor, solidity-custom, cairo-erc20, cairo-erc721, cairo-erc1155, cairo-account, cairo-multisig, cairo-governor, cairo-vesting, cairo-custom, stellar-fungible, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks␊ + ␊ + Generated contract source code is printed to stdout.␊ + ␊ + Run \`npx @openzeppelin/contracts-cli --help\` for command-specific options.␊ + ` + +## --help + +> Snapshot 1 + + `Usage: npx @openzeppelin/contracts-cli [options]␊ + ␊ + Commands:␊ + solidity-erc20, solidity-erc721, solidity-erc1155, solidity-stablecoin, solidity-rwa, solidity-account, solidity-governor, solidity-custom, cairo-erc20, cairo-erc721, cairo-erc1155, cairo-account, cairo-multisig, cairo-governor, cairo-vesting, cairo-custom, stellar-fungible, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks␊ + ␊ + Generated contract source code is printed to stdout.␊ + ␊ + Run \`npx @openzeppelin/contracts-cli --help\` for command-specific options.␊ + ` + +## solidity-erc20 --help + +> Snapshot 1 + + `solidity-erc20: Make a fungible token per the ERC-20 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --premintChainId The chain ID of the network on which to premint tokens.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --callback Whether to include support for code execution after transfers and approvals on recipient contracts in a single transaction.␊ + --permit Whether without paying gas, token holders will be able to allow third parties to transfer from their account.␊ + --votes Whether to keep track of historical balances for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps.␊ + --flashmint Whether to include built-in flash loans to allow lending tokens without requiring collateral as long as they're returned in the same transaction.␊ + --crossChainBridging Whether to allow authorized bridge contracts to mint and burn tokens for cross-chain transfers. Options are to use custom bridges on any chain, to embed an ERC-7786 based bridge directly in the token contract, or to use the SuperchainERC20 standard with the predeployed SuperchainTokenBridge. The SuperchainERC20 feature is only available on chains in the Superchain, and requires deploying your contract to the same address on every chain in the Superchain.␊ + --crossChainLinkAllowOverride Whether to allow replacing a crosschain link that has already been registered. Only used if crossChainBridging is set to "erc7786native".␊ + --namespacePrefix The prefix for ERC-7201 namespace identifiers. It should be derived from the project name or a unique naming convention specific to the project. Used only if the contract includes storage variables and upgradeability is enabled. Default is "myProject".␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## solidity-erc721 --help + +> Snapshot 1 + + `solidity-erc721: Make a non-fungible token per the ERC-721 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --baseUri A base uri for the token␊ + --enumerable Whether to allow on-chain enumeration of all tokens or those owned by an account. Increases gas cost of transfers.␊ + --uriStorage Allows updating token URIs for individual token IDs␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --incremental Whether new tokens will be automatically assigned an incremental id␊ + --votes Whether to keep track of individual units for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps (defaulting to block number if not specified).␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --namespacePrefix The prefix for ERC-7201 namespace identifiers. It should be derived from the project name or a unique naming convention specific to the project. Used only if the contract includes storage variables and upgradeability is enabled. Default is "myProject".␊ + ` + +## solidity-erc1155 --help + +> Snapshot 1 + + `solidity-erc1155: Make a non-fungible token per the ERC-1155 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --uri The location of the metadata for the token. Clients will replace any instance of {id} in this string with the tokenId.␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --supply Whether to keep track of total supply of tokens␊ + --updatableUri Whether privileged accounts will be able to set a new URI for all token types␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## solidity-stablecoin --help + +> Snapshot 1 + + `solidity-stablecoin: Make a stablecoin token that uses the ERC-20 standard. Experimental, some features are not audited and are subject to change.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --premintChainId The chain ID of the network on which to premint tokens.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --callback Whether to include support for code execution after transfers and approvals on recipient contracts in a single transaction.␊ + --permit Whether without paying gas, token holders will be able to allow third parties to transfer from their account.␊ + --votes Whether to keep track of historical balances for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps.␊ + --flashmint Whether to include built-in flash loans to allow lending tokens without requiring collateral as long as they're returned in the same transaction.␊ + --crossChainBridging Whether to allow authorized bridge contracts to mint and burn tokens for cross-chain transfers. Options are to use custom bridges on any chain, to embed an ERC-7786 based bridge directly in the token contract, or to use the SuperchainERC20 standard with the predeployed SuperchainTokenBridge. The SuperchainERC20 feature is only available on chains in the Superchain, and requires deploying your contract to the same address on every chain in the Superchain.␊ + --crossChainLinkAllowOverride Whether to allow replacing a crosschain link that has already been registered. Only used if crossChainBridging is set to "erc7786native".␊ + --namespacePrefix The prefix for ERC-7201 namespace identifiers. It should be derived from the project name or a unique naming convention specific to the project. Used only if the contract includes storage variables and upgradeability is enabled. Default is "myProject".␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --restrictions Whether to restrict certain users from transferring tokens, either via allowing or blocking them. This feature is experimental, not audited and is subject to change.␊ + --freezable Whether authorized accounts can freeze and unfreeze accounts for regulatory or security purposes. This feature is experimental, not audited and is subject to change.␊ + ` + +## solidity-rwa --help + +> Snapshot 1 + + `solidity-rwa: Make a real-world asset token that uses the ERC-20 standard. Experimental, some features are not audited and are subject to change.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --premintChainId The chain ID of the network on which to premint tokens.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --callback Whether to include support for code execution after transfers and approvals on recipient contracts in a single transaction.␊ + --permit Whether without paying gas, token holders will be able to allow third parties to transfer from their account.␊ + --votes Whether to keep track of historical balances for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps.␊ + --flashmint Whether to include built-in flash loans to allow lending tokens without requiring collateral as long as they're returned in the same transaction.␊ + --crossChainBridging Whether to allow authorized bridge contracts to mint and burn tokens for cross-chain transfers. Options are to use custom bridges on any chain, to embed an ERC-7786 based bridge directly in the token contract, or to use the SuperchainERC20 standard with the predeployed SuperchainTokenBridge. The SuperchainERC20 feature is only available on chains in the Superchain, and requires deploying your contract to the same address on every chain in the Superchain.␊ + --crossChainLinkAllowOverride Whether to allow replacing a crosschain link that has already been registered. Only used if crossChainBridging is set to "erc7786native".␊ + --namespacePrefix The prefix for ERC-7201 namespace identifiers. It should be derived from the project name or a unique naming convention specific to the project. Used only if the contract includes storage variables and upgradeability is enabled. Default is "myProject".␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --restrictions Whether to restrict certain users from transferring tokens, either via allowing or blocking them. This feature is experimental, not audited and is subject to change.␊ + --freezable Whether authorized accounts can freeze and unfreeze accounts for regulatory or security purposes. This feature is experimental, not audited and is subject to change.␊ + ` + +## solidity-account --help + +> Snapshot 1 + + `solidity-account: Make an account contract that follows the ERC-4337 standard.␊ + ␊ + Required:␊ + --name The name of the account contract␊ + ␊ + Options:␊ + --signatureValidation Whether to implement the ERC-1271 standard for validating signatures. This is useful for the account to verify signatures.␊ + --ERC721Holder Whether to implement the \`onERC721Received\` function to allow the account to receive ERC721 tokens.␊ + --ERC1155Holder Whether to implement the \`onERC1155Received\` function to allow the account to receive ERC1155 tokens.␊ + --signer Defines the signature verification algorithm used by the account to verify user operations. Options:␊ + - ECDSA: Standard Ethereum signature validation using secp256k1, validates signatures against a specified owner address␊ + - EIP7702: Special ECDSA validation using account's own address as signer, enables EOAs to delegate execution rights␊ + - Multisig: ERC-7913 multisignature requiring minimum number of signatures from authorized signers␊ + - MultisigWeighted: ERC-7913 weighted multisignature where signers have different voting weights␊ + - P256: NIST P-256 curve (secp256r1) validation for integration with Passkeys and HSMs␊ + - RSA: RSA PKCS#1 v1.5 signature validation (RFC8017) for PKI systems and HSMs␊ + - WebAuthn: Web Authentication (WebAuthn) assertion validation for integration with Passkeys and HSMs on top of P256␊ + --batchedExecution Whether to implement a minimal batching interface for the account to allow multiple operations to be executed in a single transaction following the ERC-7821 standard.␊ + --ERC7579Modules Whether to implement the ERC-7579 compatibility to enable functionality on the account with modules.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + ` + +## solidity-governor --help + +> Snapshot 1 + + `solidity-governor: Make a contract to implement governance, such as for a DAO.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --delay The delay since proposal is created until voting starts, default is "1 day"␊ + --period The length of period during which people can cast their vote, default is "1 week"␊ + ␊ + Options:␊ + --votes The type of voting to use␊ + --clockMode The clock mode used by the voting token. For Governor, this must be chosen to match what the ERC20 or ERC721 voting token uses.␊ + --timelock The type of timelock to use␊ + --blockTime The block time of the chain in seconds, default is 12␊ + --decimals The number of decimals to use for the contract, default is 18 for ERC20Votes and 0 for ERC721Votes (because it does not apply to ERC721Votes)␊ + --proposalThreshold Minimum number of votes an account must have to create a proposal, default is 0.␊ + --quorumMode The type of quorum mode to use␊ + --quorumPercent The percent required, in cases of quorumMode equals percent␊ + --quorumAbsolute The absolute quorum required, in cases of quorumMode equals absolute␊ + --storage Enable storage of proposal details and enumerability of proposals␊ + --settings Allow governance to update voting settings (delay, period, proposal threshold)␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## solidity-custom --help + +> Snapshot 1 + + `solidity-custom: Make a custom smart contract.␊ + ␊ + Required:␊ + --name The name of the contract␊ + ␊ + Options:␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --upgradeable Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## cairo-erc20 --help + +> Snapshot 1 + + `cairo-erc20: Make a fungible token per the ERC-20 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --decimals The number of decimals to use for the contract. Defaults to 18.␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --votes Whether to keep track of historical balances for voting in on-chain governance, with a way to delegate one's voting power to a trusted account.␊ + --appName Required when votes is enabled, for hashing and signing typed structured data. Name for domain separator implementing SNIP12Metadata trait. Prevents two applications from producing the same hash.␊ + --appVersion Required when votes is enabled, for hashing and signing typed structured data. Version for domain separator implementing SNIP12Metadata trait. Prevents two versions of the same application from producing the same hash.␊ + --access.type The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.␊ + --access.darInitialDelay The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".␊ + --access.darDefaultDelayIncrease The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".␊ + --access.darMaxTransferDelay The maximum delay for a default admin transfer (in case Roles (Default Admin Rules) is used). Default is "30 days".␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-erc721 --help + +> Snapshot 1 + + `cairo-erc721: Make a non-fungible token per the ERC-721 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --baseUri A base uri for the non-fungible token.␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --enumerable Whether to allow on-chain enumeration of all tokens or those owned by an account. Increases gas cost of transfers.␊ + --votes Whether to keep track of individual units for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps.␊ + --royaltyInfo.enabled Whether to enable royalty feature for the contract␊ + --royaltyInfo.defaultRoyaltyFraction The royalty fraction that will be default for all tokens. It will be used for a token if there's no custom royalty fraction set for it.␊ + --royaltyInfo.feeDenominator The denominator used to interpret a token's fee and to calculate the result fee fraction.␊ + --appName Required when votes is enabled, for hashing and signing typed structured data. Name for domain separator implementing SNIP12Metadata trait. Prevents two applications from producing the same hash.␊ + --appVersion Required when votes is enabled, for hashing and signing typed structured data. Version for domain separator implementing SNIP12Metadata trait. Prevents two versions of the same application from producing the same hash.␊ + --access.type The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.␊ + --access.darInitialDelay The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".␊ + --access.darDefaultDelayIncrease The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".␊ + --access.darMaxTransferDelay The maximum delay for a default admin transfer (in case Roles (Default Admin Rules) is used). Default is "30 days".␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-erc1155 --help + +> Snapshot 1 + + `cairo-erc1155: Make a non-fungible token per the ERC-1155 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --baseUri The location of the metadata for the token. Clients will replace any instance of {id} in this string with the tokenId.␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --updatableUri Whether privileged accounts will be able to set a new URI for all token types.␊ + --royaltyInfo.enabled Whether to enable royalty feature for the contract␊ + --royaltyInfo.defaultRoyaltyFraction The royalty fraction that will be default for all tokens. It will be used for a token if there's no custom royalty fraction set for it.␊ + --royaltyInfo.feeDenominator The denominator used to interpret a token's fee and to calculate the result fee fraction.␊ + --access.type The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.␊ + --access.darInitialDelay The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".␊ + --access.darDefaultDelayIncrease The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".␊ + --access.darMaxTransferDelay The maximum delay for a default admin transfer (in case Roles (Default Admin Rules) is used). Default is "30 days".␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-account --help + +> Snapshot 1 + + `cairo-account: Make a custom smart contract that represents an account that can be deployed and interacted with other contracts, and can be extended to implement custom logic. An account is a special type of contract that is used to validate and execute transactions.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --type Type of signature used for signature checking by the Account contract, Starknet account uses the STARK curve, Ethereum-flavored account uses the Secp256k1 curve.␊ + ␊ + Options:␊ + --declare Whether to enable the account to declare other contract classes.␊ + --deploy Whether to enables the account to be counterfactually deployed.␊ + --pubkey Whether to enables the account to change its own public key.␊ + --outsideExecution Whether to allow a protocol to submit transactions on behalf of the account, as long as it has the relevant signatures.␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-multisig --help + +> Snapshot 1 + + `cairo-multisig: Make a multi-signature smart contract, requiring a quorum of registered signers to approve and collectively execute transactions.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --quorum The minimal number of confirmations required by the Multisig to approve a transaction.␊ + ␊ + Options:␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-governor --help + +> Snapshot 1 + + `cairo-governor: Make a contract to implement governance, such as for a DAO.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --delay The delay since proposal is created until voting starts, in readable date time format matching /^(\\d+(?:\\.\\d+)?) +(second|minute|hour|day|week|month|year)s?$/, default is "1 day".␊ + --period The length of period during which people can cast their vote, in readable date time format matching /^(\\d+(?:\\.\\d+)?) +(second|minute|hour|day|week|month|year)s?$/, default is "1 week".␊ + ␊ + Options:␊ + --votes The type of voting to use. Either erc20votes, meaning voting power with a votes-enabled ERC20 token. Either erc721votes, meaning voting power with a votes-enabled ERC721 token. Voters can entrust their voting power to a delegate.␊ + --clockMode The clock mode used by the voting token. For now, only timestamp mode where the token uses voting durations expressed as timestamps is supported. For Governor, this must be chosen to match what the ERC20 or ERC721 voting token uses.␊ + --timelock Whether to add a delay to actions taken by the Governor. Gives users time to exit the system if they disagree with governance decisions. If "openzeppelin", Module compatible with OpenZeppelin's TimelockController.␊ + --decimals The number of decimals to use for the contract, default is 18 for ERC20Votes and 0 for ERC721Votes (because it does not apply to ERC721Votes).␊ + --proposalThreshold Minimum number of votes an account must have to create a proposal, default is 0.␊ + --quorumMode The type of quorum mode to use, either by percentage or absolute value.␊ + --quorumPercent The percent required, in cases of quorumMode equals percent.␊ + --quorumAbsolute The absolute quorum required, in cases of quorumMode equals absolute.␊ + --settings Whether to allow governance to update voting settings (delay, period, proposal threshold).␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --appName Required when votes is enabled, for hashing and signing typed structured data. Name for domain separator implementing SNIP12Metadata trait. Prevents two applications from producing the same hash.␊ + --appVersion Required when votes is enabled, for hashing and signing typed structured data. Version for domain separator implementing SNIP12Metadata trait. Prevents two versions of the same application from producing the same hash.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-vesting --help + +> Snapshot 1 + + `cairo-vesting: Make a vesting smart contract that manages the gradual release of ERC-20 tokens to a designated beneficiary based on a predefined vesting schedule.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --startDate The timestamp marking the beginning of the vesting period. In HTML input datetime-local format, e.g. "2026-03-15T14:30".␊ + --duration The total duration of the vesting period. In readable date time format matching /^(\\d+(?:\\.\\d+)?) +(second|minute|hour|day|week|month|year)s?$/␊ + --cliffDuration The duration of the cliff period. Must be less than or equal to the total duration. In readable date time format matching /^(\\d+(?:\\.\\d+)?) +(second|minute|hour|day|week|month|year)s?$/␊ + --schedule A vesting schedule implementation, tokens can either be vested gradually following a linear curve or with custom vesting schedule that requires the implementation of the VestingSchedule trait.␊ + ␊ + Options:␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## cairo-custom --help + +> Snapshot 1 + + `cairo-custom: Make a custom smart contract.␊ + ␊ + Required:␊ + --name The name of the contract␊ + ␊ + Options:␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --access.type The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.␊ + --access.darInitialDelay The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".␊ + --access.darDefaultDelayIncrease The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".␊ + --access.darMaxTransferDelay The maximum delay for a default admin transfer (in case Roles (Default Admin Rules) is used). Default is "30 days".␊ + --upgradeable Whether the smart contract is upgradeable.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --macros.withComponents Whether to use the "with_components" macro for simplified contract structure.␊ + ` + +## stellar-fungible --help + +> Snapshot 1 + + `stellar-fungible: Make a fungible token per the Fungible Token Standard, compatible with SEP-41, similar to ERC-20.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts.␊ + --explicitImplementations Whether the contract should use explicit trait implementations instead of using the default ones provided by the library.␊ + --upgradeable Whether the contract can be upgraded.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## stellar-stablecoin --help + +> Snapshot 1 + + `stellar-stablecoin: Make a stablecoin that uses Fungible Token Standard, compatible with SEP-41.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --premint The number of tokens to premint for the deployer.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts.␊ + --explicitImplementations Whether the contract should use explicit trait implementations instead of using the default ones provided by the library.␊ + --upgradeable Whether the contract can be upgraded.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + --limitations Whether to restrict certain users from transferring tokens, either via allowing or blocking them.␊ + ` + +## stellar-non-fungible --help + +> Snapshot 1 + + `stellar-non-fungible: Make a non-fungible token per the Non-Fungible Token Standard, compatible with SEP-50, similar to ERC-721.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --tokenUri The metadata URI returned by the token contract for every NFT.␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --enumerable Whether the NFTs are enumerable (can be iterated over).␊ + --consecutive To batch mint NFTs instead of minting them individually (sequential minting is mandatory).␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --mintable Whether privileged accounts will be able to create more supply or emit more tokens␊ + --sequential Whether the IDs of the minted NFTs will be sequential.␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts.␊ + --explicitImplementations Whether the contract should use explicit trait implementations instead of using the default ones provided by the library.␊ + --upgradeable Whether the contract can be upgraded.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## stylus-erc20 --help + +> Snapshot 1 + + `stylus-erc20: Make a fungible token per the ERC-20 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --permit Whether without paying gas, token holders will be able to allow third parties to transfer from their account.␊ + --flashmint Whether to include built-in flash loans to allow lending tokens without requiring collateral as long as they're returned in the same transaction.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## stylus-erc721 --help + +> Snapshot 1 + + `stylus-erc721: Make a non-fungible token per the ERC-721 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --enumerable Whether to allow on-chain enumeration of all tokens or those owned by an account. Increases gas cost of transfers.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## stylus-erc1155 --help + +> Snapshot 1 + + `stylus-erc1155: Make a non-fungible token per the ERC-1155 standard.␊ + ␊ + Required:␊ + --name The name of the contract␊ + ␊ + Options:␊ + --burnable Whether token holders will be able to destroy their tokens␊ + --supply Whether to keep track of total supply of tokens␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## confidential-erc7984 --help + +> Snapshot 1 + + `confidential-erc7984: Make a confidential fungible token in Solidity according to the ERC-7984 standard, similar to ERC-20 but with confidentiality.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + --contractURI The metadata URI for the token. Should follow the schema defined in [ERC-7572](https://eips.ethereum.org/EIPS/eip-7572).␊ + --networkConfig Specify the provider and network configuration to use for FHEVM contracts.␊ + ␊ + Options:␊ + --premint The number of tokens to premint for the deployer.␊ + --wrappable Whether to allow wrapping an ERC20 token into a confidential fungible token.␊ + --votes Whether to keep track of historical balances for voting in on-chain governance. Voting durations must be expressed as block numbers or timestamps.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## uniswap-hooks --help + +> Snapshot 1 + + `uniswap-hooks: Make a Uniswap v4 hook contract using the OpenZeppelin Uniswap Hooks library.␊ + ␊ + Required:␊ + --hook The name of the Uniswap hook. Available hooks:␊ + - BaseHook: Base hook implementation that defines all hook entry points, security and permission helpers. Based on the Uniswap v4 periphery implementation. Hook entry points must be overridden and implemented by the inheriting hook to be used, with respective flags set in getHookPermissions.␊ + - BaseAsyncSwap: Base implementation for async swaps that skip the v3-like swap implementation by taking the full swap input amount and returning a delta that nets out the specified amount to 0. Allows developers to implement arbitrary logic for executing swaps, including asynchronous swaps and custom swap-ordering. Mints ERC-6909 claim tokens for the specified currency and amount.␊ + - BaseCustomAccounting: Base implementation for custom accounting and hook-owned liquidity. Enables tokens to be deposited via the hook to allow control and flexibility over how liquidity is computed. The hook is the sole owner of the liquidity and manages fees over liquidity shares accordingly. Designed to work with a single pool key.␊ + - BaseCustomCurve: Base implementation for custom curves that overrides the default v3-like concentrated liquidity implementation. During a swap, calls a function to get the amount of tokens to be sent to the receiver. The return delta is then consumed and applied by the PoolManager. Does not include fee or salt mechanisms by default.␊ + - BaseDynamicFee: Base implementation to apply a dynamic fee via the PoolManager's updateDynamicLPFee function. Allows hooks to update LP fees dynamically based on external conditions. Includes a poke function that can be called by anyone to update the fee. Alternative names to refer to the hook: 'Dynamic pool fee'.␊ + - BaseOverrideFee: Base implementation for automatic dynamic fees applied before swaps. Allows hooks to override the pool's fee before a swap is processed using the override fee flag. The fee is calculated dynamically and applied to the swap. Alternative names to refer to the hook: 'Dynamic swap fee'.␊ + - BaseDynamicAfterFee: Base implementation for dynamic target hook fees applied after swaps. Enforces a dynamic target for the unspecified currency during beforeSwap, where if the swap outcome is better than the target, any positive difference is taken as a hook fee. Fees are handled or distributed by the hook via afterSwapHandler. Alternative names to refer to the hook: 'Swap target enforcer'.␊ + - BaseHookFee: Base implementation for applying hook fees to the unspecified currency of the swap. These fees are independent of the pool's LP fee and are charged as a percentage of the output amount after the swap completes. Fees are taken as ERC-6909 claims.␊ + - AntiSandwichHook: Implements sandwich-resistant AMM design that guarantees no swaps get filled at a price better than the price at the beginning of the slot window. Within a slot window, swaps impact the pool asymmetrically for buys and sells. Only protects swaps in the zeroForOne direction. Alternative names to refer to the hook: 'Sandwich resistance'.␊ + - LiquidityPenaltyHook: Just-in-Time (JIT) liquidity provisioning resistant hook that disincentivizes JIT attacks by penalizing LP fee collection during liquidity removal and disabling it during liquidity addition if liquidity was recently added. The penalty is donated to the pool's liquidity providers in range at the time of removal. Alternative names to refer to the hook: 'JIT liquidity resistance'.␊ + - LimitOrderHook: Limit Order Mechanism hook that allows users to place limit orders at specific ticks outside of the current price range. Orders will be filled if the pool's price crosses the order's tick. Orders can be cancelled at any time until filled. Once completely filled, the resulting liquidity can be withdrawn from the pool.␊ + - ReHypothecationHook: A Uniswap V4 hook that enables rehypothecation of liquidity positions. Allows users to deposit assets into yield-generating sources while providing liquidity to Uniswap pools Just-in-Time during swaps. Assets earn yield when idle and are temporarily injected as pool liquidity only when needed for swap execution, then immediately withdrawn back to yield sources. Users receive ERC20 shares representing their rehypothecated position. Alternative names to refer to the hook: 'Liquidity rehypothecation'.␊ + - BaseOracleHook: A hook that enables a Uniswap V4 pool to record price observations and expose an oracle interface. Records cumulative tick values and provides time-weighted average price data. Allows increasing observation cardinality to store more historical price data. Provides observe function to get cumulative tick values for specified time periods.␊ + - OracleHookWithV3Adapters: A hook that enables a Uniswap V4 pool to record price observations and expose an oracle interface with Uniswap V3-compatible adapters. Extends BaseOracleHook by automatically deploying standard and truncated V3 oracle adapters for each pool, making the oracle data compatible with existing V3 oracle interfaces and tools.␊ + --name The name of the contract␊ + --pausable Whether privileged accounts will be able to pause specifically marked functionality. Useful for emergency response.␊ + --currencySettler Whether to include the CurrencySettler utility to settle pending deltas with the PoolManager during flash accounting.␊ + --safeCast Whether to include the SafeCast library for safe integer conversions when handling balances or fees.␊ + --transientStorage Whether to include the TransientSlot and SlotDerivation helpers for temporary state that clears at the end of the transaction.␊ + --shares.options The implementation used to represent position shares. Options include disabling shares or issuing ERC20, ERC6909, or ERC1155 tokens.␊ + --permissions.beforeInitialize Whether to enable the \`_beforeInitialize\` callback to run before pools are initialized.␊ + --permissions.afterInitialize Whether to enable the \`_afterInitialize\` callback to react after pool initialization.␊ + --permissions.beforeAddLiquidity Whether to enable the \`_beforeAddLiquidity\` callback to validate or modify adds before liquidity is deposited.␊ + --permissions.beforeRemoveLiquidity Whether to enable the \`_beforeRemoveLiquidity\` callback to validate removals before liquidity is withdrawn.␊ + --permissions.afterAddLiquidity Whether to enable the \`_afterAddLiquidity\` callback to update accounting after liquidity is added.␊ + --permissions.afterRemoveLiquidity Whether to enable the \`_afterRemoveLiquidity\` callback to update accounting after liquidity is withdrawn.␊ + --permissions.beforeSwap Whether to enable the \`_beforeSwap\` callback to inspect and optionally modify swap parameters.␊ + --permissions.afterSwap Whether to enable the \`_afterSwap\` callback to perform post-swap accounting or logic.␊ + --permissions.beforeDonate Whether to enable the \`_beforeDonate\` callback to run before donations are processed.␊ + --permissions.afterDonate Whether to enable the \`_afterDonate\` callback to run after donations are processed.␊ + --permissions.beforeSwapReturnDelta Whether to allow \`_beforeSwap\` to return a \`BeforeSwapDelta\`, adjusting the assets that must be provided for the swap.␊ + --permissions.afterSwapReturnDelta Whether to allow \`_afterSwap\` to return an additional delta that adjusts the final swap settlement.␊ + --permissions.afterAddLiquidityReturnDelta Whether to allow \`_afterAddLiquidity\` to return a \`BalanceDelta\` that adjusts how liquidity addition balances are settled.␊ + --permissions.afterRemoveLiquidityReturnDelta Whether to allow \`_afterRemoveLiquidity\` to return a \`BalanceDelta\` that adjusts how liquidity removal balances are settled.␊ + --inputs.blockNumberOffset The number of blocks that must elapse after liquidity is added before it can be removed without penalties. Used by liquidity protection hooks.␊ + --inputs.maxAbsTickDelta The maximum absolute tick change that can be recorded per oracle observation. Lower values resist manipulation but lag during volatility.␊ + ␊ + Options:␊ + --shares.name The name of the share token when ERC20 shares are enabled.␊ + --shares.symbol The symbol of the share token when ERC20 shares are enabled.␊ + --shares.uri The metadata URI used when ERC1155 shares are enabled.␊ + --access The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.␊ + --info.securityContact Email where people can contact you to report security issues. Will only be visible if contract source code is verified.␊ + --info.license The license used by the contract, default is "MIT"␊ + ` + +## unknown command + +> Snapshot 1 + + `Unknown command: nonexistent-command␊ + ␊ + Run \`npx @openzeppelin/contracts-cli --help\` for available commands.␊ + ` + +## unknown option + +> Snapshot 1 + + `Error in 'solidity-erc20': Unknown option: --notreal␊ + ` + +## missing required option + +> Snapshot 1 + + `Error in 'solidity-erc20': Missing required options:␊ + --symbol The short symbol for the token␊ + ` + +## missing multiple required options + +> Snapshot 1 + + `Error in 'solidity-erc20': Missing required options:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ` + +## unexpected argument without -- + +> Snapshot 1 + + `Error in 'solidity-erc20': Unexpected argument: foo␊ + ` + +## missing value for string option (followed by another flag) + +> Snapshot 1 + + `Error in 'solidity-erc20': Missing value for option: --name␊ + ` + +## missing value for string option (at end of args) + +> Snapshot 1 + + `Error in 'solidity-erc20': Missing value for option: --name␊ + ` + +## invalid enum value + +> Snapshot 1 + + `Error in 'solidity-erc20': Invalid values for options:␊ + --votes Whether to keep track of historical balances for voting in on-chain governance. Voting durations can be expressed as block numbers or timestamps.␊ + ` + +## invalid number value + +> Snapshot 1 + + `Error in 'solidity-governor': Invalid values for options:␊ + --quorumPercent The percent required, in cases of quorumMode equals percent␊ + ` + +## duplicate flag + +> Snapshot 1 + + `Error in 'solidity-erc20': Duplicate option: --name␊ + ` diff --git a/packages/cli/src/cli.test.ts.snap b/packages/cli/src/cli.test.ts.snap new file mode 100644 index 000000000..75036e5d9 Binary files /dev/null and b/packages/cli/src/cli.test.ts.snap differ diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000..4b7970731 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +import { generateHelp } from './cli-adapter'; +import { registry } from './registry'; + +const [command, ...commandArgs] = process.argv.slice(2); + +if (!command || command === '--help' || command === '-h') { + process.stdout.write(`Usage: npx @openzeppelin/contracts-cli [options] + +Commands: + ${Object.keys(registry).join(', ')} + +Generated contract source code is printed to stdout. + +Run \`npx @openzeppelin/contracts-cli --help\` for command-specific options. +`); + process.exit(0); +} + +if (!(command in registry)) { + process.stderr.write(`Unknown command: ${command} + +Run \`npx @openzeppelin/contracts-cli --help\` for available commands. +`); + process.exit(1); +} + +const entry = registry[command as keyof typeof registry]; + +if (commandArgs.includes('--help') || commandArgs.includes('-h')) { + process.stdout.write(generateHelp(command, entry.schema, entry.description) + '\n'); + process.exit(0); +} + +try { + process.stdout.write(entry.run(commandArgs)); +} catch (e) { + const message = e instanceof Error ? e.message : String(e); + process.stderr.write(`Error in '${command}': ${message}\n`); + process.exit(1); +} diff --git a/packages/cli/src/registry.ts b/packages/cli/src/registry.ts new file mode 100644 index 000000000..1a7648ecb --- /dev/null +++ b/packages/cli/src/registry.ts @@ -0,0 +1,140 @@ +import type { z } from 'zod'; +import { parseArgsFromSchema } from './cli-adapter'; + +import { erc20, erc721, erc1155, stablecoin, realWorldAsset, account, governor, custom } from '@openzeppelin/wizard'; +import { solidityPrompts } from '@openzeppelin/wizard-common'; +import { + solidityERC20Schema, + solidityERC721Schema, + solidityERC1155Schema, + solidityStablecoinSchema, + solidityRWASchema, + solidityAccountSchema, + solidityGovernorSchema, + solidityCustomSchema, +} from '@openzeppelin/wizard-common/schemas'; + +import { + erc20 as cairoErc20, + erc721 as cairoErc721, + erc1155 as cairoErc1155, + account as cairoAccount, + multisig as cairoMultisig, + governor as cairoGovernor, + vesting as cairoVesting, + custom as cairoCustom, +} from '@openzeppelin/wizard-cairo'; +import { cairoPrompts } from '@openzeppelin/wizard-common'; +import { + cairoERC20Schema, + cairoERC721Schema, + cairoERC1155Schema, + cairoAccountSchema, + cairoMultisigSchema, + cairoGovernorSchema, + cairoVestingSchema, + cairoCustomSchema, +} from '@openzeppelin/wizard-common/schemas'; + +import { fungible, stablecoin as stellarStablecoin, nonFungible } from '@openzeppelin/wizard-stellar'; +import { stellarPrompts } from '@openzeppelin/wizard-common'; +import { + stellarFungibleSchema, + stellarStablecoinSchema, + stellarNonFungibleSchema, +} from '@openzeppelin/wizard-common/schemas'; + +import { erc20 as stylusErc20, erc721 as stylusErc721, erc1155 as stylusErc1155 } from '@openzeppelin/wizard-stylus'; +import { stylusPrompts } from '@openzeppelin/wizard-common'; +import { stylusERC20Schema, stylusERC721Schema, stylusERC1155Schema } from '@openzeppelin/wizard-common/schemas'; + +import { erc7984 } from '@openzeppelin/wizard-confidential'; +import { confidentialPrompts } from '@openzeppelin/wizard-common'; +import { confidentialERC7984Schema } from '@openzeppelin/wizard-common/schemas'; + +import { hooks } from '@openzeppelin/wizard-uniswap-hooks'; +import { uniswapHooksPrompts } from '@openzeppelin/wizard-common'; +import { uniswapHooksHooksSchema } from '@openzeppelin/wizard-common/schemas'; + +export interface RegistryEntry { + schema: T; + print(opts: z.infer>): string; + run(argv: string[]): string; + description: string; +} + +function createRegistryEntry( + schema: T, + print: (opts: z.infer>) => string, + description: string, +): RegistryEntry { + return { + schema, + print(opts) { + return print(opts); + }, + run(argv) { + const opts = parseArgsFromSchema(schema, argv); + return print(opts); + }, + description, + }; +} + +export const registry = { + // Solidity + 'solidity-erc20': createRegistryEntry(solidityERC20Schema, opts => erc20.print(opts), solidityPrompts.ERC20), + 'solidity-erc721': createRegistryEntry(solidityERC721Schema, opts => erc721.print(opts), solidityPrompts.ERC721), + 'solidity-erc1155': createRegistryEntry(solidityERC1155Schema, opts => erc1155.print(opts), solidityPrompts.ERC1155), + 'solidity-stablecoin': createRegistryEntry( + solidityStablecoinSchema, + opts => stablecoin.print(opts), + solidityPrompts.Stablecoin, + ), + 'solidity-rwa': createRegistryEntry(solidityRWASchema, opts => realWorldAsset.print(opts), solidityPrompts.RWA), + 'solidity-account': createRegistryEntry(solidityAccountSchema, opts => account.print(opts), solidityPrompts.Account), + 'solidity-governor': createRegistryEntry( + solidityGovernorSchema, + opts => governor.print(opts), + solidityPrompts.Governor, + ), + 'solidity-custom': createRegistryEntry(solidityCustomSchema, opts => custom.print(opts), solidityPrompts.Custom), + + // Cairo + 'cairo-erc20': createRegistryEntry(cairoERC20Schema, opts => cairoErc20.print(opts), cairoPrompts.ERC20), + 'cairo-erc721': createRegistryEntry(cairoERC721Schema, opts => cairoErc721.print(opts), cairoPrompts.ERC721), + 'cairo-erc1155': createRegistryEntry(cairoERC1155Schema, opts => cairoErc1155.print(opts), cairoPrompts.ERC1155), + 'cairo-account': createRegistryEntry(cairoAccountSchema, opts => cairoAccount.print(opts), cairoPrompts.Account), + 'cairo-multisig': createRegistryEntry(cairoMultisigSchema, opts => cairoMultisig.print(opts), cairoPrompts.Multisig), + 'cairo-governor': createRegistryEntry(cairoGovernorSchema, opts => cairoGovernor.print(opts), cairoPrompts.Governor), + 'cairo-vesting': createRegistryEntry(cairoVestingSchema, opts => cairoVesting.print(opts), cairoPrompts.Vesting), + 'cairo-custom': createRegistryEntry(cairoCustomSchema, opts => cairoCustom.print(opts), cairoPrompts.Custom), + + // Stellar + 'stellar-fungible': createRegistryEntry(stellarFungibleSchema, opts => fungible.print(opts), stellarPrompts.Fungible), + 'stellar-stablecoin': createRegistryEntry( + stellarStablecoinSchema, + opts => stellarStablecoin.print(opts), + stellarPrompts.Stablecoin, + ), + 'stellar-non-fungible': createRegistryEntry( + stellarNonFungibleSchema, + opts => nonFungible.print(opts), + stellarPrompts.NonFungible, + ), + + // Stylus + 'stylus-erc20': createRegistryEntry(stylusERC20Schema, opts => stylusErc20.print(opts), stylusPrompts.ERC20), + 'stylus-erc721': createRegistryEntry(stylusERC721Schema, opts => stylusErc721.print(opts), stylusPrompts.ERC721), + 'stylus-erc1155': createRegistryEntry(stylusERC1155Schema, opts => stylusErc1155.print(opts), stylusPrompts.ERC1155), + + // Confidential + 'confidential-erc7984': createRegistryEntry( + confidentialERC7984Schema, + opts => erc7984.print(opts), + confidentialPrompts.ERC7984, + ), + + // Uniswap Hooks + 'uniswap-hooks': createRegistryEntry(uniswapHooksHooksSchema, opts => hooks.print(opts), uniswapHooksPrompts.Hooks), +} satisfies Record; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000..f6a21a39d --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "module": "commonjs", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/common/ava.config.js b/packages/common/ava.config.js new file mode 100644 index 000000000..5858bdf5b --- /dev/null +++ b/packages/common/ava.config.js @@ -0,0 +1,7 @@ +module.exports = { + extensions: ['ts'], + require: ['ts-node/register'], + timeout: '10m', + workerThreads: false, + files: ['src/**/*.test.ts'], +}; diff --git a/packages/common/package.json b/packages/common/package.json index 160b53e60..0ae5bc480 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -6,6 +6,15 @@ "repository": "https://github.com/OpenZeppelin/contracts-wizard", "main": "dist/index.js", "ts:main": "src/index.ts", + "exports": { + ".": "./dist/index.js", + "./schemas": "./dist/ai/schemas/index.js" + }, + "typesVersions": { + "*": { + "schemas": ["dist/ai/schemas/index.d.ts"] + } + }, "files": [ "LICENSE", "NOTICE", @@ -15,10 +24,23 @@ ], "scripts": { "prepare": "tsc", - "watch": "tsc --watch" + "watch": "tsc --watch", + "test": "ava", + "test:watch": "ava --watch" + }, + "dependencies": { + "zod": "^4.0" }, "devDependencies": { + "@openzeppelin/wizard": "^0.10.7", + "@openzeppelin/wizard-cairo": "^3.0.0", + "@openzeppelin/wizard-stellar": "^0.5.0", + "@openzeppelin/wizard-stylus": "^0.3.0", + "@openzeppelin/wizard-confidential": "^0.1.0", + "@openzeppelin/wizard-uniswap-hooks": "^0.1.0", "@types/node": "^20.0.0", + "ava": "^6.0.0", + "ts-node": "^10.4.0", "typescript": "^5.0.0" } } diff --git a/packages/common/src/ai/descriptions/cairo.ts b/packages/common/src/ai/descriptions/cairo.ts index 7f6effc16..96688117e 100644 --- a/packages/common/src/ai/descriptions/cairo.ts +++ b/packages/common/src/ai/descriptions/cairo.ts @@ -34,11 +34,12 @@ export const cairoMacrosDescriptions = { export const cairoAccessDescriptions = { accessType: 'The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.', - darInitialDelay: 'The initial delay for the default admin role (in case Roles (Default Admin Rules) is used).', + darInitialDelay: + 'The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".', darDefaultDelayIncrease: - 'The default delay increase in seconds for the default admin role (in case Roles (Default Admin Rules) is used).', + 'The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".', darMaxTransferDelay: - 'The maximum delay in seconds for a default admin transfer (in case Roles (Default Admin Rules) is used).', + 'The maximum delay for a default admin transfer (in case Roles (Default Admin Rules) is used). Default is "30 days".', }; export const cairoRoyaltyInfoDescriptions = { @@ -97,7 +98,8 @@ export const cairoGovernorDescriptions = { }; export const cairoVestingDescriptions = { - startDate: 'The timestamp marking the beginning of the vesting period. In HTML input datetime-local format', + startDate: + 'The timestamp marking the beginning of the vesting period. In HTML input datetime-local format, e.g. "2026-03-15T14:30".', duration: 'The total duration of the vesting period. In readable date time format matching /^(\\d+(?:\\.\\d+)?) +(second|minute|hour|day|week|month|year)s?$/', cliffDuration: diff --git a/packages/common/src/ai/descriptions/solidity.ts b/packages/common/src/ai/descriptions/solidity.ts index 3940b6b67..37ae5abdf 100644 --- a/packages/common/src/ai/descriptions/solidity.ts +++ b/packages/common/src/ai/descriptions/solidity.ts @@ -85,7 +85,7 @@ export const solidityAccountDescriptions = { export const solidityGovernorDescriptions = { delay: 'The delay since proposal is created until voting starts, default is "1 day"', period: 'The length of period during which people can cast their vote, default is "1 week"', - blockTime: 'The block time of the chain, default is 12', + blockTime: 'The block time of the chain in seconds, default is 12', proposalThreshold: 'Minimum number of votes an account must have to create a proposal, default is 0.', decimals: 'The number of decimals to use for the contract, default is 18 for ERC20Votes and 0 for ERC721Votes (because it does not apply to ERC721Votes)', diff --git a/packages/common/src/ai/descriptions/uniswap-hooks.ts b/packages/common/src/ai/descriptions/uniswap-hooks.ts index 0c6abf84b..7585737ea 100644 --- a/packages/common/src/ai/descriptions/uniswap-hooks.ts +++ b/packages/common/src/ai/descriptions/uniswap-hooks.ts @@ -33,11 +33,7 @@ export const byHooksDescriptions = { } as const; export const uniswapHooksPrompts = { - Hooks: `Make a Uniswap v4 hook contract using the OpenZeppelin Uniswap Hooks library. ${Object.entries( - byHooksDescriptions, - ) - .map(([hookName, hookDescription]) => `${hookName}: ${hookDescription}`) - .join(', ')}`, + Hooks: 'Make a Uniswap v4 hook contract using the OpenZeppelin Uniswap Hooks library.', }; export const uniswapHooksSharesDescriptions = { @@ -49,7 +45,9 @@ export const uniswapHooksSharesDescriptions = { }; export const uniswapHooksDescriptions = { - hook: 'The name of the Uniswap hook', + hook: `The name of the Uniswap hook. Available hooks:\n${Object.entries(byHooksDescriptions) + .map(([name, desc]) => ` - ${name}: ${desc}`) + .join('\n')}`, currencySettler: 'Whether to include the CurrencySettler utility to settle pending deltas with the PoolManager during flash accounting.', safeCast: 'Whether to include the SafeCast library for safe integer conversions when handling balances or fees.', diff --git a/packages/mcp/src/cairo/schemas.ts b/packages/common/src/ai/schemas/cairo.ts similarity index 76% rename from packages/mcp/src/cairo/schemas.ts rename to packages/common/src/ai/schemas/cairo.ts index 88fa68a5e..3131ca8c5 100644 --- a/packages/mcp/src/cairo/schemas.ts +++ b/packages/common/src/ai/schemas/cairo.ts @@ -13,39 +13,22 @@ import { cairoGovernorDescriptions, cairoMultisigDescriptions, cairoVestingDescriptions, -} from '@openzeppelin/wizard-common'; -import type { KindedOptions } from '@openzeppelin/wizard-cairo'; +} from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - ERC20: z.object(erc20Schema).parse({}), - ERC721: z.object(erc721Schema).parse({}), - ERC1155: z.object(erc1155Schema).parse({}), - Account: z.object(accountSchema).parse({}), - Multisig: z.object(multisigSchema).parse({}), - Governor: z.object(governorSchema).parse({}), - Vesting: z.object(vestingSchema).parse({}), - Custom: z.object(customSchema).parse({}), - }; -} - -export const commonSchema = { - access: z.object({ - type: z - .literal('ownable') - .or(z.literal('roles')) - .or(z.literal('roles-dar')) - .or(z.literal(false)) - .describe(cairoAccessDescriptions.accessType), - darInitialDelay: z.string().describe(cairoAccessDescriptions.darInitialDelay), - darDefaultDelayIncrease: z.string().describe(cairoAccessDescriptions.darDefaultDelayIncrease), - darMaxTransferDelay: z.string().describe(cairoAccessDescriptions.darMaxTransferDelay), - }), +export const cairoCommonSchema = { + access: z + .object({ + type: z + .literal('ownable') + .or(z.literal('roles')) + .or(z.literal('roles-dar')) + .or(z.literal(false)) + .describe(cairoAccessDescriptions.accessType), + darInitialDelay: z.string().describe(cairoAccessDescriptions.darInitialDelay), + darDefaultDelayIncrease: z.string().describe(cairoAccessDescriptions.darDefaultDelayIncrease), + darMaxTransferDelay: z.string().describe(cairoAccessDescriptions.darMaxTransferDelay), + }) + .optional(), upgradeable: z.boolean().optional().describe(cairoCommonDescriptions.upgradeable), info: z .object({ @@ -62,7 +45,7 @@ export const commonSchema = { .describe(cairoMacrosDescriptions.macros), } as const satisfies z.ZodRawShape; -export const erc20Schema = { +export const cairoERC20Schema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), decimals: z.string().optional().describe(cairoERC20Descriptions.decimals), @@ -73,10 +56,10 @@ export const erc20Schema = { votes: z.boolean().optional().describe(cairoERC20Descriptions.votes), appName: z.string().optional().describe(cairoCommonDescriptions.appName), appVersion: z.string().optional().describe(cairoCommonDescriptions.appVersion), - ...commonSchema, + ...cairoCommonSchema, } as const satisfies z.ZodRawShape; -export const erc721Schema = { +export const cairoERC721Schema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), baseUri: z.string().optional().describe(cairoERC721Descriptions.baseUri), @@ -95,10 +78,10 @@ export const erc721Schema = { .describe(cairoCommonDescriptions.royaltyInfo), appName: z.string().optional().describe(cairoCommonDescriptions.appName), appVersion: z.string().optional().describe(cairoCommonDescriptions.appVersion), - ...commonSchema, + ...cairoCommonSchema, } as const satisfies z.ZodRawShape; -export const erc1155Schema = { +export const cairoERC1155Schema = { name: z.string().describe(commonDescriptions.name), baseUri: z.string().describe(cairoERC1155Descriptions.baseUri), burnable: z.boolean().optional().describe(commonDescriptions.burnable), @@ -113,30 +96,30 @@ export const erc1155Schema = { }) .optional() .describe(cairoCommonDescriptions.royaltyInfo), - ...commonSchema, + ...cairoCommonSchema, } as const satisfies z.ZodRawShape; -export const accountSchema = { +export const cairoAccountSchema = { name: z.string().describe(commonDescriptions.name), type: z.enum(['stark', 'eth']).describe(cairoAccountDescriptions.type), declare: z.boolean().optional().describe(cairoAccountDescriptions.declare), deploy: z.boolean().optional().describe(cairoAccountDescriptions.deploy), pubkey: z.boolean().optional().describe(cairoAccountDescriptions.pubkey), outsideExecution: z.boolean().optional().describe(cairoAccountDescriptions.outsideExecution), - upgradeable: commonSchema.upgradeable, - info: commonSchema.info, - macros: commonSchema.macros, + upgradeable: cairoCommonSchema.upgradeable, + info: cairoCommonSchema.info, + macros: cairoCommonSchema.macros, } as const satisfies z.ZodRawShape; -export const multisigSchema = { +export const cairoMultisigSchema = { name: z.string().describe(commonDescriptions.name), quorum: z.string().describe(cairoMultisigDescriptions.quorum), - upgradeable: commonSchema.upgradeable, - info: commonSchema.info, - macros: commonSchema.macros, + upgradeable: cairoCommonSchema.upgradeable, + info: cairoCommonSchema.info, + macros: cairoCommonSchema.macros, } as const satisfies z.ZodRawShape; -export const governorSchema = { +export const cairoGovernorSchema = { name: z.string().describe(commonDescriptions.name), delay: z.string().describe(cairoGovernorDescriptions.delay), period: z.string().describe(cairoGovernorDescriptions.period), @@ -152,25 +135,25 @@ export const governorSchema = { quorumPercent: z.number().optional().describe(cairoGovernorDescriptions.quorumPercent), quorumAbsolute: z.string().optional().describe(cairoGovernorDescriptions.quorumAbsolute), settings: z.boolean().optional().describe(cairoGovernorDescriptions.settings), - upgradeable: commonSchema.upgradeable, + upgradeable: cairoCommonSchema.upgradeable, appName: z.string().optional().describe(cairoCommonDescriptions.appName), appVersion: z.string().optional().describe(cairoCommonDescriptions.appVersion), - info: commonSchema.info, - macros: commonSchema.macros, + info: cairoCommonSchema.info, + macros: cairoCommonSchema.macros, } as const satisfies z.ZodRawShape; -export const vestingSchema = { +export const cairoVestingSchema = { name: z.string().describe(commonDescriptions.name), startDate: z.string().describe(cairoVestingDescriptions.startDate), duration: z.string().describe(cairoVestingDescriptions.duration), cliffDuration: z.string().describe(cairoVestingDescriptions.cliffDuration), schedule: z.enum(['linear', 'custom']).describe(cairoVestingDescriptions.schedule), - info: commonSchema.info, - macros: commonSchema.macros, + info: cairoCommonSchema.info, + macros: cairoCommonSchema.macros, } as const satisfies z.ZodRawShape; -export const customSchema = { +export const cairoCustomSchema = { name: z.string().describe(commonDescriptions.name), pausable: z.boolean().optional().describe(commonDescriptions.pausable), - ...commonSchema, + ...cairoCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/mcp/src/confidential/schemas.ts b/packages/common/src/ai/schemas/confidential.ts similarity index 67% rename from packages/mcp/src/confidential/schemas.ts rename to packages/common/src/ai/schemas/confidential.ts index 822a6a318..0754ef285 100644 --- a/packages/mcp/src/confidential/schemas.ts +++ b/packages/common/src/ai/schemas/confidential.ts @@ -1,19 +1,7 @@ import { z } from 'zod'; -import { commonDescriptions, infoDescriptions, confidentialERC7984Descriptions } from '@openzeppelin/wizard-common'; -import type { KindedOptions } from '@openzeppelin/wizard-confidential'; +import { commonDescriptions, infoDescriptions, confidentialERC7984Descriptions } from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - ERC7984: z.object(erc7984Schema).parse({}), - }; -} - -export const commonSchema = { +export const confidentialCommonSchema = { info: z .object({ securityContact: z.string().optional().describe(infoDescriptions.securityContact), @@ -23,7 +11,7 @@ export const commonSchema = { .describe(infoDescriptions.info), } as const satisfies z.ZodRawShape; -export const erc7984Schema = { +export const confidentialERC7984Schema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), contractURI: z.string().describe(confidentialERC7984Descriptions.contractURI), @@ -31,5 +19,5 @@ export const erc7984Schema = { networkConfig: z.literal('zama-ethereum').describe(confidentialERC7984Descriptions.networkConfig), wrappable: z.boolean().optional().describe(confidentialERC7984Descriptions.wrappable), votes: z.literal('blocknumber').or(z.literal('timestamp')).optional().describe(confidentialERC7984Descriptions.votes), - ...commonSchema, + ...confidentialCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/common/src/ai/schemas/index.ts b/packages/common/src/ai/schemas/index.ts new file mode 100644 index 000000000..3d2c04416 --- /dev/null +++ b/packages/common/src/ai/schemas/index.ts @@ -0,0 +1,42 @@ +export { + solidityCommonSchema, + solidityERC20Schema, + solidityERC721Schema, + solidityERC1155Schema, + solidityStablecoinSchema, + solidityRWASchema, + solidityAccountSchema, + solidityGovernorSchema, + solidityCustomSchema, +} from './solidity'; + +export { + cairoCommonSchema, + cairoERC20Schema, + cairoERC721Schema, + cairoERC1155Schema, + cairoAccountSchema, + cairoMultisigSchema, + cairoGovernorSchema, + cairoVestingSchema, + cairoCustomSchema, +} from './cairo'; + +export { + stellarCommonSchema, + stellarFungibleSchema, + stellarStablecoinSchema, + stellarNonFungibleSchema, +} from './stellar'; + +export { stylusCommonSchema, stylusERC20Schema, stylusERC721Schema, stylusERC1155Schema } from './stylus'; + +export { confidentialCommonSchema, confidentialERC7984Schema } from './confidential'; + +export { + uniswapHooksCommonSchema, + uniswapHooksSharesSchema, + uniswapHooksPermissionsSchema, + uniswapHooksInputsSchema, + uniswapHooksHooksSchema, +} from './uniswap-hooks'; diff --git a/packages/common/src/ai/schemas/schemas.test.ts b/packages/common/src/ai/schemas/schemas.test.ts new file mode 100644 index 000000000..c24af2f55 --- /dev/null +++ b/packages/common/src/ai/schemas/schemas.test.ts @@ -0,0 +1,192 @@ +import test from 'ava'; +import { z } from 'zod'; + +import type { KindedOptions as SolidityKindedOptions } from '@openzeppelin/wizard'; +import type { KindedOptions as CairoKindedOptions } from '@openzeppelin/wizard-cairo'; +import type { KindedOptions as StellarKindedOptions } from '@openzeppelin/wizard-stellar'; +import type { KindedOptions as StylusKindedOptions } from '@openzeppelin/wizard-stylus'; +import type { KindedOptions as ConfidentialKindedOptions } from '@openzeppelin/wizard-confidential'; +import type { KindedOptions as UniswapHooksKindedOptions } from '@openzeppelin/wizard-uniswap-hooks'; + +import { + solidityERC20Schema, + solidityERC721Schema, + solidityERC1155Schema, + solidityStablecoinSchema, + solidityRWASchema, + solidityAccountSchema, + solidityGovernorSchema, + solidityCustomSchema, + cairoERC20Schema, + cairoERC721Schema, + cairoERC1155Schema, + cairoAccountSchema, + cairoMultisigSchema, + cairoGovernorSchema, + cairoVestingSchema, + cairoCustomSchema, + stellarFungibleSchema, + stellarStablecoinSchema, + stellarNonFungibleSchema, + stylusERC20Schema, + stylusERC721Schema, + stylusERC1155Schema, + confidentialERC7984Schema, + uniswapHooksHooksSchema, +} from './index'; + +/** + * Verifies that every field in a Zod schema shape has a .describe() string. + * Guards against accidentally dropping descriptions when modifying schemas. + */ +function checkAllFieldsHaveDescriptions( + t: { fail: (msg: string) => void }, + shape: z.ZodRawShape, + prefix = '', +): boolean { + let ok = true; + for (const [key, schema] of Object.entries(shape)) { + const zodSchema = schema as z.ZodTypeAny; + const fullKey = prefix ? `${prefix}.${key}` : key; + + // Unwrap optional to check inner type + const innerSchema = zodSchema instanceof z.ZodOptional ? zodSchema.unwrap() : zodSchema; + + // For ZodObject fields, recurse into inner fields (the object itself may or may not have a description) + if (innerSchema instanceof z.ZodObject) { + if (!checkAllFieldsHaveDescriptions(t, innerSchema.shape, fullKey)) { + ok = false; + } + continue; + } + + const desc = + zodSchema.description ?? + (zodSchema instanceof z.ZodOptional ? (zodSchema.unwrap() as z.ZodTypeAny).description : undefined); + if (!desc) { + t.fail(`Field "${fullKey}" is missing a .describe() string`); + ok = false; + } + } + return ok; +} + +// --- Description presence tests --- + +const allSchemas: [string, z.ZodRawShape][] = [ + ['solidityERC20', solidityERC20Schema], + ['solidityERC721', solidityERC721Schema], + ['solidityERC1155', solidityERC1155Schema], + ['solidityStablecoin', solidityStablecoinSchema], + ['solidityRWA', solidityRWASchema], + ['solidityAccount', solidityAccountSchema], + ['solidityGovernor', solidityGovernorSchema], + ['solidityCustom', solidityCustomSchema], + ['cairoERC20', cairoERC20Schema], + ['cairoERC721', cairoERC721Schema], + ['cairoERC1155', cairoERC1155Schema], + ['cairoAccount', cairoAccountSchema], + ['cairoMultisig', cairoMultisigSchema], + ['cairoGovernor', cairoGovernorSchema], + ['cairoVesting', cairoVestingSchema], + ['cairoCustom', cairoCustomSchema], + ['stellarFungible', stellarFungibleSchema], + ['stellarStablecoin', stellarStablecoinSchema], + ['stellarNonFungible', stellarNonFungibleSchema], + ['stylusERC20', stylusERC20Schema], + ['stylusERC721', stylusERC721Schema], + ['stylusERC1155', stylusERC1155Schema], + ['confidentialERC7984', confidentialERC7984Schema], + ['uniswapHooksHooks', uniswapHooksHooksSchema], +]; + +for (const [name, shape] of allSchemas) { + test(`${name} schema: all fields have descriptions`, t => { + if (checkAllFieldsHaveDescriptions(t, shape)) { + t.pass(); + } + }); +} + +// --- Type sync tests --- +// These are compile-time only. If a schema produces a type incompatible with +// the wizard options interface, TypeScript will error when compiling this file. +// The pattern mirrors _typeAssertions in the MCP schema files. + +function _solidityTypeAssertions() { + const _: { + [K in keyof SolidityKindedOptions]: Omit; + } = { + ERC20: z.object(solidityERC20Schema).parse({}), + ERC721: z.object(solidityERC721Schema).parse({}), + ERC1155: z.object(solidityERC1155Schema).parse({}), + Stablecoin: z.object(solidityStablecoinSchema).parse({}), + RealWorldAsset: z.object(solidityRWASchema).parse({}), + Account: z.object(solidityAccountSchema).parse({}), + Governor: z.object(solidityGovernorSchema).parse({}), + Custom: z.object(solidityCustomSchema).parse({}), + }; +} + +function _cairoTypeAssertions() { + const _: { + [K in keyof CairoKindedOptions]: Omit; + } = { + ERC20: z.object(cairoERC20Schema).parse({}), + ERC721: z.object(cairoERC721Schema).parse({}), + ERC1155: z.object(cairoERC1155Schema).parse({}), + Account: z.object(cairoAccountSchema).parse({}), + Multisig: z.object(cairoMultisigSchema).parse({}), + Governor: z.object(cairoGovernorSchema).parse({}), + Vesting: z.object(cairoVestingSchema).parse({}), + Custom: z.object(cairoCustomSchema).parse({}), + }; +} + +function _stellarTypeAssertions() { + const _: { + [K in keyof StellarKindedOptions]: Omit; + } = { + Fungible: z.object(stellarFungibleSchema).parse({}), + Stablecoin: z.object(stellarStablecoinSchema).parse({}), + NonFungible: z.object(stellarNonFungibleSchema).parse({}), + }; +} + +function _stylusTypeAssertions() { + const _: { + [K in keyof StylusKindedOptions]: Omit; + } = { + ERC20: z.object(stylusERC20Schema).parse({}), + ERC721: z.object(stylusERC721Schema).parse({}), + ERC1155: z.object(stylusERC1155Schema).parse({}), + }; +} + +function _confidentialTypeAssertions() { + const _: { + [K in keyof ConfidentialKindedOptions]: Omit; + } = { + ERC7984: z.object(confidentialERC7984Schema).parse({}), + }; +} + +function _uniswapHooksTypeAssertions() { + const _: { + [K in keyof UniswapHooksKindedOptions]: Omit; + } = { + Hooks: z.object(uniswapHooksHooksSchema).parse({}), + }; +} + +test('type assertions compile (schema-to-options type sync)', t => { + // These functions are never called — they only need to compile. + // If a schema drifts from its wizard options interface, this file won't compile. + void _solidityTypeAssertions; + void _cairoTypeAssertions; + void _stellarTypeAssertions; + void _stylusTypeAssertions; + void _confidentialTypeAssertions; + void _uniswapHooksTypeAssertions; + t.pass(); +}); diff --git a/packages/mcp/src/solidity/schemas.ts b/packages/common/src/ai/schemas/solidity.ts similarity index 83% rename from packages/mcp/src/solidity/schemas.ts rename to packages/common/src/ai/schemas/solidity.ts index 99b66abe1..e990fcbc7 100644 --- a/packages/mcp/src/solidity/schemas.ts +++ b/packages/common/src/ai/schemas/solidity.ts @@ -9,28 +9,9 @@ import { solidityGovernorDescriptions, solidityAccountDescriptions, solidityStablecoinDescriptions, -} from '@openzeppelin/wizard-common'; -import type { KindedOptions } from '@openzeppelin/wizard'; +} from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - ERC20: z.object(erc20Schema).parse({}), - ERC721: z.object(erc721Schema).parse({}), - ERC1155: z.object(erc1155Schema).parse({}), - Stablecoin: z.object(stablecoinSchema).parse({}), - RealWorldAsset: z.object(rwaSchema).parse({}), - Account: z.object(accountSchema).parse({}), - Governor: z.object(governorSchema).parse({}), - Custom: z.object(customSchema).parse({}), - }; -} - -export const commonSchema = { +export const solidityCommonSchema = { access: z .literal('ownable') .or(z.literal('roles')) @@ -51,7 +32,7 @@ export const commonSchema = { .describe(infoDescriptions.info), } as const satisfies z.ZodRawShape; -export const erc20Schema = { +export const solidityERC20Schema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), burnable: z.boolean().optional().describe(commonDescriptions.burnable), @@ -71,10 +52,10 @@ export const erc20Schema = { .describe(solidityERC20Descriptions.crossChainBridging), crossChainLinkAllowOverride: z.boolean().optional().describe(solidityERC20Descriptions.crossChainLinkAllowOverride), namespacePrefix: z.string().optional().describe(solidityCommonDescriptions.namespacePrefix), - ...commonSchema, + ...solidityCommonSchema, } as const satisfies z.ZodRawShape; -export const erc721Schema = { +export const solidityERC721Schema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), baseUri: z.string().optional().describe(solidityERC721Descriptions.baseUri), @@ -85,11 +66,11 @@ export const erc721Schema = { mintable: z.boolean().optional().describe(commonDescriptions.mintable), incremental: z.boolean().optional().describe(solidityERC721Descriptions.incremental), votes: z.literal('blocknumber').or(z.literal('timestamp')).optional().describe(solidityERC721Descriptions.votes), - ...commonSchema, + ...solidityCommonSchema, namespacePrefix: z.string().optional().describe(solidityCommonDescriptions.namespacePrefix), } as const satisfies z.ZodRawShape; -export const erc1155Schema = { +export const solidityERC1155Schema = { name: z.string().describe(commonDescriptions.name), uri: z.string().describe(solidityERC1155Descriptions.uri), burnable: z.boolean().optional().describe(commonDescriptions.burnable), @@ -97,13 +78,13 @@ export const erc1155Schema = { mintable: z.boolean().optional().describe(commonDescriptions.mintable), supply: z.boolean().optional().describe(solidityERC1155Descriptions.supply), updatableUri: z.boolean().optional().describe(solidityERC1155Descriptions.updatableUri), - ...commonSchema, + ...solidityCommonSchema, } as const satisfies z.ZodRawShape; -const { upgradeable: _, ...erc20SchemaOmitUpgradeable } = erc20Schema; +const { upgradeable: _, ...solidityERC20SchemaOmitUpgradeable } = solidityERC20Schema; -export const stablecoinSchema = { - ...erc20SchemaOmitUpgradeable, +export const solidityStablecoinSchema = { + ...solidityERC20SchemaOmitUpgradeable, restrictions: z .literal(false) .or(z.literal('allowlist')) @@ -113,9 +94,9 @@ export const stablecoinSchema = { freezable: z.boolean().optional().describe(solidityStablecoinDescriptions.freezable), } as const satisfies z.ZodRawShape; -export const rwaSchema = stablecoinSchema; +export const solidityRWASchema = solidityStablecoinSchema; -export const accountSchema = { +export const solidityAccountSchema = { name: z.string().describe('The name of the account contract'), signatureValidation: z .literal(false) @@ -143,11 +124,11 @@ export const accountSchema = { .or(z.literal('AccountERC7579Hooked')) .optional() .describe(solidityAccountDescriptions.ERC7579Modules), - info: commonSchema.info, - upgradeable: commonSchema.upgradeable, + info: solidityCommonSchema.info, + upgradeable: solidityCommonSchema.upgradeable, } as const satisfies z.ZodRawShape; -export const governorSchema = { +export const solidityGovernorSchema = { name: z.string().describe(commonDescriptions.name), delay: z.string().describe(solidityGovernorDescriptions.delay), period: z.string().describe(solidityGovernorDescriptions.period), @@ -175,12 +156,12 @@ export const governorSchema = { quorumAbsolute: z.string().optional().describe(solidityGovernorDescriptions.quorumAbsolute), storage: z.boolean().optional().describe(solidityGovernorDescriptions.storage), settings: z.boolean().optional().describe(solidityGovernorDescriptions.settings), - upgradeable: commonSchema.upgradeable, - info: commonSchema.info, + upgradeable: solidityCommonSchema.upgradeable, + info: solidityCommonSchema.info, } as const satisfies z.ZodRawShape; -export const customSchema = { +export const solidityCustomSchema = { name: z.string().describe(commonDescriptions.name), pausable: z.boolean().optional().describe(commonDescriptions.pausable), - ...commonSchema, + ...solidityCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/mcp/src/stellar/schemas.ts b/packages/common/src/ai/schemas/stellar.ts similarity index 75% rename from packages/mcp/src/stellar/schemas.ts rename to packages/common/src/ai/schemas/stellar.ts index 93174730e..443a76d2b 100644 --- a/packages/mcp/src/stellar/schemas.ts +++ b/packages/common/src/ai/schemas/stellar.ts @@ -6,23 +6,9 @@ import { stellarFungibleDescriptions, stellarNonFungibleDescriptions, stellarStablecoinDescriptions, -} from '@openzeppelin/wizard-common'; -import type { KindedOptions } from '@openzeppelin/wizard-stellar'; +} from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - Fungible: z.object(fungibleSchema).parse({}), - Stablecoin: z.object(stablecoinSchema).parse({}), - NonFungible: z.object(nonFungibleSchema).parse({}), - }; -} - -export const commonSchema = { +export const stellarCommonSchema = { access: z.literal('ownable').or(z.literal('roles')).optional().describe(stellarCommonDescriptions.access), explicitImplementations: z.boolean().optional().describe(stellarCommonDescriptions.explicitImplementations), upgradeable: z.boolean().optional().describe(stellarCommonDescriptions.upgradeable), @@ -35,18 +21,18 @@ export const commonSchema = { .describe(infoDescriptions.info), } as const satisfies z.ZodRawShape; -export const fungibleSchema = { +export const stellarFungibleSchema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), burnable: z.boolean().optional().describe(commonDescriptions.burnable), pausable: z.boolean().optional().describe(commonDescriptions.pausable), premint: z.string().optional().describe(stellarFungibleDescriptions.premint), mintable: z.boolean().optional().describe(commonDescriptions.mintable), - ...commonSchema, + ...stellarCommonSchema, } as const satisfies z.ZodRawShape; -export const stablecoinSchema = { - ...fungibleSchema, +export const stellarStablecoinSchema = { + ...stellarFungibleSchema, limitations: z .literal(false) .or(z.literal('allowlist')) @@ -55,7 +41,7 @@ export const stablecoinSchema = { .describe(stellarStablecoinDescriptions.limitations), } as const satisfies z.ZodRawShape; -export const nonFungibleSchema = { +export const stellarNonFungibleSchema = { name: z.string().describe(commonDescriptions.name), symbol: z.string().describe(commonDescriptions.symbol), tokenUri: z.string().optional().describe(stellarNonFungibleDescriptions.tokenUri), @@ -65,5 +51,5 @@ export const nonFungibleSchema = { pausable: z.boolean().optional().describe(commonDescriptions.pausable), mintable: z.boolean().optional().describe(commonDescriptions.mintable), sequential: z.boolean().optional().describe(stellarNonFungibleDescriptions.sequential), - ...commonSchema, + ...stellarCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/mcp/src/stylus/schemas.ts b/packages/common/src/ai/schemas/stylus.ts similarity index 65% rename from packages/mcp/src/stylus/schemas.ts rename to packages/common/src/ai/schemas/stylus.ts index 2dade99fa..a921ae45e 100644 --- a/packages/mcp/src/stylus/schemas.ts +++ b/packages/common/src/ai/schemas/stylus.ts @@ -5,23 +5,9 @@ import { stylusERC20Descriptions, stylusERC721Descriptions, stylusERC1155Descriptions, -} from '@openzeppelin/wizard-common'; -import type { KindedOptions } from '@openzeppelin/wizard-stylus'; +} from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - ERC20: z.object(erc20Schema).parse({}), - ERC721: z.object(erc721Schema).parse({}), - ERC1155: z.object(erc1155Schema).parse({}), - }; -} - -export const commonSchema = { +export const stylusCommonSchema = { info: z .object({ securityContact: z.string().optional().describe(infoDescriptions.securityContact), @@ -31,24 +17,24 @@ export const commonSchema = { .describe(infoDescriptions.info), } as const satisfies z.ZodRawShape; -export const erc20Schema = { +export const stylusERC20Schema = { name: z.string().describe(commonDescriptions.name), burnable: z.boolean().optional().describe(commonDescriptions.burnable), permit: z.boolean().optional().describe(stylusERC20Descriptions.permit), flashmint: z.boolean().optional().describe(stylusERC20Descriptions.flashmint), - ...commonSchema, + ...stylusCommonSchema, } as const satisfies z.ZodRawShape; -export const erc721Schema = { +export const stylusERC721Schema = { name: z.string().describe(commonDescriptions.name), burnable: z.boolean().optional().describe(commonDescriptions.burnable), enumerable: z.boolean().optional().describe(stylusERC721Descriptions.enumerable), - ...commonSchema, + ...stylusCommonSchema, } as const satisfies z.ZodRawShape; -export const erc1155Schema = { +export const stylusERC1155Schema = { name: z.string().describe(commonDescriptions.name), burnable: z.boolean().optional().describe(commonDescriptions.burnable), supply: z.boolean().optional().describe(stylusERC1155Descriptions.supply), - ...commonSchema, + ...stylusCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/mcp/src/uniswap-hooks/schemas.ts b/packages/common/src/ai/schemas/uniswap-hooks.ts similarity index 75% rename from packages/mcp/src/uniswap-hooks/schemas.ts rename to packages/common/src/ai/schemas/uniswap-hooks.ts index 644eee1e9..2c8535c45 100644 --- a/packages/mcp/src/uniswap-hooks/schemas.ts +++ b/packages/common/src/ai/schemas/uniswap-hooks.ts @@ -7,25 +7,9 @@ import { uniswapHooksPermissionDescriptions, uniswapHooksInputsDescriptions, solidityCommonDescriptions, -} from '@openzeppelin/wizard-common'; -import { HooksNames, ShareOptions, type KindedOptions } from '@openzeppelin/wizard-uniswap-hooks'; +} from '../../index'; -/** - * Static type assertions to ensure schemas satisfy the Wizard API types. Not called at runtime. - */ -function _typeAssertions() { - const _assertions: { - [K in keyof KindedOptions]: Omit; - } = { - Hooks: z.object(hooksSchema).parse({}), - }; -} - -const hookEnum = z.enum(HooksNames); - -const sharesOptionsSchema = z.union([z.literal(false), z.enum(ShareOptions)]); - -export const commonSchema = { +export const uniswapHooksCommonSchema = { access: z .literal('ownable') .or(z.literal('roles')) @@ -41,16 +25,18 @@ export const commonSchema = { .describe(infoDescriptions.info), } as const satisfies z.ZodRawShape; -const sharesSchema = z +export const uniswapHooksSharesSchema = z .object({ - options: sharesOptionsSchema.describe(uniswapHooksSharesDescriptions.options), + options: z + .union([z.literal(false), z.literal('ERC20'), z.literal('ERC1155'), z.literal('ERC6909')]) + .describe(uniswapHooksSharesDescriptions.options), name: z.string().optional().describe(uniswapHooksSharesDescriptions.name), symbol: z.string().optional().describe(uniswapHooksSharesDescriptions.symbol), uri: z.string().optional().describe(uniswapHooksSharesDescriptions.uri), }) .describe(uniswapHooksDescriptions.shares); -const permissionsSchema = z +export const uniswapHooksPermissionsSchema = z .object({ beforeInitialize: z.boolean().describe(uniswapHooksPermissionDescriptions.beforeInitialize), afterInitialize: z.boolean().describe(uniswapHooksPermissionDescriptions.afterInitialize), @@ -71,22 +57,39 @@ const permissionsSchema = z }) .describe(uniswapHooksDescriptions.permissions); -const inputsSchema = z +export const uniswapHooksInputsSchema = z .object({ blockNumberOffset: z.number().describe(uniswapHooksInputsDescriptions.blockNumberOffset), maxAbsTickDelta: z.number().describe(uniswapHooksInputsDescriptions.maxAbsTickDelta), }) .describe(uniswapHooksDescriptions.inputs); -export const hooksSchema = { - hook: hookEnum.describe(uniswapHooksDescriptions.hook), +export const uniswapHooksHooksSchema = { + hook: z + .enum([ + 'BaseHook', + 'BaseAsyncSwap', + 'BaseCustomAccounting', + 'BaseCustomCurve', + 'BaseDynamicFee', + 'BaseOverrideFee', + 'BaseDynamicAfterFee', + 'BaseHookFee', + 'AntiSandwichHook', + 'LiquidityPenaltyHook', + 'LimitOrderHook', + 'ReHypothecationHook', + 'BaseOracleHook', + 'OracleHookWithV3Adapters', + ]) + .describe(uniswapHooksDescriptions.hook), name: z.string().describe(commonDescriptions.name), pausable: z.boolean().describe(commonDescriptions.pausable), currencySettler: z.boolean().describe(uniswapHooksDescriptions.currencySettler), safeCast: z.boolean().describe(uniswapHooksDescriptions.safeCast), transientStorage: z.boolean().describe(uniswapHooksDescriptions.transientStorage), - shares: sharesSchema, - permissions: permissionsSchema, - inputs: inputsSchema, - ...commonSchema, + shares: uniswapHooksSharesSchema, + permissions: uniswapHooksPermissionsSchema, + inputs: uniswapHooksInputsSchema, + ...uniswapHooksCommonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index f6a21a39d..88cb0ef5e 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -12,5 +12,8 @@ }, "include": [ "src/**/*" + ], + "exclude": [ + "src/**/*.test.ts" ] } diff --git a/packages/mcp/src/cairo/tools/account.test.ts b/packages/mcp/src/cairo/tools/account.test.ts index 89823c23d..f09841592 100644 --- a/packages/mcp/src/cairo/tools/account.test.ts +++ b/packages/mcp/src/cairo/tools/account.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { AccountOptions } from '@openzeppelin/wizard-cairo'; import { account } from '@openzeppelin/wizard-cairo'; -import { accountSchema } from '../schemas'; +import { cairoAccountSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoAccount(new McpServer(testMcpInfo)); - t.context.schema = z.object(accountSchema); + t.context.schema = z.object(cairoAccountSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/account.ts b/packages/mcp/src/cairo/tools/account.ts index 67fb44dc8..8ded1549a 100644 --- a/packages/mcp/src/cairo/tools/account.ts +++ b/packages/mcp/src/cairo/tools/account.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { AccountOptions } from '@openzeppelin/wizard-cairo'; import { account } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { accountSchema } from '../schemas'; +import { cairoAccountSchema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoAccount(server: McpServer): RegisteredTool { return server.tool( 'cairo-account', makeDetailedPrompt(cairoPrompts.Account), - accountSchema, + cairoAccountSchema, async ({ name, type, declare, deploy, pubkey, outsideExecution, upgradeable, info, macros }) => { const opts: AccountOptions = { name, diff --git a/packages/mcp/src/cairo/tools/custom.test.ts b/packages/mcp/src/cairo/tools/custom.test.ts index 185d06329..9e855471d 100644 --- a/packages/mcp/src/cairo/tools/custom.test.ts +++ b/packages/mcp/src/cairo/tools/custom.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { CustomOptions } from '@openzeppelin/wizard-cairo'; import { custom } from '@openzeppelin/wizard-cairo'; -import { customSchema } from '../schemas'; +import { cairoCustomSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoCustom(new McpServer(testMcpInfo)); - t.context.schema = z.object(customSchema); + t.context.schema = z.object(cairoCustomSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/custom.ts b/packages/mcp/src/cairo/tools/custom.ts index 80aa48486..bdbb63313 100644 --- a/packages/mcp/src/cairo/tools/custom.ts +++ b/packages/mcp/src/cairo/tools/custom.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { CustomOptions } from '@openzeppelin/wizard-cairo'; import { custom } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { customSchema } from '../schemas'; +import { cairoCustomSchema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoCustom(server: McpServer): RegisteredTool { return server.tool( 'cairo-custom', makeDetailedPrompt(cairoPrompts.Custom), - customSchema, + cairoCustomSchema, async ({ name, pausable, access, upgradeable, info, macros }) => { const opts: CustomOptions = { name, diff --git a/packages/mcp/src/cairo/tools/erc1155.test.ts b/packages/mcp/src/cairo/tools/erc1155.test.ts index 102397bf0..94b7d8df5 100644 --- a/packages/mcp/src/cairo/tools/erc1155.test.ts +++ b/packages/mcp/src/cairo/tools/erc1155.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC1155Options } from '@openzeppelin/wizard-cairo'; import { erc1155 } from '@openzeppelin/wizard-cairo'; -import { erc1155Schema } from '../schemas'; +import { cairoERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoERC1155(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc1155Schema); + t.context.schema = z.object(cairoERC1155Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/erc1155.ts b/packages/mcp/src/cairo/tools/erc1155.ts index 2eb93ee94..cab000b34 100644 --- a/packages/mcp/src/cairo/tools/erc1155.ts +++ b/packages/mcp/src/cairo/tools/erc1155.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC1155Options } from '@openzeppelin/wizard-cairo'; import { erc1155 } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc1155Schema } from '../schemas'; +import { cairoERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoERC1155(server: McpServer): RegisteredTool { return server.tool( 'cairo-erc1155', makeDetailedPrompt(cairoPrompts.ERC1155), - erc1155Schema, + cairoERC1155Schema, async ({ name, baseUri, diff --git a/packages/mcp/src/cairo/tools/erc20.test.ts b/packages/mcp/src/cairo/tools/erc20.test.ts index fa30f5d7b..005ad54d4 100644 --- a/packages/mcp/src/cairo/tools/erc20.test.ts +++ b/packages/mcp/src/cairo/tools/erc20.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC20Options } from '@openzeppelin/wizard-cairo'; import { erc20 } from '@openzeppelin/wizard-cairo'; -import { erc20Schema } from '../schemas'; +import { cairoERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoERC20(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc20Schema); + t.context.schema = z.object(cairoERC20Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/erc20.ts b/packages/mcp/src/cairo/tools/erc20.ts index fa16834e2..44ca5ced9 100644 --- a/packages/mcp/src/cairo/tools/erc20.ts +++ b/packages/mcp/src/cairo/tools/erc20.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC20Options } from '@openzeppelin/wizard-cairo'; import { erc20 } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc20Schema } from '../schemas'; +import { cairoERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoERC20(server: McpServer): RegisteredTool { return server.tool( 'cairo-erc20', makeDetailedPrompt(cairoPrompts.ERC20), - erc20Schema, + cairoERC20Schema, async ({ name, symbol, diff --git a/packages/mcp/src/cairo/tools/erc721.test.ts b/packages/mcp/src/cairo/tools/erc721.test.ts index e34a1c7ed..1dacd56ce 100644 --- a/packages/mcp/src/cairo/tools/erc721.test.ts +++ b/packages/mcp/src/cairo/tools/erc721.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC721Options } from '@openzeppelin/wizard-cairo'; import { erc721 } from '@openzeppelin/wizard-cairo'; -import { erc721Schema } from '../schemas'; +import { cairoERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoERC721(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc721Schema); + t.context.schema = z.object(cairoERC721Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/erc721.ts b/packages/mcp/src/cairo/tools/erc721.ts index bce92ccbf..9a1af8edc 100644 --- a/packages/mcp/src/cairo/tools/erc721.ts +++ b/packages/mcp/src/cairo/tools/erc721.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC721Options } from '@openzeppelin/wizard-cairo'; import { erc721 } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc721Schema } from '../schemas'; +import { cairoERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoERC721(server: McpServer): RegisteredTool { return server.tool( 'cairo-erc721', makeDetailedPrompt(cairoPrompts.ERC721), - erc721Schema, + cairoERC721Schema, async ({ name, symbol, diff --git a/packages/mcp/src/cairo/tools/governor.test.ts b/packages/mcp/src/cairo/tools/governor.test.ts index 95790f245..916208545 100644 --- a/packages/mcp/src/cairo/tools/governor.test.ts +++ b/packages/mcp/src/cairo/tools/governor.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { GovernorOptions } from '@openzeppelin/wizard-cairo'; import { governor } from '@openzeppelin/wizard-cairo'; -import { governorSchema } from '../schemas'; +import { cairoGovernorSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoGovernor(new McpServer(testMcpInfo)); - t.context.schema = z.object(governorSchema); + t.context.schema = z.object(cairoGovernorSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/governor.ts b/packages/mcp/src/cairo/tools/governor.ts index dc7265136..edd7cb49e 100644 --- a/packages/mcp/src/cairo/tools/governor.ts +++ b/packages/mcp/src/cairo/tools/governor.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { GovernorOptions } from '@openzeppelin/wizard-cairo'; import { governor } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { governorSchema } from '../schemas'; +import { cairoGovernorSchema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoGovernor(server: McpServer): RegisteredTool { return server.tool( 'cairo-governor', makeDetailedPrompt(cairoPrompts.Governor), - governorSchema, + cairoGovernorSchema, async ({ name, delay, diff --git a/packages/mcp/src/cairo/tools/multisig.test.ts b/packages/mcp/src/cairo/tools/multisig.test.ts index 967b4bb91..a39d482a2 100644 --- a/packages/mcp/src/cairo/tools/multisig.test.ts +++ b/packages/mcp/src/cairo/tools/multisig.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { MultisigOptions } from '@openzeppelin/wizard-cairo'; import { multisig } from '@openzeppelin/wizard-cairo'; -import { multisigSchema } from '../schemas'; +import { cairoMultisigSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoMultisig(new McpServer(testMcpInfo)); - t.context.schema = z.object(multisigSchema); + t.context.schema = z.object(cairoMultisigSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/multisig.ts b/packages/mcp/src/cairo/tools/multisig.ts index 26cdc36ec..875b6d8e1 100644 --- a/packages/mcp/src/cairo/tools/multisig.ts +++ b/packages/mcp/src/cairo/tools/multisig.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { MultisigOptions } from '@openzeppelin/wizard-cairo'; import { multisig } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { multisigSchema } from '../schemas'; +import { cairoMultisigSchema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoMultisig(server: McpServer): RegisteredTool { return server.tool( 'cairo-multisig', makeDetailedPrompt(cairoPrompts.Multisig), - multisigSchema, + cairoMultisigSchema, async ({ name, quorum, upgradeable, info, macros }) => { const opts: MultisigOptions = { name, diff --git a/packages/mcp/src/cairo/tools/vesting.test.ts b/packages/mcp/src/cairo/tools/vesting.test.ts index 643014807..aa1bad8ec 100644 --- a/packages/mcp/src/cairo/tools/vesting.test.ts +++ b/packages/mcp/src/cairo/tools/vesting.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { VestingOptions } from '@openzeppelin/wizard-cairo'; import { vesting } from '@openzeppelin/wizard-cairo'; -import { vestingSchema } from '../schemas'; +import { cairoVestingSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerCairoVesting(new McpServer(testMcpInfo)); - t.context.schema = z.object(vestingSchema); + t.context.schema = z.object(cairoVestingSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/cairo/tools/vesting.ts b/packages/mcp/src/cairo/tools/vesting.ts index fee678153..8be80c8f4 100644 --- a/packages/mcp/src/cairo/tools/vesting.ts +++ b/packages/mcp/src/cairo/tools/vesting.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { VestingOptions } from '@openzeppelin/wizard-cairo'; import { vesting } from '@openzeppelin/wizard-cairo'; import { safePrintCairoCodeBlock, makeDetailedPrompt } from '../../utils'; -import { vestingSchema } from '../schemas'; +import { cairoVestingSchema } from '@openzeppelin/wizard-common/schemas'; import { cairoPrompts } from '@openzeppelin/wizard-common'; export function registerCairoVesting(server: McpServer): RegisteredTool { return server.tool( 'cairo-vesting', makeDetailedPrompt(cairoPrompts.Vesting), - vestingSchema, + cairoVestingSchema, async ({ name, startDate, duration, cliffDuration, schedule, info, macros }) => { const opts: VestingOptions = { name, diff --git a/packages/mcp/src/confidential/tools/erc7984.test.ts b/packages/mcp/src/confidential/tools/erc7984.test.ts index 57f803a20..2866a20d2 100644 --- a/packages/mcp/src/confidential/tools/erc7984.test.ts +++ b/packages/mcp/src/confidential/tools/erc7984.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC7984Options } from '@openzeppelin/wizard-confidential'; import { erc7984 } from '@openzeppelin/wizard-confidential'; -import { erc7984Schema } from '../schemas'; +import { confidentialERC7984Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerConfidentialERC7984(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc7984Schema); + t.context.schema = z.object(confidentialERC7984Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/confidential/tools/erc7984.ts b/packages/mcp/src/confidential/tools/erc7984.ts index f2efd5245..baa3be2e0 100644 --- a/packages/mcp/src/confidential/tools/erc7984.ts +++ b/packages/mcp/src/confidential/tools/erc7984.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC7984Options } from '@openzeppelin/wizard-confidential'; import { erc7984 } from '@openzeppelin/wizard-confidential'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc7984Schema } from '../schemas'; +import { confidentialERC7984Schema } from '@openzeppelin/wizard-common/schemas'; import { confidentialPrompts } from '@openzeppelin/wizard-common'; export function registerConfidentialERC7984(server: McpServer): RegisteredTool { return server.tool( 'erc7984', makeDetailedPrompt(confidentialPrompts.ERC7984), - erc7984Schema, + confidentialERC7984Schema, async ({ name, symbol, contractURI, premint, networkConfig, wrappable, votes, info }) => { const opts: ERC7984Options = { name, diff --git a/packages/mcp/src/solidity/tools/account.test.ts b/packages/mcp/src/solidity/tools/account.test.ts index 1bb676e5c..677b903ee 100644 --- a/packages/mcp/src/solidity/tools/account.test.ts +++ b/packages/mcp/src/solidity/tools/account.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { AccountOptions } from '@openzeppelin/wizard'; import { account } from '@openzeppelin/wizard'; -import { accountSchema } from '../schemas'; +import { solidityAccountSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityAccount(new McpServer(testMcpInfo)); - t.context.schema = z.object(accountSchema); + t.context.schema = z.object(solidityAccountSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/account.ts b/packages/mcp/src/solidity/tools/account.ts index 2fbfd68a9..8f854c717 100644 --- a/packages/mcp/src/solidity/tools/account.ts +++ b/packages/mcp/src/solidity/tools/account.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { AccountOptions } from '@openzeppelin/wizard'; import { account } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { accountSchema } from '../schemas'; +import { solidityAccountSchema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityAccount(server: McpServer): RegisteredTool { return server.tool( 'solidity-account', makeDetailedPrompt(solidityPrompts.Account), - accountSchema, + solidityAccountSchema, async ({ name, signatureValidation, diff --git a/packages/mcp/src/solidity/tools/custom.test.ts b/packages/mcp/src/solidity/tools/custom.test.ts index d2e2baf6e..73cb3b7ca 100644 --- a/packages/mcp/src/solidity/tools/custom.test.ts +++ b/packages/mcp/src/solidity/tools/custom.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { CustomOptions } from '@openzeppelin/wizard'; import { custom } from '@openzeppelin/wizard'; -import { customSchema } from '../schemas'; +import { solidityCustomSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityCustom(new McpServer(testMcpInfo)); - t.context.schema = z.object(customSchema); + t.context.schema = z.object(solidityCustomSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/custom.ts b/packages/mcp/src/solidity/tools/custom.ts index cd9bba4f2..6dc3cdd96 100644 --- a/packages/mcp/src/solidity/tools/custom.ts +++ b/packages/mcp/src/solidity/tools/custom.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { CustomOptions } from '@openzeppelin/wizard'; import { custom } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { customSchema } from '../schemas'; +import { solidityCustomSchema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityCustom(server: McpServer): RegisteredTool { return server.tool( 'solidity-custom', makeDetailedPrompt(solidityPrompts.Custom), - customSchema, + solidityCustomSchema, async ({ name, pausable, access, upgradeable, info }) => { const opts: CustomOptions = { name, diff --git a/packages/mcp/src/solidity/tools/erc1155.test.ts b/packages/mcp/src/solidity/tools/erc1155.test.ts index a729fdeac..33eb1d273 100644 --- a/packages/mcp/src/solidity/tools/erc1155.test.ts +++ b/packages/mcp/src/solidity/tools/erc1155.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC1155Options } from '@openzeppelin/wizard'; import { erc1155 } from '@openzeppelin/wizard'; -import { erc1155Schema } from '../schemas'; +import { solidityERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityERC1155(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc1155Schema); + t.context.schema = z.object(solidityERC1155Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/erc1155.ts b/packages/mcp/src/solidity/tools/erc1155.ts index cbd633da5..243ea7c0a 100644 --- a/packages/mcp/src/solidity/tools/erc1155.ts +++ b/packages/mcp/src/solidity/tools/erc1155.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC1155Options } from '@openzeppelin/wizard'; import { erc1155 } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc1155Schema } from '../schemas'; +import { solidityERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityERC1155(server: McpServer): RegisteredTool { return server.tool( 'solidity-erc1155', makeDetailedPrompt(solidityPrompts.ERC1155), - erc1155Schema, + solidityERC1155Schema, async ({ name, uri, burnable, pausable, mintable, supply, updatableUri, access, upgradeable, info }) => { const opts: ERC1155Options = { name, diff --git a/packages/mcp/src/solidity/tools/erc20.test.ts b/packages/mcp/src/solidity/tools/erc20.test.ts index feb3bbb90..1c1451369 100644 --- a/packages/mcp/src/solidity/tools/erc20.test.ts +++ b/packages/mcp/src/solidity/tools/erc20.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC20Options } from '@openzeppelin/wizard'; import { erc20 } from '@openzeppelin/wizard'; -import { erc20Schema } from '../schemas'; +import { solidityERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityERC20(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc20Schema); + t.context.schema = z.object(solidityERC20Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/erc20.ts b/packages/mcp/src/solidity/tools/erc20.ts index 0fbdeaa20..bb8700473 100644 --- a/packages/mcp/src/solidity/tools/erc20.ts +++ b/packages/mcp/src/solidity/tools/erc20.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC20Options } from '@openzeppelin/wizard'; import { erc20 } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc20Schema } from '../schemas'; +import { solidityERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityERC20(server: McpServer): RegisteredTool { return server.tool( 'solidity-erc20', makeDetailedPrompt(solidityPrompts.ERC20), - erc20Schema, + solidityERC20Schema, async ({ name, symbol, diff --git a/packages/mcp/src/solidity/tools/erc721.test.ts b/packages/mcp/src/solidity/tools/erc721.test.ts index 32f1d1eac..dcec44039 100644 --- a/packages/mcp/src/solidity/tools/erc721.test.ts +++ b/packages/mcp/src/solidity/tools/erc721.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC721Options } from '@openzeppelin/wizard'; import { erc721 } from '@openzeppelin/wizard'; -import { erc721Schema } from '../schemas'; +import { solidityERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityERC721(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc721Schema); + t.context.schema = z.object(solidityERC721Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/erc721.ts b/packages/mcp/src/solidity/tools/erc721.ts index e4449bb93..97cea93ba 100644 --- a/packages/mcp/src/solidity/tools/erc721.ts +++ b/packages/mcp/src/solidity/tools/erc721.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC721Options } from '@openzeppelin/wizard'; import { erc721 } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc721Schema } from '../schemas'; +import { solidityERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityERC721(server: McpServer): RegisteredTool { return server.tool( 'solidity-erc721', makeDetailedPrompt(solidityPrompts.ERC721), - erc721Schema, + solidityERC721Schema, async ({ name, symbol, diff --git a/packages/mcp/src/solidity/tools/governor.test.ts b/packages/mcp/src/solidity/tools/governor.test.ts index f9120cb5c..7d5ace969 100644 --- a/packages/mcp/src/solidity/tools/governor.test.ts +++ b/packages/mcp/src/solidity/tools/governor.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { GovernorOptions } from '@openzeppelin/wizard'; import { governor } from '@openzeppelin/wizard'; -import { governorSchema } from '../schemas'; +import { solidityGovernorSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityGovernor(new McpServer(testMcpInfo)); - t.context.schema = z.object(governorSchema); + t.context.schema = z.object(solidityGovernorSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/governor.ts b/packages/mcp/src/solidity/tools/governor.ts index 4de70478c..e6250bff1 100644 --- a/packages/mcp/src/solidity/tools/governor.ts +++ b/packages/mcp/src/solidity/tools/governor.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { GovernorOptions } from '@openzeppelin/wizard'; import { governor } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { governorSchema } from '../schemas'; +import { solidityGovernorSchema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityGovernor(server: McpServer): RegisteredTool { return server.tool( 'solidity-governor', makeDetailedPrompt(solidityPrompts.Governor), - governorSchema, + solidityGovernorSchema, async ({ name, delay, diff --git a/packages/mcp/src/solidity/tools/rwa.test.ts b/packages/mcp/src/solidity/tools/rwa.test.ts index 9e65474cd..977ad4c44 100644 --- a/packages/mcp/src/solidity/tools/rwa.test.ts +++ b/packages/mcp/src/solidity/tools/rwa.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { StablecoinOptions } from '@openzeppelin/wizard'; import { realWorldAsset } from '@openzeppelin/wizard'; -import { rwaSchema } from '../schemas'; +import { solidityRWASchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityRWA(new McpServer(testMcpInfo)); - t.context.schema = z.object(rwaSchema); + t.context.schema = z.object(solidityRWASchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/rwa.ts b/packages/mcp/src/solidity/tools/rwa.ts index 32e47ae6f..d72af300f 100644 --- a/packages/mcp/src/solidity/tools/rwa.ts +++ b/packages/mcp/src/solidity/tools/rwa.ts @@ -2,14 +2,14 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { StablecoinOptions } from '@openzeppelin/wizard'; import { realWorldAsset } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { rwaSchema } from '../schemas'; +import { solidityRWASchema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityRWA(server: McpServer) { return server.tool( 'solidity-rwa', makeDetailedPrompt(solidityPrompts.RWA), - rwaSchema, + solidityRWASchema, async ({ name, symbol, diff --git a/packages/mcp/src/solidity/tools/stablecoin.test.ts b/packages/mcp/src/solidity/tools/stablecoin.test.ts index 87a5af157..b1b82bde6 100644 --- a/packages/mcp/src/solidity/tools/stablecoin.test.ts +++ b/packages/mcp/src/solidity/tools/stablecoin.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { StablecoinOptions } from '@openzeppelin/wizard'; import { stablecoin } from '@openzeppelin/wizard'; -import { stablecoinSchema } from '../schemas'; +import { solidityStablecoinSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerSolidityStablecoin(new McpServer(testMcpInfo)); - t.context.schema = z.object(stablecoinSchema); + t.context.schema = z.object(solidityStablecoinSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/solidity/tools/stablecoin.ts b/packages/mcp/src/solidity/tools/stablecoin.ts index 3ff16bc76..655aafe78 100644 --- a/packages/mcp/src/solidity/tools/stablecoin.ts +++ b/packages/mcp/src/solidity/tools/stablecoin.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { StablecoinOptions } from '@openzeppelin/wizard'; import { stablecoin } from '@openzeppelin/wizard'; import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; -import { stablecoinSchema } from '../schemas'; +import { solidityStablecoinSchema } from '@openzeppelin/wizard-common/schemas'; import { solidityPrompts } from '@openzeppelin/wizard-common'; export function registerSolidityStablecoin(server: McpServer): RegisteredTool { return server.tool( 'solidity-stablecoin', makeDetailedPrompt(solidityPrompts.Stablecoin), - stablecoinSchema, + solidityStablecoinSchema, async ({ name, symbol, diff --git a/packages/mcp/src/stellar/tools/fungible.test.ts b/packages/mcp/src/stellar/tools/fungible.test.ts index a58252a5a..b471256c3 100644 --- a/packages/mcp/src/stellar/tools/fungible.test.ts +++ b/packages/mcp/src/stellar/tools/fungible.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { FungibleOptions } from '@openzeppelin/wizard-stellar'; import { fungible } from '@openzeppelin/wizard-stellar'; -import { fungibleSchema } from '../schemas'; +import { stellarFungibleSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStellarFungible(new McpServer(testMcpInfo)); - t.context.schema = z.object(fungibleSchema); + t.context.schema = z.object(stellarFungibleSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stellar/tools/fungible.ts b/packages/mcp/src/stellar/tools/fungible.ts index afacdf9e8..20e936939 100644 --- a/packages/mcp/src/stellar/tools/fungible.ts +++ b/packages/mcp/src/stellar/tools/fungible.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { FungibleOptions } from '@openzeppelin/wizard-stellar'; import { fungible } from '@openzeppelin/wizard-stellar'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { fungibleSchema } from '../schemas'; +import { stellarFungibleSchema } from '@openzeppelin/wizard-common/schemas'; import { stellarPrompts } from '@openzeppelin/wizard-common'; export function registerStellarFungible(server: McpServer): RegisteredTool { return server.tool( 'stellar-fungible', makeDetailedPrompt(stellarPrompts.Fungible), - fungibleSchema, + stellarFungibleSchema, async ({ name, symbol, diff --git a/packages/mcp/src/stellar/tools/non-fungible.test.ts b/packages/mcp/src/stellar/tools/non-fungible.test.ts index fa575b1a8..7108cb786 100644 --- a/packages/mcp/src/stellar/tools/non-fungible.test.ts +++ b/packages/mcp/src/stellar/tools/non-fungible.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { NonFungibleOptions } from '@openzeppelin/wizard-stellar'; import { nonFungible } from '@openzeppelin/wizard-stellar'; -import { nonFungibleSchema } from '../schemas'; +import { stellarNonFungibleSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStellarNonFungible(new McpServer(testMcpInfo)); - t.context.schema = z.object(nonFungibleSchema); + t.context.schema = z.object(stellarNonFungibleSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stellar/tools/non-fungible.ts b/packages/mcp/src/stellar/tools/non-fungible.ts index 0b23d61a4..7ffac0bc6 100644 --- a/packages/mcp/src/stellar/tools/non-fungible.ts +++ b/packages/mcp/src/stellar/tools/non-fungible.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { NonFungibleOptions } from '@openzeppelin/wizard-stellar'; import { nonFungible } from '@openzeppelin/wizard-stellar'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { nonFungibleSchema } from '../schemas'; +import { stellarNonFungibleSchema } from '@openzeppelin/wizard-common/schemas'; import { stellarPrompts } from '@openzeppelin/wizard-common'; export function registerStellarNonFungible(server: McpServer): RegisteredTool { return server.tool( 'stellar-non-fungible', makeDetailedPrompt(stellarPrompts.NonFungible), - nonFungibleSchema, + stellarNonFungibleSchema, async ({ name, symbol, diff --git a/packages/mcp/src/stellar/tools/stablecoin.test.ts b/packages/mcp/src/stellar/tools/stablecoin.test.ts index d4e81be06..dcb2a96d6 100644 --- a/packages/mcp/src/stellar/tools/stablecoin.test.ts +++ b/packages/mcp/src/stellar/tools/stablecoin.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { StablecoinOptions } from '@openzeppelin/wizard-stellar'; import { stablecoin } from '@openzeppelin/wizard-stellar'; -import { stablecoinSchema } from '../schemas'; +import { stellarStablecoinSchema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStellarStablecoin(new McpServer(testMcpInfo)); - t.context.schema = z.object(stablecoinSchema); + t.context.schema = z.object(stellarStablecoinSchema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stellar/tools/stablecoin.ts b/packages/mcp/src/stellar/tools/stablecoin.ts index f5ef4a741..cebe5eebf 100644 --- a/packages/mcp/src/stellar/tools/stablecoin.ts +++ b/packages/mcp/src/stellar/tools/stablecoin.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { StablecoinOptions } from '@openzeppelin/wizard-stellar'; import { stablecoin } from '@openzeppelin/wizard-stellar'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { stablecoinSchema } from '../schemas'; +import { stellarStablecoinSchema } from '@openzeppelin/wizard-common/schemas'; import { stellarPrompts } from '@openzeppelin/wizard-common'; export function registerStellarStablecoin(server: McpServer): RegisteredTool { return server.tool( 'stellar-stablecoin', makeDetailedPrompt(stellarPrompts.Stablecoin), - stablecoinSchema, + stellarStablecoinSchema, async ({ name, symbol, diff --git a/packages/mcp/src/stylus/tools/erc1155.test.ts b/packages/mcp/src/stylus/tools/erc1155.test.ts index 658a0020b..aa9f1a00a 100644 --- a/packages/mcp/src/stylus/tools/erc1155.test.ts +++ b/packages/mcp/src/stylus/tools/erc1155.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC1155Options } from '@openzeppelin/wizard-stylus'; import { erc1155 } from '@openzeppelin/wizard-stylus'; -import { erc1155Schema } from '../schemas'; +import { stylusERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStylusERC1155(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc1155Schema); + t.context.schema = z.object(stylusERC1155Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stylus/tools/erc1155.ts b/packages/mcp/src/stylus/tools/erc1155.ts index 8c9ba3111..56cda697d 100644 --- a/packages/mcp/src/stylus/tools/erc1155.ts +++ b/packages/mcp/src/stylus/tools/erc1155.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC1155Options } from '@openzeppelin/wizard-stylus'; import { erc1155 } from '@openzeppelin/wizard-stylus'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc1155Schema } from '../schemas'; +import { stylusERC1155Schema } from '@openzeppelin/wizard-common/schemas'; import { stylusPrompts } from '@openzeppelin/wizard-common'; export function registerStylusERC1155(server: McpServer): RegisteredTool { return server.tool( 'stylus-erc1155', makeDetailedPrompt(stylusPrompts.ERC1155), - erc1155Schema, + stylusERC1155Schema, async ({ name, burnable, supply, info }) => { const opts: ERC1155Options = { name, diff --git a/packages/mcp/src/stylus/tools/erc20.test.ts b/packages/mcp/src/stylus/tools/erc20.test.ts index fc5f437be..68f181d6c 100644 --- a/packages/mcp/src/stylus/tools/erc20.test.ts +++ b/packages/mcp/src/stylus/tools/erc20.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC20Options } from '@openzeppelin/wizard-stylus'; import { erc20 } from '@openzeppelin/wizard-stylus'; -import { erc20Schema } from '../schemas'; +import { stylusERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStylusERC20(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc20Schema); + t.context.schema = z.object(stylusERC20Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stylus/tools/erc20.ts b/packages/mcp/src/stylus/tools/erc20.ts index 21edab047..05dbff5d5 100644 --- a/packages/mcp/src/stylus/tools/erc20.ts +++ b/packages/mcp/src/stylus/tools/erc20.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC20Options } from '@openzeppelin/wizard-stylus'; import { erc20 } from '@openzeppelin/wizard-stylus'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc20Schema } from '../schemas'; +import { stylusERC20Schema } from '@openzeppelin/wizard-common/schemas'; import { stylusPrompts } from '@openzeppelin/wizard-common'; export function registerStylusERC20(server: McpServer): RegisteredTool { return server.tool( 'stylus-erc20', makeDetailedPrompt(stylusPrompts.ERC20), - erc20Schema, + stylusERC20Schema, async ({ name, burnable, permit, flashmint, info }) => { const opts: ERC20Options = { name, diff --git a/packages/mcp/src/stylus/tools/erc721.test.ts b/packages/mcp/src/stylus/tools/erc721.test.ts index f429c2804..eb88040ad 100644 --- a/packages/mcp/src/stylus/tools/erc721.test.ts +++ b/packages/mcp/src/stylus/tools/erc721.test.ts @@ -7,19 +7,19 @@ import type { DeepRequired } from '../../helpers.test'; import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; import type { ERC721Options } from '@openzeppelin/wizard-stylus'; import { erc721 } from '@openzeppelin/wizard-stylus'; -import { erc721Schema } from '../schemas'; +import { stylusERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { z } from 'zod'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerStylusERC721(new McpServer(testMcpInfo)); - t.context.schema = z.object(erc721Schema); + t.context.schema = z.object(stylusERC721Schema); }); function assertHasAllSupportedFields( diff --git a/packages/mcp/src/stylus/tools/erc721.ts b/packages/mcp/src/stylus/tools/erc721.ts index 9cc7b62a6..4eeb8d1b5 100644 --- a/packages/mcp/src/stylus/tools/erc721.ts +++ b/packages/mcp/src/stylus/tools/erc721.ts @@ -2,14 +2,14 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server import type { ERC721Options } from '@openzeppelin/wizard-stylus'; import { erc721 } from '@openzeppelin/wizard-stylus'; import { safePrintRustCodeBlock, makeDetailedPrompt } from '../../utils'; -import { erc721Schema } from '../schemas'; +import { stylusERC721Schema } from '@openzeppelin/wizard-common/schemas'; import { stylusPrompts } from '@openzeppelin/wizard-common'; export function registerStylusERC721(server: McpServer): RegisteredTool { return server.tool( 'stylus-erc721', makeDetailedPrompt(stylusPrompts.ERC721), - erc721Schema, + stylusERC721Schema, async ({ name, burnable, enumerable, info }) => { const opts: ERC721Options = { name, diff --git a/packages/mcp/src/uniswap-hooks/tools/hooks.test.ts b/packages/mcp/src/uniswap-hooks/tools/hooks.test.ts index 4f5afa415..6074c3290 100644 --- a/packages/mcp/src/uniswap-hooks/tools/hooks.test.ts +++ b/packages/mcp/src/uniswap-hooks/tools/hooks.test.ts @@ -7,18 +7,18 @@ import { z } from 'zod'; import { registerUniswapHooks } from './hooks'; import { assertAPIEquivalence, testMcpInfo, type DeepRequired } from '../../helpers.test'; -import { hooksSchema } from '../schemas'; +import { uniswapHooksHooksSchema } from '@openzeppelin/wizard-common/schemas'; interface Context { tool: RegisteredTool; - schema: z.ZodObject; + schema: z.ZodObject; } const test = _test as TestFn; test.before(t => { t.context.tool = registerUniswapHooks(new McpServer(testMcpInfo)); - t.context.schema = z.object(hooksSchema); + t.context.schema = z.object(uniswapHooksHooksSchema); }); type SchemaParams = DeepRequired>; diff --git a/packages/mcp/src/uniswap-hooks/tools/hooks.ts b/packages/mcp/src/uniswap-hooks/tools/hooks.ts index 82b4e2adc..04c957e1d 100644 --- a/packages/mcp/src/uniswap-hooks/tools/hooks.ts +++ b/packages/mcp/src/uniswap-hooks/tools/hooks.ts @@ -1,6 +1,6 @@ import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; import { hooks, type HooksOptions } from '@openzeppelin/wizard-uniswap-hooks'; -import { hooksSchema } from '../schemas'; +import { uniswapHooksHooksSchema } from '@openzeppelin/wizard-common/schemas'; import { makeDetailedPrompt, safePrintSolidityCodeBlock } from '../../utils'; import { uniswapHooksPrompts } from '@openzeppelin/wizard-common'; @@ -8,7 +8,7 @@ export function registerUniswapHooks(server: McpServer): RegisteredTool { return server.tool( 'uniswap-hooks', makeDetailedPrompt(uniswapHooksPrompts.Hooks), - hooksSchema, + uniswapHooksHooksSchema, async ({ hook, name, diff --git a/scripts/check-circular-deps.mjs b/scripts/check-circular-deps.mjs new file mode 100644 index 000000000..a4e697258 --- /dev/null +++ b/scripts/check-circular-deps.mjs @@ -0,0 +1,99 @@ +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, resolve } from 'path'; + +const root = resolve(import.meta.dirname, '..'); + +function getWorkspacePackages() { + const rootPkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); + const patterns = rootPkg.workspaces?.packages ?? rootPkg.workspaces ?? []; + + const packages = new Map(); // name -> Set + + for (const pattern of patterns) { + // Expand simple globs (e.g. "packages/core/*") + const dirs = pattern.includes('*') ? expandGlob(pattern) : [join(root, pattern)]; + + for (const dir of dirs) { + const pkgPath = join(dir, 'package.json'); + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + if (!pkg.name) continue; + packages.set(pkg.name, { + dir, + deps: new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.peerDependencies ?? {})]), + }); + } catch (e) { + if (e.code !== 'ENOENT') throw e; + } + } + } + + // Filter deps to only monorepo packages + for (const [, info] of packages) { + info.deps = new Set([...info.deps].filter(d => packages.has(d))); + } + + return packages; +} + +function expandGlob(pattern) { + const base = join(root, pattern.replace(/\/?\*$/, '')); + try { + return readdirSync(base) + .map(name => join(base, name)) + .filter(p => { + try { + return statSync(p).isDirectory(); + } catch { + return false; + } + }); + } catch { + return []; + } +} + +function findCycles(packages) { + const cycles = []; + const visited = new Set(); + const inStack = new Set(); + + function dfs(name, path) { + if (inStack.has(name)) { + const cycleStart = path.indexOf(name); + cycles.push(path.slice(cycleStart).concat(name)); + return; + } + if (visited.has(name)) return; + + visited.add(name); + inStack.add(name); + path.push(name); + + for (const dep of packages.get(name)?.deps ?? []) { + dfs(dep, path); + } + + path.pop(); + inStack.delete(name); + } + + for (const name of packages.keys()) { + dfs(name, []); + } + + return cycles; +} + +const packages = getWorkspacePackages(); +const cycles = findCycles(packages); + +if (cycles.length > 0) { + console.error('Circular dependencies detected:\n'); + for (const cycle of cycles) { + console.error(' ' + cycle.join(' -> ')); + } + process.exit(1); +} else { + console.log('No circular dependencies found.'); +} diff --git a/yarn.lock b/yarn.lock index d0dcbfc6a..da155accb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6587,7 +6587,7 @@ zod-to-json-schema@^3.25.1: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz#3fa799a7badd554541472fb65843fdc460b2e5aa" integrity sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA== -"zod@^3.25 || ^4.0": +"zod@^3.25 || ^4.0", zod@^4.0: version "4.3.6" resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==