Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/changeset/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ First line is a high-level summary without leading dash (PR number gets appended
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.
8. **UI that ships in MCP Apps**: `ui` is private (versioned in-repo, not published to npm). MCP App HTML is built from `ui` when `@openzeppelin/contracts-mcp` is published. Changes to Wizard controls or other UI that ships in MCP Apps need **one changeset listing both** `ui` and `@openzeppelin/contracts-mcp`. Web-only UI (Cairo Alpha, Polkadot, `App.svelte`) does not need a changeset. Never version `ui` in a changeset without also listing `@openzeppelin/contracts-mcp`.
8. **UI that ships in MCP Apps**: `ui` is private (versioned in-repo, not published to npm). MCP App HTML is built from `ui` when `@openzeppelin/contracts-mcp` is published, but only from the `packages/ui/src/mcp-apps/` bundle graph — the entries import the Wizard controls directly, not the web shells. Changes to modules reachable from `src/mcp-apps/` need **one changeset listing both** `ui` and `@openzeppelin/contracts-mcp`; never version `ui` without also listing `@openzeppelin/contracts-mcp`. Web-only UI needs **no changeset**: the `App.svelte` shells and modules imported only by them (e.g. `solidity/overrides.ts`, `solidity/remix.ts`, `common/post-config.ts`), plus Cairo Alpha and Polkadot. To decide, search `packages/ui/src/mcp-apps/` for imports of the changed modules. `changeset status` in CI still flags most `packages/ui/src` paths as needing a changeset, so a web-only UI PR gets the `ignore-changeset` label instead of a changeset.

## Steps

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog


## 0.1.7 (2026-08-26)

- Add a supported-languages table and an example command for each language to the README. No functional changes. ([#857](https://github.com/OpenZeppelin/contracts-wizard/pull/857))

## 0.1.6 (2026-08-25)

- Add support for TRON Contracts. ([#806](https://github.com/OpenZeppelin/contracts-wizard/pull/806))
Expand Down
47 changes: 46 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@ npx @openzeppelin/contracts-cli --help
npx @openzeppelin/contracts-cli solidity-erc20 --help
```

### Examples
## Supported languages

Commands are named in the format `<language>-<contract>`.

| Language | Contracts |
| --- | --- |
| solidity | erc20, erc721, erc1155, stablecoin, rwa, account, governor, custom |
| cairo | erc20, erc721, erc1155, account, multisig, governor, vesting, custom |
| confidential | erc7984 |
| stellar | fungible, stablecoin, non-fungible, governor, vault, account |
| stylus | erc20, erc721, erc1155 |
| tron | trc20, trc721, trc1155, governor, custom |
| uniswap-hooks | hooks (command is just `uniswap-hooks`) |

## Examples

These examples are not exhaustive — run `npx @openzeppelin/contracts-cli --help` for all commands, and `npx @openzeppelin/contracts-cli <command> --help` for all options of a command.

```sh
npx @openzeppelin/contracts-cli solidity-erc20 --name MyToken --symbol MTK --mintable --burnable --access ownable
Expand All @@ -26,3 +42,32 @@ npx @openzeppelin/contracts-cli solidity-erc20 --name MyToken --symbol MTK --min
```sh
npx @openzeppelin/contracts-cli cairo-erc20 --name MyToken --symbol MTK --mintable --pausable --upgradeable
```

```sh
npx @openzeppelin/contracts-cli confidential-erc7984 --name MyToken --symbol MTK --contractURI https://example.com/token.json --networkConfig zama-ethereum --wrappable
```

```sh
npx @openzeppelin/contracts-cli stellar-fungible --name MyToken --symbol MTK --premint 1000 --mintable
```

```sh
npx @openzeppelin/contracts-cli stylus-erc721 --name MyNFT --burnable --enumerable
```

```sh
npx @openzeppelin/contracts-cli tron-trc20 --name MyToken --symbol MTK --mintable --upgradeable uups
```

```sh
npx @openzeppelin/contracts-cli uniswap-hooks --hook BaseHook --name MyHook \
--pausable false --currencySettler false --safeCast false --transientStorage false \
--shares.options false --inputs.blockNumberOffset 0 --inputs.maxAbsTickDelta 0 \
--permissions.beforeInitialize false --permissions.afterInitialize false \
--permissions.beforeAddLiquidity false --permissions.afterAddLiquidity false \
--permissions.beforeRemoveLiquidity false --permissions.afterRemoveLiquidity false \
--permissions.beforeSwap --permissions.afterSwap \
--permissions.beforeDonate false --permissions.afterDonate false \
--permissions.beforeSwapReturnDelta false --permissions.afterSwapReturnDelta false \
--permissions.afterAddLiquidityReturnDelta false --permissions.afterRemoveLiquidityReturnDelta false
```
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/contracts-cli",
"version": "0.1.6",
"version": "0.1.7",
"description": "CLI for generating smart contracts using OpenZeppelin Contracts Wizard",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
Expand Down
60 changes: 60 additions & 0 deletions packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,66 @@ test('each core kind has cli registry entry', async t => {
}
});

// --- README completeness ---
// The README is what npm renders for the package, so a language shipped without
// a table row or example there is invisible to anyone browsing the npm page.

/** `uniswap-hooks` is a whole command; every other command is `<language>-<contract>`. */
function commandLanguage(command: string): string {
return command === 'uniswap-hooks' ? command : command.split('-')[0]!;
}

test('each registry command is listed in the README table', async t => {
const readme = await readFile(join(__dirname, '..', 'README.md'), 'utf-8');

for (const command of Object.keys(registry)) {
const language = commandLanguage(command);
const row = readme.split('\n').find(line => line.startsWith(`| ${language} |`));

t.truthy(row, `packages/cli/README.md 'Supported languages' table is missing a row for '${language}'`);
const contract = command.slice(language.length + 1);
if (row !== undefined && contract !== '') {
t.true(row.includes(contract), `README table row for '${language}' is missing '${contract}'`);
}
}
});

test('each registry language has a README example', async t => {
const readme = await readFile(join(__dirname, '..', 'README.md'), 'utf-8');
// Match only within the Examples section, so commands mentioned elsewhere
// (e.g. the `--help` invocation under Usage) cannot satisfy the check.
const examplesStart = readme.indexOf('## Examples');
t.true(examplesStart >= 0, `packages/cli/README.md is missing the '## Examples' section`);
const examples = readme.slice(examplesStart);
const languages = new Set(Object.keys(registry).map(commandLanguage));

for (const language of languages) {
t.regex(
examples,
new RegExp(`contracts-cli ${language}[ -]`),
`packages/cli/README.md 'Examples' is missing an example command for '${language}'`,
);
}
});

test('each README example runs', async t => {
const readme = await readFile(join(__dirname, '..', 'README.md'), 'utf-8');
const examplesStart = readme.indexOf('## Examples');
t.true(examplesStart >= 0, `packages/cli/README.md is missing the '## Examples' section`);
const blocks = [...readme.slice(examplesStart).matchAll(/```sh\n(.*?)```/gs)].map(match => match[1]!);
t.true(blocks.length > 0, 'Expected sh blocks in the Examples section');

for (const block of blocks) {
// Join backslash line continuations the way a shell would.
const command = block.replace(/\\\n/g, ' ').trim();
t.true(command.startsWith('npx @openzeppelin/contracts-cli '), `Unexpected example command: ${command}`);

const args = command.replace('npx @openzeppelin/contracts-cli ', '').split(/\s+/);
const output = run(...args);
t.true(output.length > 0, `Example printed no contract: ${command}`);
}
});

// --- Help snapshots ---

test('no args', t => {
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog


## 0.6.6 (2026-08-26)

- Add TRON to the README's language table. No functional changes. ([#857](https://github.com/OpenZeppelin/contracts-wizard/pull/857))

## 0.6.5 (2026-08-25)

- Add support for TRON Contracts. ([#806](https://github.com/OpenZeppelin/contracts-wizard/pull/806))
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Provides tools to generate smart contract source code for the following language
| confidential | erc7984 |
| stellar | fungible, stablecoin, non-fungible, governor, vault, account |
| stylus | erc20, erc721, erc1155 |
| tron | trc20, trc721, trc1155, governor, custom |
| uniswap-hooks | hooks (tool name is just `uniswap-hooks`) |

### MCP Apps
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/contracts-mcp",
"version": "0.6.5",
"version": "0.6.6",
"description": "OpenZeppelin Contracts MCP Server",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
Expand Down
26 changes: 26 additions & 0 deletions packages/mcp/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ test('each mcp tools folder is exported from index.ts', async t => {
}
});

/**
* The README is what npm renders for the package, so a language shipped without
* a row in its table is invisible to anyone browsing the npm page (TRON was
* initially missed this way).
*/
test('each mcp language has a README table row listing its tools', async t => {
const readme = await readFile(join(__dirname, '..', 'README.md'), 'utf-8');
const byLanguage = new Map<string, string[]>();

for (const { language, toolName } of await listExpectedWizardTools()) {
const suffix = toolName.startsWith(`${language}-`) ? toolName.slice(language.length + 1) : toolName;
byLanguage.set(language, [...(byLanguage.get(language) ?? []), suffix]);
}

for (const [language, suffixes] of byLanguage) {
const row = readme.split('\n').find(line => line.startsWith(`| ${language} |`));

t.truthy(row, `packages/mcp/README.md language table is missing a row for '${language}'`);
if (row === undefined) continue;

for (const suffix of suffixes) {
t.true(row.includes(suffix), `README table row for '${language}' is missing '${suffix}'`);
}
}
});

/**
* Kind-level counterpart to the language check above, mirroring `each core kind has cli registry
* entry` in packages/cli: a kind added to an existing core language must also get an MCP tool.
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/common/icons/TronIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- TRON logo. Copyright TRON DAO. Official brand asset (https://tronbrandkit.figma.site/),
inheriting currentColor so it matches the other action-button icons. -->
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.8652 83.1535" aria-hidden="true" focusable="false">
<path
d="M60.1338 8.45628L0.000244141 0L33.5435 83.1535L79.8652 23.9595L60.1338 8.55025V8.45628ZM68.6841 22.738L49.1407 26.8722L59.2882 15.4092L68.6841 22.738ZM52.993 13.5301L40.4965 27.7178L15.5974 8.26837L53.0869 13.5301H52.993ZM33.0737 65.5832L11.8391 13.0603L37.5837 33.1674L33.0737 65.5832ZM38.9931 66.4288L43.5031 34.3889L68.1203 29.1272L38.9931 66.4288Z"
></path>
</svg>
51 changes: 44 additions & 7 deletions packages/ui/src/solidity/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,21 @@
}, 1000);
};

$: remixUnsupportedTooltip = overrides.openInRemix?.unsupportedSource?.test(code)
? overrides.openInRemix.unsupportedSource.tooltip
: undefined;
$: remixUpgradeableWarning = opts?.upgradeable ? overrides.openInRemix?.upgradeableWarning?.(opts) : undefined;

const remixHandler = async (e: MouseEvent) => {
e.preventDefault();
if ((e.target as Element)?.classList.contains('disabled')) return;

const remappings = overrides.overrideVersionedRemappings
? overrides.overrideVersionedRemappings(opts)
: getVersionedRemappings(opts);
window.open(remixURL(code, remappings, !!opts?.upgradeable).toString(), '_blank', 'noopener,noreferrer');
const url = overrides.openInRemix
? overrides.openInRemix.url(code, remappings, !!opts?.upgradeable)
: remixURL(code, remappings, !!opts?.upgradeable);
window.open(url.toString(), '_blank', 'noopener,noreferrer');
if (opts) {
await postConfig(opts, 'remix', language);
}
Expand Down Expand Up @@ -298,9 +305,35 @@
</button>

{#if showButtons.openInRemix}
{#if opts?.upgradeable === 'transparent'}
{#if remixUnsupportedTooltip !== undefined}
<!-- Hard block, no "open anyway": the IDE would receive a corrupted
source, while Copy/Download right next to it stay lossless. -->
<Tooltip let:trigger theme="light-red border" hideOnClick={false} interactive maxWidth="22em">
<button use:trigger class="action-button with-text disabled">
<svelte:component this={overrides.openInRemix?.icon ?? RemixIcon} />
{overrides.openInRemix?.label ?? 'Open in Remix'}
</button>
<div slot="content">
<p>{remixUnsupportedTooltip}</p>
</div>
</Tooltip>
{:else if remixUpgradeableWarning !== undefined}
<Tooltip let:trigger theme="light-red border" hideOnClick={false} interactive>
<button use:trigger class="action-button with-text disabled">
<svelte:component this={overrides.openInRemix?.icon ?? RemixIcon} />
{overrides.openInRemix?.label ?? 'Open in Remix'}
</button>
<div slot="content">
<p style="margin-bottom: 0.5rem;">{remixUpgradeableWarning}</p>
<p>
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="#" on:click={remixHandler}>{overrides.openInRemix?.label ?? 'Open in Remix'} anyway</a>.
</p>
</div>
</Tooltip>
{:else if overrides.openInRemix === undefined && opts?.upgradeable === 'transparent'}
<Tooltip let:trigger theme="light-red border" hideOnClick={false} interactive>
<button use:trigger class="action-button with-text disabled" on:click={remixHandler}>
<button use:trigger class="action-button with-text disabled">
<RemixIcon />
Open in Remix
</button>
Expand All @@ -320,8 +353,8 @@
</Tooltip>
{:else}
<button class="action-button with-text" on:click={remixHandler}>
<RemixIcon />
Open in Remix
<svelte:component this={overrides.openInRemix?.icon ?? RemixIcon} />
{overrides.openInRemix?.label ?? 'Open in Remix'}
</button>
{/if}
{/if}
Expand All @@ -336,7 +369,11 @@
<FileIcon />
<div class="download-option-content">
<p>Single file</p>
<p>Requires installation of npm package (<code>@openzeppelin/contracts</code>).</p>
<p>
Requires installation of npm package (<code
>{overrides.npmPackageName ?? '@openzeppelin/contracts'}</code
>).
</p>
<p>Simple to receive updates.</p>
</div>
</button>
Expand Down
48 changes: 44 additions & 4 deletions packages/ui/src/solidity/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,51 @@ export interface Overrides {
omitOpenInRemix?: boolean;

/**
* Override the remappings passed to Remix when "Open in Remix" is used.
* Defaults to `@openzeppelin/wizard`'s `getVersionedRemappings(opts)`.
* Set this when the generated source uses a non-default contracts
* library (e.g. `@openzeppelin/tron-contracts`).
* Override the remappings passed to the web IDE when the "Open in ..."
* action is used. Defaults to `@openzeppelin/wizard`'s
* `getVersionedRemappings(opts)`. Set this when the generated source uses a
* non-default contracts library (e.g. `@openzeppelin/tron-contracts`).
*/
overrideVersionedRemappings?: (opts?: GenericOptions) => string[];

/**
* Retargets the "Open in Remix" action to a different web IDE. Used by
* ecosystems with their own Remix fork. `omitOpenInRemix`
* still hides the action entirely when set.
*/
openInRemix?: {
/** Button label replacing "Open in Remix". */
label: string;
/** Button icon component. Defaults to the Remix logo. */
icon?: ComponentType;
/** Builds the IDE deep link for the rendered source. */
url: (code: string, remappings: string[], upgradeable: boolean) => URL;
/**
* Disables the button (showing this tooltip) when the rendered source
* matches `test` — for IDEs whose URL loader cannot receive some sources
* faithfully.
*/
unsupportedSource?: {
test: (code: string) => boolean;
tooltip: string;
};
/**
* Greys out the button for upgradeable contracts, with this warning and an
* "open anyway" link in its tooltip — the same pattern as Remix's
* transparent-proxy warning, which this replaces (that warning is
* Remix-specific). Takes the current options so the message can vary by
* kind (e.g. Governor has no package downloads to point to).
*/
upgradeableWarning?: (opts?: GenericOptions) => string;
};

/**
* npm package named in the "Single file" download description. Defaults to
* `@openzeppelin/contracts`; ecosystems with their own contracts library
* override it.
*/
npmPackageName?: string;

/**
* Print options passed to `printContract` when the UI renders the source for
* display, copy, and single-file download. Ecosystems use this to apply a
Expand Down Expand Up @@ -130,6 +168,8 @@ export const defaultOverrides: Overrides = {
secondaryDownloadAction: undefined,
omitOpenInRemix: false,
overrideVersionedRemappings: undefined,
openInRemix: undefined,
npmPackageName: undefined,
printOptions: undefined,
sanitizeOmittedFeatures: (_: GenericOptions) => {},
postConfigLanguage: undefined,
Expand Down
11 changes: 7 additions & 4 deletions packages/ui/src/solidity/remix.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// UTF-8-safe base64 without trailing padding for the web-IDE deep links; the
// IDEs' atob decoding accepts unpadded input.
export const encodeUtf8Base64 = (str: string): string =>
btoa(String.fromCharCode(...new TextEncoder().encode(str))).replace(/=*$/, '');

export function remixURL(code: string, remappings: string[] = [], upgradeable = false): URL {
const remix = new URL('https://remix.ethereum.org');

const encodeBase64 = (str: string) => btoa(String.fromCharCode(...new TextEncoder().encode(str))).replace(/=*$/, '');

const hashParams = new URLSearchParams();

hashParams.set('code', encodeBase64(code));
hashParams.set('code', encodeUtf8Base64(code));

if (remappings.length > 0) {
hashParams.set('remaps', encodeBase64(remappings.join('\n')));
hashParams.set('remaps', encodeUtf8Base64(remappings.join('\n')));
}

if (upgradeable) {
Expand Down
Loading
Loading