diff --git a/.changeset/light-regions-design.md b/.changeset/light-regions-design.md new file mode 100644 index 000000000..9887520fc --- /dev/null +++ b/.changeset/light-regions-design.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/wizard': patch +--- + +Use unicode syntax for strings with non-ASCII characters diff --git a/packages/core/solidity/src/contract.test.ts b/packages/core/solidity/src/contract.test.ts index 0a1824cee..9e81545bb 100644 --- a/packages/core/solidity/src/contract.test.ts +++ b/packages/core/solidity/src/contract.test.ts @@ -22,6 +22,11 @@ test('contract basics', t => { t.snapshot(printContract(Foo)); }); +test('contract name is unicodeSafe', t => { + const Foo = new ContractBuilder('Footeć'); + t.snapshot(printContract(Foo)); +}); + test('contract with a parent', t => { const Foo = new ContractBuilder('Foo'); const Bar = toParentContract('Bar', './Bar.sol'); diff --git a/packages/core/solidity/src/contract.test.ts.md b/packages/core/solidity/src/contract.test.ts.md index 09cdba7fc..8a9748522 100644 --- a/packages/core/solidity/src/contract.test.ts.md +++ b/packages/core/solidity/src/contract.test.ts.md @@ -16,6 +16,18 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## contract name is unicodeSafe + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + contract Footec {␊ + }␊ + ` + ## contract with a parent > Snapshot 1 diff --git a/packages/core/solidity/src/contract.test.ts.snap b/packages/core/solidity/src/contract.test.ts.snap index 98d495d4a..e51df0f83 100644 Binary files a/packages/core/solidity/src/contract.test.ts.snap and b/packages/core/solidity/src/contract.test.ts.snap differ diff --git a/packages/core/solidity/src/custom.test.ts b/packages/core/solidity/src/custom.test.ts index 751b40277..6262bf26a 100644 --- a/packages/core/solidity/src/custom.test.ts +++ b/packages/core/solidity/src/custom.test.ts @@ -34,6 +34,10 @@ function testAPIEquivalence(title: string, opts?: CustomOptions) { testCustom('custom', {}); +testCustom('custom name is unicode safe', { + name: 'ćontract', +}); + testCustom('pausable', { pausable: true, }); diff --git a/packages/core/solidity/src/custom.test.ts.md b/packages/core/solidity/src/custom.test.ts.md index 40bc9b94b..e23c2db14 100644 --- a/packages/core/solidity/src/custom.test.ts.md +++ b/packages/core/solidity/src/custom.test.ts.md @@ -16,6 +16,18 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## custom name is unicode safe + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + contract Contract {␊ + }␊ + ` + ## pausable > Snapshot 1 diff --git a/packages/core/solidity/src/custom.test.ts.snap b/packages/core/solidity/src/custom.test.ts.snap index d48b8fd6e..62f68ccd2 100644 Binary files a/packages/core/solidity/src/custom.test.ts.snap and b/packages/core/solidity/src/custom.test.ts.snap differ diff --git a/packages/core/solidity/src/erc1155.test.ts b/packages/core/solidity/src/erc1155.test.ts index bea4ae13e..5c818868b 100644 --- a/packages/core/solidity/src/erc1155.test.ts +++ b/packages/core/solidity/src/erc1155.test.ts @@ -36,6 +36,8 @@ function testAPIEquivalence(title: string, opts?: ERC1155Options) { testERC1155('basic', {}); +testERC1155('name is unicodeSafe', { name: 'MyTokeć' }); + testERC1155('basic + roles', { access: 'roles', }); diff --git a/packages/core/solidity/src/erc1155.test.ts.md b/packages/core/solidity/src/erc1155.test.ts.md index 6b9b25c53..12dc5ca16 100644 --- a/packages/core/solidity/src/erc1155.test.ts.md +++ b/packages/core/solidity/src/erc1155.test.ts.md @@ -27,6 +27,29 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## name is unicodeSafe + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + ␊ + contract MyTokec is ERC1155, Ownable {␊ + constructor(address initialOwner)␊ + ERC1155("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + function setURI(string memory newuri) public onlyOwner {␊ + _setURI(newuri);␊ + }␊ + }␊ + ` + ## basic + roles > Snapshot 1 diff --git a/packages/core/solidity/src/erc1155.test.ts.snap b/packages/core/solidity/src/erc1155.test.ts.snap index bfafbc810..dfa322671 100644 Binary files a/packages/core/solidity/src/erc1155.test.ts.snap and b/packages/core/solidity/src/erc1155.test.ts.snap differ diff --git a/packages/core/solidity/src/erc20.test.ts b/packages/core/solidity/src/erc20.test.ts index 2ea481d3d..aa20d9c5c 100644 --- a/packages/core/solidity/src/erc20.test.ts +++ b/packages/core/solidity/src/erc20.test.ts @@ -37,6 +37,8 @@ function testAPIEquivalence(title: string, opts?: ERC20Options) { testERC20('basic erc20', {}); +testERC20('erc20 name is unicodeSafe', { name: 'MyTokeć' }); + testERC20('erc20 burnable', { burnable: true, }); diff --git a/packages/core/solidity/src/erc20.test.ts.md b/packages/core/solidity/src/erc20.test.ts.md index 41aa2e47e..d144f5651 100644 --- a/packages/core/solidity/src/erc20.test.ts.md +++ b/packages/core/solidity/src/erc20.test.ts.md @@ -20,6 +20,22 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 name is unicodeSafe + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyTokec is ERC20, ERC20Permit {␊ + constructor() ERC20(unicode"MyTokeć", "MTK") ERC20Permit(unicode"MyTokeć") {}␊ + }␊ + ` + ## erc20 burnable > Snapshot 1 diff --git a/packages/core/solidity/src/erc20.test.ts.snap b/packages/core/solidity/src/erc20.test.ts.snap index d99b1bc7b..33e9034f9 100644 Binary files a/packages/core/solidity/src/erc20.test.ts.snap and b/packages/core/solidity/src/erc20.test.ts.snap differ diff --git a/packages/core/solidity/src/erc721.test.ts b/packages/core/solidity/src/erc721.test.ts index 1b26718e0..a63cc4b88 100644 --- a/packages/core/solidity/src/erc721.test.ts +++ b/packages/core/solidity/src/erc721.test.ts @@ -36,6 +36,8 @@ function testAPIEquivalence(title: string, opts?: ERC721Options) { testERC721('basic', {}); +testERC721('name is unicodeSafe', { name: 'MyTokeć' }); + testERC721('base uri', { baseUri: 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/', }); diff --git a/packages/core/solidity/src/erc721.test.ts.md b/packages/core/solidity/src/erc721.test.ts.md index 31b2f1cad..2d4dcae6b 100644 --- a/packages/core/solidity/src/erc721.test.ts.md +++ b/packages/core/solidity/src/erc721.test.ts.md @@ -19,6 +19,21 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## name is unicodeSafe + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";␊ + ␊ + contract MyTokec is ERC721 {␊ + constructor() ERC721(unicode"MyTokeć", "MTK") {}␊ + }␊ + ` + ## base uri > Snapshot 1 diff --git a/packages/core/solidity/src/erc721.test.ts.snap b/packages/core/solidity/src/erc721.test.ts.snap index 9c2145943..623412145 100644 Binary files a/packages/core/solidity/src/erc721.test.ts.snap and b/packages/core/solidity/src/erc721.test.ts.snap differ diff --git a/packages/core/solidity/src/print.ts b/packages/core/solidity/src/print.ts index 11fce8d12..c9e85615a 100644 --- a/packages/core/solidity/src/print.ts +++ b/packages/core/solidity/src/print.ts @@ -16,6 +16,7 @@ import { mapValues } from './utils/map-values'; import SOLIDITY_VERSION from './solidity-version.json'; import { inferTranspiled } from './infer-transpiled'; import { compatibleContractsSemver } from './utils/version'; +import { stringifyUnicodeSafe } from './utils/sanitize'; export function printContract(contract: Contract, opts?: Options): string { const helpers = withHelpers(contract, opts); @@ -148,7 +149,7 @@ export function printValue(value: Value): string { throw new Error(`Number not representable (${value})`); } } else { - return JSON.stringify(value); + return stringifyUnicodeSafe(value); } } diff --git a/packages/core/solidity/src/utils/sanitize.test.ts b/packages/core/solidity/src/utils/sanitize.test.ts new file mode 100644 index 000000000..327e5dc95 --- /dev/null +++ b/packages/core/solidity/src/utils/sanitize.test.ts @@ -0,0 +1,46 @@ +import test from 'ava'; +import { stringifyUnicodeSafe } from './sanitize'; + +test('stringifyUnicodeSafe', t => { + const cases = [ + { + input: 'My Token', + expected: '"My Token"', + description: 'should handle string with no special characters', + }, + { + input: 'MyToke"ć"', + expected: 'unicode"MyToke\\"ć\\""', + description: 'should escape double quotes and wrap in unicode"" if unicode characters are present', + }, + { + input: '', + expected: '""', + description: 'should handle empty string', + }, + { + input: 'ć', + expected: 'unicode"ć"', + description: 'should handle string with only unicode characters', + }, + { + input: 'MyToken', + expected: '"MyToken"', + description: 'should handle string with no special characters', + }, + { + input: 'MyTok"e"n', + expected: '"MyTok\\"e\\"n"', + description: 'should handle escaped double quotes', + }, + { + input: 'MyTokeć', + expected: 'unicode"MyTokeć"', + description: 'should handle string with mixed ASCII and unicode characters', + }, + ]; + + for (const { input, expected, description } of cases) { + t.is(stringifyUnicodeSafe(input), expected, description); + } +}); diff --git a/packages/core/solidity/src/utils/sanitize.ts b/packages/core/solidity/src/utils/sanitize.ts new file mode 100644 index 000000000..7830cc51f --- /dev/null +++ b/packages/core/solidity/src/utils/sanitize.ts @@ -0,0 +1,6 @@ +export function stringifyUnicodeSafe(str: string): string { + // eslint-disable-next-line no-control-regex + const containsUnicode = /[^\x00-\x7F]/.test(str); + + return containsUnicode ? `unicode"${str.replace(/"/g, '\\"')}"` : JSON.stringify(str); +}