diff --git a/.changeset/add-tron-support.md b/.changeset/add-tron-support.md new file mode 100644 index 000000000..abe19e400 --- /dev/null +++ b/.changeset/add-tron-support.md @@ -0,0 +1,10 @@ +--- +'@openzeppelin/wizard': patch +'@openzeppelin/wizard-common': patch +'@openzeppelin/contracts-mcp': patch +'@openzeppelin/contracts-cli': patch +--- + +Add support for TRON Contracts. +- Covers TRC20, TRC721, TRC1155, Governor, and Custom contracts. +- On TRON, the Governor's `blockTime` defaults to 3 seconds to match its block production. diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts index 9606cafc8..80c27786d 100644 --- a/packages/cli/src/cli.test.ts +++ b/packages/cli/src/cli.test.ts @@ -3,7 +3,7 @@ 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 { erc20, stablecoin, buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; import { registry } from './registry'; const CLI = join(__dirname, '..', 'dist', 'index.js'); @@ -195,3 +195,44 @@ test('nested dot options with multiple fields', t => { }), ); }); + +// --- TRON --- + +test('tron-trc20 rewrites ERC20 to TRC20', t => { + const output = run('tron-trc20', '--name', 'TestToken', '--symbol', 'TST'); + t.is(output, printContract(buildGeneric({ kind: 'ERC20', name: 'TestToken', symbol: 'TST' }), tronPrintProfile)); + t.true(output.includes('TRC20'), 'output should contain TRC20'); + t.true( + output.includes('@openzeppelin/tron-contracts/token/TRC20/TRC20.sol'), + 'output should import from @openzeppelin/tron-contracts', + ); +}); + +test('tron-trc721 rewrites ERC721 to TRC721', t => { + const output = run('tron-trc721', '--name', 'TestNFT', '--symbol', 'TNFT'); + t.is(output, printContract(buildGeneric({ kind: 'ERC721', name: 'TestNFT', symbol: 'TNFT' }), tronPrintProfile)); + t.true(output.includes('TRC721'), 'output should contain TRC721'); +}); + +test('tron-trc1155 rewrites ERC1155 to TRC1155', t => { + const output = run('tron-trc1155', '--name', 'TestMulti', '--uri', 'ipfs://example/{id}'); + t.is( + output, + printContract(buildGeneric({ kind: 'ERC1155', name: 'TestMulti', uri: 'ipfs://example/{id}' }), tronPrintProfile), + ); + t.true(output.includes('TRC1155'), 'output should contain TRC1155'); +}); + +test('tron-trc20 caps pragma at 0.8.26', t => { + const output = run('tron-trc20', '--name', 'TestToken', '--symbol', 'TST'); + t.true(output.includes('pragma solidity ^0.8.26;'), 'pragma should be capped at 0.8.26'); + t.false(output.includes('pragma solidity ^0.8.27'), 'pragma should not be 0.8.27 (above tron-solc max)'); +}); + +test('tron-trc20 renames the library but never user name/symbol literals', t => { + // A name and symbol that embed a token standard: only the inherited base is + // renamed to TRC20; the user's deployed name() and symbol() are untouched. + const output = run('tron-trc20', '--name', 'My ERC20 Token', '--symbol', 'ERC20'); + t.true(output.includes('contract MyERC20Token is TRC20'), 'base renamed, contract name kept'); + t.true(output.includes('TRC20("My ERC20 Token", "ERC20")'), 'name/symbol literals preserved'); +}); diff --git a/packages/cli/src/cli.test.ts.md b/packages/cli/src/cli.test.ts.md index 5f9bc889c..0915dce20 100644 --- a/packages/cli/src/cli.test.ts.md +++ b/packages/cli/src/cli.test.ts.md @@ -11,7 +11,7 @@ Generated by [AVA](https://avajs.dev). `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-governor, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks␊ + 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-governor, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks, tron-trc20, tron-trc721, tron-trc1155, tron-governor, tron-custom␊ ␊ Generated contract source code is printed to stdout.␊ ␊ @@ -25,7 +25,7 @@ Generated by [AVA](https://avajs.dev). `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-governor, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks␊ + 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-governor, stellar-stablecoin, stellar-non-fungible, stylus-erc20, stylus-erc721, stylus-erc1155, confidential-erc7984, uniswap-hooks, tron-trc20, tron-trc721, tron-trc1155, tron-governor, tron-custom␊ ␊ Generated contract source code is printed to stdout.␊ ␊ @@ -663,6 +663,129 @@ Generated by [AVA](https://avajs.dev). --info.license The license used by the contract, default is "MIT"␊ ` +## tron-trc20 --help + +> Snapshot 1 + + `tron-trc20: Make a fungible token per the TRC-20 standard, targeting the TRON Virtual Machine.␊ + ␊ + Required:␊ + --name The name of the contract␊ + --symbol The short symbol for the token␊ + ␊ + Options:␊ + --decimals The number of decimals used to represent token amounts. 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.␊ + --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"␊ + ` + +## tron-trc721 --help + +> Snapshot 1 + + `tron-trc721: Make a non-fungible token per the TRC-721 standard, targeting the TRON Virtual Machine.␊ + ␊ + 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".␊ + ` + +## tron-trc1155 --help + +> Snapshot 1 + + `tron-trc1155: Make a multi-token contract per the TRC-1155 standard, targeting the TRON Virtual Machine.␊ + ␊ + 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"␊ + ` + +## tron-governor --help + +> Snapshot 1 + + `tron-governor: Make a contract to implement governance, such as for a DAO, targeting the TRON Virtual Machine.␊ + ␊ + 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 3.␊ + --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"␊ + ` + +## tron-custom --help + +> Snapshot 1 + + `tron-custom: Make a custom smart contract, targeting the TRON Virtual Machine.␊ + ␊ + 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"␊ + ` + ## unknown command > Snapshot 1 diff --git a/packages/cli/src/cli.test.ts.snap b/packages/cli/src/cli.test.ts.snap index 52435c832..6ef7648fa 100644 Binary files a/packages/cli/src/cli.test.ts.snap and b/packages/cli/src/cli.test.ts.snap differ diff --git a/packages/cli/src/registry.ts b/packages/cli/src/registry.ts index 78d77ce26..f9fea36ff 100644 --- a/packages/cli/src/registry.ts +++ b/packages/cli/src/registry.ts @@ -1,8 +1,22 @@ 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 { + erc20, + erc721, + erc1155, + stablecoin, + realWorldAsset, + account, + governor, + custom, + buildGeneric, + printContract, + tronPrintProfile, + sanitizeTronOptions, + TRON_DEFAULT_BLOCK_TIME, +} from '@openzeppelin/wizard'; +import { solidityPrompts, tronPrompts } from '@openzeppelin/wizard-common'; import { solidityERC20Schema, solidityERC721Schema, @@ -12,6 +26,7 @@ import { solidityAccountSchema, solidityGovernorSchema, solidityCustomSchema, + tronGovernorSchema, } from '@openzeppelin/wizard-common/schemas'; import { @@ -148,4 +163,41 @@ export const registry = { // Uniswap Hooks 'uniswap-hooks': createRegistryEntry(uniswapHooksHooksSchema, opts => hooks.print(opts), uniswapHooksPrompts.Hooks), + + // TRON: build the structured contract, then render through the TRON library + // profile (TRC* token names + @openzeppelin/tron-contracts import paths). Going + // through printContract(buildGeneric(...), tronPrintProfile) — rather than + // post-processing rendered text — keeps user data (name/symbol/securityContact) + // and the contract name untouched. + 'tron-trc20': createRegistryEntry( + solidityERC20Schema, + opts => printContract(buildGeneric({ kind: 'ERC20', ...sanitizeTronOptions(opts) }), tronPrintProfile), + tronPrompts.TRC20, + ), + 'tron-trc721': createRegistryEntry( + solidityERC721Schema, + opts => printContract(buildGeneric({ kind: 'ERC721', ...opts }), tronPrintProfile), + tronPrompts.TRC721, + ), + 'tron-trc1155': createRegistryEntry( + solidityERC1155Schema, + opts => printContract(buildGeneric({ kind: 'ERC1155', ...opts }), tronPrintProfile), + tronPrompts.TRC1155, + ), + // Stablecoin and RealWorldAsset are intentionally not exposed on TRON — they + // depend on @openzeppelin/community-contracts, which is not ported to TRON. + 'tron-governor': createRegistryEntry( + tronGovernorSchema, + opts => + printContract( + buildGeneric({ kind: 'Governor', ...opts, blockTime: opts.blockTime ?? TRON_DEFAULT_BLOCK_TIME }), + tronPrintProfile, + ), + tronPrompts.Governor, + ), + 'tron-custom': createRegistryEntry( + solidityCustomSchema, + opts => printContract(buildGeneric({ kind: 'Custom', ...opts }), tronPrintProfile), + tronPrompts.Custom, + ), } satisfies Record; diff --git a/packages/common/src/ai/descriptions/tron.ts b/packages/common/src/ai/descriptions/tron.ts new file mode 100644 index 000000000..4424c64e8 --- /dev/null +++ b/packages/common/src/ai/descriptions/tron.ts @@ -0,0 +1,21 @@ +// IMPORTANT: This file must not have any imports since it is used in both Node and Deno environments, +// which have different requirements for file extensions in import statements. + +// TRON prompts. Used by MCP + CLI tron-* tools. Options are otherwise +// identical to the Solidity ecosystem; only the standard names + library +// import paths differ in the output (handled by `tronPrintProfile`). +export const tronPrompts = { + TRC20: 'Make a fungible token per the TRC-20 standard, targeting the TRON Virtual Machine.', + TRC721: 'Make a non-fungible token per the TRC-721 standard, targeting the TRON Virtual Machine.', + TRC1155: 'Make a multi-token contract per the TRC-1155 standard, targeting the TRON Virtual Machine.', + Governor: 'Make a contract to implement governance, such as for a DAO, targeting the TRON Virtual Machine.', + Custom: 'Make a custom smart contract, targeting the TRON Virtual Machine.', +}; + +// TRON-specific overrides of the Solidity Governor descriptions. Only the +// fields that differ from `solidityGovernorDescriptions` are listed here; the +// rest are reused. TRON's SR consensus produces a block every ~3s (vs ~12s on +// Ethereum), so the Governor's "block time" field defaults to 3 here. +export const tronGovernorDescriptions = { + blockTime: 'The block time of the chain in seconds, default is 3.', +}; diff --git a/packages/common/src/ai/schemas/index.ts b/packages/common/src/ai/schemas/index.ts index 1f028bf3d..d4d89185f 100644 --- a/packages/common/src/ai/schemas/index.ts +++ b/packages/common/src/ai/schemas/index.ts @@ -34,6 +34,8 @@ export { export { stylusCommonSchema, stylusERC20Schema, stylusERC721Schema, stylusERC1155Schema } from './stylus'; +export { tronGovernorSchema } from './tron'; + export { confidentialCommonSchema, confidentialERC7984Schema } from './confidential'; export { diff --git a/packages/common/src/ai/schemas/tron.ts b/packages/common/src/ai/schemas/tron.ts new file mode 100644 index 000000000..37f4dfaaa --- /dev/null +++ b/packages/common/src/ai/schemas/tron.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { tronGovernorDescriptions } from '../../index'; +import { solidityGovernorSchema } from './solidity'; + +// TRON tools reuse the Solidity schemas almost verbatim — the options are +// identical and only the generated standard names / import paths differ +// (handled downstream by `tronPrintProfile`). The one exception is the Governor's +// `blockTime`, whose description must mention TRON's ~3s default without +// leaking that note into the shared Solidity (and Polkadot) schemas. +export const tronGovernorSchema = { + ...solidityGovernorSchema, + blockTime: z.number().optional().describe(tronGovernorDescriptions.blockTime), +} as const satisfies z.ZodRawShape; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 35b07f282..881358831 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -4,5 +4,6 @@ export * from './ai/descriptions/confidential'; export * from './ai/descriptions/solidity'; export * from './ai/descriptions/stellar'; export * from './ai/descriptions/stylus'; +export * from './ai/descriptions/tron'; export * from './ai/descriptions/uniswap-hooks'; export * from './utils/object'; diff --git a/packages/core/solidity/src/contract.ts b/packages/core/solidity/src/contract.ts index 5a7931525..567287dad 100644 --- a/packages/core/solidity/src/contract.ts +++ b/packages/core/solidity/src/contract.ts @@ -59,6 +59,12 @@ export interface ContractFunction extends BaseFunction { export type FunctionKind = 'private' | 'internal' | 'public' | 'external'; export interface ContractStruct { name: string; + /** + * ERC-7201 ``. When set, the struct is printed with a + * `@custom:storage-location :` annotation, where + * `` comes from `Options.formulaId` (defaults to `erc7201`). + */ + namespaceId?: string; comments: string[]; variables: string[]; } diff --git a/packages/core/solidity/src/environments/hardhat/tron/package.json b/packages/core/solidity/src/environments/hardhat/tron/package.json new file mode 100644 index 000000000..a7de15574 --- /dev/null +++ b/packages/core/solidity/src/environments/hardhat/tron/package.json @@ -0,0 +1,19 @@ +{ + "name": "hardhat-tron-sample", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "hardhat test" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", + "@nomicfoundation/hardhat-ethers": "^3.0.9", + "@openzeppelin/hardhat-tron": "^0.1.0", + "@openzeppelin/tron-contracts": "^0.0.1", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } +} diff --git a/packages/core/solidity/src/index.ts b/packages/core/solidity/src/index.ts index b79c0005f..e1df24e32 100644 --- a/packages/core/solidity/src/index.ts +++ b/packages/core/solidity/src/index.ts @@ -55,3 +55,9 @@ export { formatLinesWithSpaces, spaceBetween } from './utils/format-lines'; export { findCover } from './utils/find-cover'; export type { PremintCalculation } from './erc20'; export { calculatePremint as calculateERC20Premint, scaleByPowerOfTen } from './erc20'; +export { + tronPrintProfile, + sanitizeTronOptions, + TRON_DEFAULT_BLOCK_TIME, + TRON_SOLIDITY_VERSION, +} from './utils/transform-tron'; diff --git a/packages/core/solidity/src/options.ts b/packages/core/solidity/src/options.ts index 7653e7001..344c8402c 100644 --- a/packages/core/solidity/src/options.ts +++ b/packages/core/solidity/src/options.ts @@ -27,6 +27,25 @@ export function upgradeableImport(p: ImportContract): ImportContract { export interface Options { transformImport?: (parent: ImportContract) => ImportContract; + /** + * Rename a referenced library symbol (e.g. an inherited base, an override + * target, or an argument type). Receives the symbol as it would otherwise be + * printed (after any upgradeable `*Upgradeable` rename) and returns the final + * name. Use this to remap OpenZeppelin library symbols — it never sees + * user-supplied data (names, symbols, NatSpec) so those can't be corrupted. + */ + transformName?: (name: string) => string; + /** + * Override the `pragma solidity` version (defaults to the Wizard's mainline + * version). Useful for ecosystems whose compiler lags mainline. + */ + solidityVersion?: string; + /** + * ERC-7201 `` written in the `@custom:storage-location` annotation + * of namespaced storage structs (defaults to `erc7201`). This only sets the + * annotation label, not the slot derivation — see `computeNamespacedStorageSlot`. + */ + formulaId?: string; /** * Add additional libraries to the compatibility banner printed at the top of the contract. * @@ -35,22 +54,37 @@ export interface Options { additionalCompatibleLibraries?: { name: string; path: string; version: string; alwaysKeepOzPrefix?: boolean }[]; } -export interface Helpers extends Required { +export interface Helpers { upgradeable: boolean; + /** Final printed name of a referenced symbol (upgradeable rename, then `Options.transformName`). */ transformName: (name: ReferencedContract) => string; + /** + * Name used for an upgradeable parent's `__{Name}_init` initializer call. + * This is the base name with `Options.transformName` applied but WITHOUT the + * `Upgradeable` suffix (the initializer keeps the base name, e.g. `__ERC20_init`). + */ + transformInitName: (name: ReferencedContract) => string; transformImport: (name: ImportContract) => ImportContract; + /** ERC-7201 `` for namespaced storage annotations (`Options.formulaId`, default `erc7201`). */ + formulaId: string; + additionalCompatibleLibraries: NonNullable; } export function withHelpers(contract: Contract, opts: Options = {}): Helpers { const contractUpgradeable = contract.upgradeable; + const renameSymbol = opts.transformName ?? ((name: string) => name); return { upgradeable: contractUpgradeable, - transformName: (n: ReferencedContract) => - contractUpgradeable && inferTranspiled(n) ? upgradeableName(n.name) : n.name, + transformName: (n: ReferencedContract) => { + const afterUpgradeable = contractUpgradeable && inferTranspiled(n) ? upgradeableName(n.name) : n.name; + return renameSymbol(afterUpgradeable); + }, + transformInitName: (n: ReferencedContract) => renameSymbol(n.name), transformImport: (p1: ImportContract) => { const p2 = contractUpgradeable && inferTranspiled(p1) ? upgradeableImport(p1) : p1; return opts.transformImport?.(p2) ?? p2; }, + formulaId: opts.formulaId ?? 'erc7201', additionalCompatibleLibraries: opts.additionalCompatibleLibraries ?? [], }; } diff --git a/packages/core/solidity/src/print.ts b/packages/core/solidity/src/print.ts index c12ba8e4c..3049c31f3 100644 --- a/packages/core/solidity/src/print.ts +++ b/packages/core/solidity/src/print.ts @@ -25,7 +25,7 @@ import { getCommunityContractsGitCommit } from './utils/community-contracts-git- export function printContract(contract: Contract, opts?: Options): string { const helpers = withHelpers(contract, opts); - const structs = contract.structs.map(_struct => printStruct(_struct)); + const structs = contract.structs.map(_struct => printStruct(_struct, helpers)); const fns = mapValues(sortedFunctions(contract), fns => fns.map(fn => printFunction(fn, helpers))); const hasOverrides = fns.override.some(l => l.length > 0); return formatLines( @@ -33,7 +33,7 @@ export function printContract(contract: Contract, opts?: Options): string { [ `// SPDX-License-Identifier: ${contract.license}`, printCompatibleLibraryVersions(contract, opts), - `pragma solidity ^${SOLIDITY_VERSION};`, + `pragma solidity ^${opts?.solidityVersion ?? SOLIDITY_VERSION};`, ], printImports(contract.imports, helpers), @@ -220,7 +220,10 @@ function sortedFunctions(contract: Contract): SortedFunctions { function printParentConstructor({ contract, params }: Parent, helpers: Helpers): [] | [string] { const useTranspiled = helpers.upgradeable && inferTranspiled(contract); - const fn = useTranspiled ? `__${contract.name}_init` : contract.name; + // The transpiled initializer keeps the base name (`__ERC20_init`), so it uses + // transformInitName (no `Upgradeable` suffix); a plain parent constructor call + // matches the inheritance list, so it uses the full transformName. + const fn = useTranspiled ? `__${helpers.transformInitName(contract)}_init` : helpers.transformName(contract); if (useTranspiled || params.length > 0) { return [fn + '(' + params.map(printValue).join(', ') + ')']; } else { @@ -320,9 +323,11 @@ function printFunction2( return fn; } -function printStruct(_struct: ContractStruct): Lines[] { +function printStruct(_struct: ContractStruct, { formulaId }: Helpers): Lines[] { const [comments, kindedName, code] = [_struct.comments, _struct.name, _struct.variables]; - const struct: Lines[] = [...comments]; + const storageLocation = + _struct.namespaceId !== undefined ? [`/// @custom:storage-location ${formulaId}:${_struct.namespaceId}`] : []; + const struct: Lines[] = [...storageLocation, ...comments]; const braces = code.length > 0 ? '{' : '{}'; struct.push([`struct ${kindedName}`, braces].join(' ')); diff --git a/packages/core/solidity/src/set-namespaced-storage.ts b/packages/core/solidity/src/set-namespaced-storage.ts index 712a32139..0bc2948a7 100644 --- a/packages/core/solidity/src/set-namespaced-storage.ts +++ b/packages/core/solidity/src/set-namespaced-storage.ts @@ -62,7 +62,8 @@ function makeStorageFunction(name: string): BaseFunction { function makeStorageStruct(name: string, namespaceId: string) { const struct: ContractStruct = { name: `${name}Storage`, - comments: [`/// @custom:storage-location erc7201:${namespaceId}`], + namespaceId, + comments: [], variables: [], }; return struct; diff --git a/packages/core/solidity/src/tron-compile.test.ts b/packages/core/solidity/src/tron-compile.test.ts new file mode 100644 index 000000000..4c9d89596 --- /dev/null +++ b/packages/core/solidity/src/tron-compile.test.ts @@ -0,0 +1,75 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import test from 'ava'; +import type { ExecutionContext } from 'ava'; +import hre from 'hardhat'; + +import { generateOptions } from './generate/sources'; +import { buildGeneric } from './build-generic'; +import type { KindedOptions } from './build-generic'; +import { printContract } from './print'; +import { OptionsError } from './error'; +import { tronPrintProfile, sanitizeTronOptions } from './utils/transform-tron'; + +// Contract kinds offered on TRON. Account (ERC-4337 EntryPoint is out of scope) and +// Stablecoin / RealWorldAsset (depend on @openzeppelin/community-contracts, not ported +// to TRON) are intentionally excluded, mirroring the CLI / MCP / UI surfaces. +const TRON_KINDS = [ + 'ERC20', + 'ERC721', + 'ERC1155', + 'Governor', + 'Custom', +] as const satisfies readonly (keyof KindedOptions)[]; + +// TRON analogue of `testCompile` in test.ts: generate every option combination for the +// TRON-eligible kinds, run each through the TRON sanitizer + print profile, and verify +// the result compiles against `@openzeppelin/tron-contracts`. This is the test that +// catches ERC->TRC mapping gaps (e.g. the Callback extension resolving to `ERC1363` +// instead of `TRC1363`) — something the content-snapshot tests structurally cannot do. +// +// SKIPPED until `@openzeppelin/tron-contracts` (and `@openzeppelin/tron-contracts-upgradeable`) +// are published to npm. To enable: +// 1. add both packages as devDependencies of this package; +// 2. compile these sources against the TRON library with tron-solc settings +// (0.8.26 + cancun + viaIR) — e.g. a dedicated Hardhat config / compile target that +// only picks up `generated-tron` and resolves `@openzeppelin/tron-contracts(-upgradeable)`; +// 3. remove `.skip` below. +for (const kind of TRON_KINDS) { + test.skip(`tron ${kind} result compiles`, async t => { + await testCompileTron(t, kind); + }); +} + +async function testCompileTron(t: ExecutionContext, kind: keyof KindedOptions) { + const generatedSourcesPath = path.join(hre.config.paths.sources, 'generated-tron'); + await fs.rm(generatedSourcesPath, { force: true, recursive: true }); + await fs.mkdir(generatedSourcesPath, { recursive: true }); + + let index = 0; + for (const options of generateOptions(kind)) { + // Mirror the TRON surfaces: drop options TRON doesn't support (e.g. `superchain`). + const tronOptions = options.kind === 'ERC20' ? sanitizeTronOptions(options) : options; + + let source: string; + try { + source = printContract(buildGeneric(tronOptions), tronPrintProfile); + } catch (e: unknown) { + if (e instanceof OptionsError) { + continue; + } + throw e; + } + + await fs.writeFile(path.format({ dir: generatedSourcesPath, name: `${kind}_${index++}`, ext: '.sol' }), source); + } + + // We only care that the contracts compile, not the artifacts. Empty outputSelection + // keeps compilation fast and within memory (same trick as test.ts:testCompile). + for (const { settings } of hre.config.solidity.compilers) { + settings.outputSelection = {}; + } + await hre.run('compile'); + + t.pass(); +} diff --git a/packages/core/solidity/src/utils/namespaced-slot.ts b/packages/core/solidity/src/utils/namespaced-slot.ts index 88fecb674..bb026d978 100644 --- a/packages/core/solidity/src/utils/namespaced-slot.ts +++ b/packages/core/solidity/src/utils/namespaced-slot.ts @@ -2,7 +2,13 @@ import { keccak256 } from 'ethereum-cryptography/keccak'; import { hexToBytes, toHex, utf8ToBytes } from 'ethereum-cryptography/utils'; /** - * Returns the ERC-7201 storage location for a given namespace id + * Returns the ERC-7201 storage location for a given namespace id. + * + * This is the only namespaced-storage slot derivation. `Options.formulaId` sets + * the `@custom:storage-location` annotation label but does NOT select a + * derivation, so every supported formula id must share this computation. A + * formula whose derivation actually differs would have to change this function + * and be computed at print time (formulaId is only available to the printer). */ export function computeNamespacedStorageSlot(id: string): string { const innerHash = keccak256(utf8ToBytes(id)); diff --git a/packages/core/solidity/src/utils/transform-tron.test.ts b/packages/core/solidity/src/utils/transform-tron.test.ts new file mode 100644 index 000000000..58806a5f1 --- /dev/null +++ b/packages/core/solidity/src/utils/transform-tron.test.ts @@ -0,0 +1,99 @@ +import test from 'ava'; + +import { printContract } from '../print'; +import { buildERC20 } from '../erc20'; +import { buildERC721 } from '../erc721'; +import { buildGovernor } from '../governor'; +import { buildCustom } from '../custom'; +import { tronPrintProfile, sanitizeTronOptions } from './transform-tron'; + +test('renames token standards in imports and inheritance', t => { + const source = printContract(buildERC20({ name: 'My Token', symbol: 'MTK', votes: true }), tronPrintProfile); + t.true(source.includes('import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol";')); + t.true(source.includes('@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol')); + t.regex(source, /contract MyToken is TRC20, TRC20Permit, TRC20Votes/); + // No bare ERC token standards leak through. + t.false(/\b(I)?ERC\d+/.test(source)); +}); + +test('maps the upgradeable package, keeping base proxy utils on tron-contracts', t => { + const source = printContract( + buildERC20({ name: 'My Token', symbol: 'MTK', upgradeable: 'uups', access: 'ownable', mintable: true }), + tronPrintProfile, + ); + // Transpiled parents resolve from the upgradeable package... + t.true(source.includes('@openzeppelin/tron-contracts-upgradeable/token/TRC20/TRC20Upgradeable.sol')); + t.true(source.includes('{TRC20Upgradeable}')); + // ...while the stateless proxy utilities stay on the base package. + t.true(source.includes('@openzeppelin/tron-contracts/proxy/utils/UUPSUpgradeable.sol')); + // The initializer keeps the base (TRC) name, no `Upgradeable` suffix. + t.true(source.includes('__TRC20_init(')); + t.false(source.includes('@openzeppelin/contracts-upgradeable/')); +}); + +test('caps the pragma at the tron-solc maximum (0.8.26)', t => { + const source = printContract(buildERC20({ name: 'T', symbol: 'T' }), tronPrintProfile); + t.regex(source, /pragma solidity \^0\.8\.26;/); +}); + +test('does NOT corrupt user name/symbol/securityContact literals', t => { + // A name and symbol that contain a token-standard string, and a matching + // securityContact. A text regex would rewrite all three; the structured + // transform leaves them untouched because they are user data, not symbols. + const source = printContract( + buildERC20({ name: 'My ERC20 Token', symbol: 'ERC20', info: { securityContact: 'ERC20-team@example.com' } }), + tronPrintProfile, + ); + t.true(source.includes('TRC20("My ERC20 Token", "ERC20")'), 'name/symbol literals preserved, base renamed'); + t.true(source.includes('@custom:security-contact ERC20-team@example.com'), 'securityContact preserved'); +}); + +test('does NOT corrupt a user contract name that contains a token standard', t => { + // The contract name is user data: it must match the filename/artifact the zip + // writes, so it stays verbatim while inherited bases are still renamed. + const source = printContract(buildCustom({ name: 'ERC20Token' }), tronPrintProfile); + t.true(source.includes('contract ERC20Token'), 'contract name preserved verbatim'); +}); + +test('renames standards beyond the core token set (TRC1363, TIP712)', t => { + // `callback` pulls in ERC1363 and `votes` without `permit` pulls in EIP712, so + // this exercises the rename on standards outside the core token set. + const source = printContract( + buildERC20({ name: 'My Token', symbol: 'MTK', callback: true, votes: true, permit: false }), + tronPrintProfile, + ); + t.true(source.includes('TRC1363'), 'ERC1363 -> TRC1363'); + t.true(source.includes('@openzeppelin/tron-contracts/utils/cryptography/TIP712.sol'), 'EIP712 import -> TIP712.sol'); + t.regex(source, /\bTIP712\b/, 'EIP712 symbol -> TIP712'); + t.false(/\b(I)?ERC\d+/.test(source), 'no bare ERC standard leaks through'); + t.false(/\bEIP\d+/.test(source), 'no bare EIP standard leaks through'); +}); + +test('renames the governor initializer base names to TRC where applicable', t => { + const source = printContract( + buildGovernor({ name: 'My Gov', delay: '1 day', period: '1 week', votes: 'erc20votes', upgradeable: 'uups' }), + tronPrintProfile, + ); + t.true(source.includes('@openzeppelin/tron-contracts-upgradeable/governance/GovernorUpgradeable.sol')); + // IVotes interface stays on the base package, name unchanged. + t.true(source.includes('@openzeppelin/tron-contracts/governance/utils/IVotes.sol')); +}); + +test('sanitizeTronOptions downgrades superchain bridging to custom', t => { + t.deepEqual(sanitizeTronOptions({ crossChainBridging: 'superchain' }), { crossChainBridging: 'custom' }); + t.deepEqual(sanitizeTronOptions({ crossChainBridging: 'erc7786native' }), { crossChainBridging: 'erc7786native' }); + t.deepEqual(sanitizeTronOptions({ crossChainBridging: 'custom' }), { crossChainBridging: 'custom' }); + t.deepEqual(sanitizeTronOptions({}), {}); +}); + +test('uses the TRON formula id (trc7201) for namespaced storage annotations', t => { + // Upgradeable ERC721 with incremental ids uses namespaced storage, whose struct + // carries a `@custom:storage-location :` annotation. On + // TRON the formula id is `trc7201` (TIP-7201) rather than `erc7201`. + const source = printContract( + buildERC721({ name: 'My NFT', symbol: 'NFT', mintable: true, incremental: true, upgradeable: 'uups' }), + tronPrintProfile, + ); + t.regex(source, /@custom:storage-location trc7201:/, 'uses the trc7201 formula id'); + t.false(source.includes('erc7201:'), 'no erc7201 formula id leaks through'); +}); diff --git a/packages/core/solidity/src/utils/transform-tron.ts b/packages/core/solidity/src/utils/transform-tron.ts new file mode 100644 index 000000000..904476fbc --- /dev/null +++ b/packages/core/solidity/src/utils/transform-tron.ts @@ -0,0 +1,105 @@ +// Structured print profile that adapts generated Solidity output for the TRON +// ecosystem. It plugs into `printContract(contract, tronPrintProfile)` via the +// `transformName` / `transformImport` hooks, so it only ever rewrites +// OpenZeppelin library symbols and import paths — never user-supplied data +// (contract name, token name/symbol literals, securityContact). +// +// `@openzeppelin/tron-contracts` mirrors `@openzeppelin/contracts` with two +// systematic differences, applied here structurally: +// +// 1. Path root: `@openzeppelin/contracts/...` -> `@openzeppelin/tron-contracts/...` +// (and `-upgradeable/...` -> `tron-contracts-upgradeable/...`) +// 2. Standards are renamed to the TRON family: ERC -> TRC, IERC -> ITRC, +// EIP -> TIP (e.g. ERC20 -> TRC20, ERC1363 -> TRC1363, EIP712 -> TIP712). +// tron-contracts adopts every standard as a TRC, so this is a blanket +// prefix swap. +// +// Non-standard identifiers (Ownable, AccessControl, etc.) stay verbatim. The +// rename is case-sensitive and only touches symbols/imports, so lowercase +// annotation strings are unaffected — notably the `erc7201:` storage-location +// tag emitted for upgradeable contracts (see set-namespaced-storage.ts). +// +// TODO(tron): the `erc7201:` storage-location tag stays as-is rather than +// TIP-7201's `trc7201:`. It's a build-time comment the symbol rename here +// doesn't touch, and it's inert on TRON (nothing in the deploy flow reads it). +// Revisit if `trc7201:` becomes the convention. + +import type { Options } from '../options'; +import type { ImportContract } from '../contract'; +import SOLIDITY_VERSION from '../solidity-version.json'; + +// The maximum 0.8.x patch level that tron-solc currently supports. The mainline +// Wizard emits a higher version (tron-solc lags), so TRON caps to this; the TVM +// Democritus hardfork targets 0.8.26 + cancun. Bump when tron-solc catches up. +const TRON_SOLC_MAX_PATCH = 26; + +// The `pragma solidity` version TRON emits and the version its toolchains +// compile with: the mainline Wizard version, capped at the tron-solc maximum. +// Hardhat/TronBox configs source their compiler version from this same constant. +export const TRON_SOLIDITY_VERSION = capTronSolidityVersion(SOLIDITY_VERSION); + +function capTronSolidityVersion(version: string): string { + const [major, minor, patch] = version.split('.').map(part => parseInt(part, 10)); + if (major === 0 && minor === 8 && Number.isInteger(patch)) { + return `0.8.${Math.min(patch!, TRON_SOLC_MAX_PATCH)}`; + } + return version; +} + +// TRON blocks are produced every 3 seconds (SR consensus), versus Ethereum's +// ~12s. Used wherever the Governor's `blockTime` would otherwise inherit the +// Solidity default of 12 (UI, CLI registry, and MCP tron-governor tool). +export const TRON_DEFAULT_BLOCK_TIME = 3; + +// Renames an OpenZeppelin standard symbol to its TRON counterpart (see file +// header): ERC->TRC, IERC->ITRC, EIP->TIP. Case-sensitive and applied only to +// library symbols and import paths, never user data or lowercase annotations. +function renameTronSymbol(name: string): string { + return name + .replace(/\bIERC(\d+)/g, 'ITRC$1') + .replace(/\bERC(\d+)/g, 'TRC$1') + .replace(/\bEIP(\d+)/g, 'TIP$1'); +} + +function rewriteTronImportPath(path: string): string { + // Rewrite the package root first (the `-upgradeable` root is matched before + // the base root since it's a longer prefix), then rename token standards in + // the directory and file names. + return renameTronSymbol( + path + .replace(/^@openzeppelin\/contracts-upgradeable\//, '@openzeppelin/tron-contracts-upgradeable/') + .replace(/^@openzeppelin\/contracts\//, '@openzeppelin/tron-contracts/'), + ); +} + +/** + * TRON library profile for `printContract`. Routes every surface (UI display, + * zip generators, CLI, MCP) through one definition. + */ +export const tronPrintProfile: Options = { + transformName: renameTronSymbol, + transformImport: (parent: ImportContract): ImportContract => ({ + ...parent, + name: renameTronSymbol(parent.name), + path: rewriteTronImportPath(parent.path), + }), + solidityVersion: TRON_SOLIDITY_VERSION, + // trc7201 uses the same slot derivation as erc7201; only the annotation label differs. + formulaId: 'trc7201', +}; + +// `superchain` cross-chain bridging is OP Stack-specific: it pulls in +// `draft-ERC20Bridgeable` plus the hardcoded OP-Stack `0x42...0028` predeploy, +// neither of which exists on the TVM. The UI form hides the option, but the CLI +// and MCP surfaces reuse the full Solidity schemas, so they funnel options +// through this gate to downgrade `superchain` to `custom` before building. +// Mutates in place (to match the UI's `sanitizeOmittedFeatures` override +// contract) and returns the same object for ergonomic use at call sites. +type TronSanitizableOptions = { crossChainBridging?: false | 'custom' | 'erc7786native' | 'superchain' }; + +export function sanitizeTronOptions(opts: T): T { + if (opts.crossChainBridging === 'superchain') { + opts.crossChainBridging = 'custom'; + } + return opts; +} diff --git a/packages/core/solidity/src/utils/tron-upgradeable.ts b/packages/core/solidity/src/utils/tron-upgradeable.ts new file mode 100644 index 000000000..eacb5192a --- /dev/null +++ b/packages/core/solidity/src/utils/tron-upgradeable.ts @@ -0,0 +1,65 @@ +import type { Contract } from '../contract'; + +// Helpers for generating upgradeable TRON deployment scaffolding. +// +// OpenZeppelin's Hardhat and Foundry Upgrades plugins target EVM chains and do +// NOT deploy to the TRON network, so the upgradeable TRON projects can't reuse +// the mainline `upgrades.deployProxy` flow. Instead they deploy the proxy by +// hand, per the official guide: +// https://github.com/OpenZeppelin/tron-contracts-upgradeable -> "Using with Upgrades". +// +// UUPS contracts deploy behind a `TRC1967Proxy`; transparent contracts deploy +// behind a `TransparentUpgradeableProxy` (which additionally takes an admin +// owner). Both proxies live in the base `@openzeppelin/tron-contracts` package +// (the transpiler renames `ERC1967` -> `TRC1967`). + +/** UUPS upgradeable contracts inherit `UUPSUpgradeable`; transparent ones don't. */ +export function isUUPS(c: Contract): boolean { + return c.parents.some(p => p.contract.name === 'UUPSUpgradeable'); +} + +export interface TronProxy { + /** Solidity contract name, also the artifact name for getContractFactory / artifacts.require. */ + contractName: 'TRC1967Proxy' | 'TransparentUpgradeableProxy'; + /** Import path within `@openzeppelin/tron-contracts`. */ + importPath: string; + /** Transparent proxies take an admin owner address before the init data. */ + isTransparent: boolean; +} + +export function tronProxyFor(c: Contract): TronProxy { + return isUUPS(c) + ? { + contractName: 'TRC1967Proxy', + importPath: '@openzeppelin/tron-contracts/proxy/TRC1967/TRC1967Proxy.sol', + isTransparent: false, + } + : { + contractName: 'TransparentUpgradeableProxy', + importPath: '@openzeppelin/tron-contracts/proxy/transparent/TransparentUpgradeableProxy.sol', + isTransparent: true, + }; +} + +/** + * Source for a throwaway `Proxy.sol` whose only purpose is to pull the proxy + * contract into the build, so the deploy script / migration can load its + * artifact. The generated `${c.name}` contract never imports it. + */ +export function tronProxyHelperSource(c: Contract, pragmaVersion: string): string { + const proxy = tronProxyFor(c); + return `\ +// SPDX-License-Identifier: MIT +pragma solidity ^${pragmaVersion}; + +// This file is NOT imported by ${c.name}. It exists only so the toolchain +// compiles the proxy contract that the deploy script puts ${c.name} behind. +// See https://github.com/OpenZeppelin/tron-contracts-upgradeable +import {${proxy.contractName}} from "${proxy.importPath}"; +`; +} + +/** Non-address initializer args have no auto-fillable placeholder. */ +export function hasUnsetInitArgs(c: Contract): boolean { + return c.constructorArgs.some(arg => arg.type !== 'address'); +} diff --git a/packages/core/solidity/src/zip-hardhat-tron.test.ts b/packages/core/solidity/src/zip-hardhat-tron.test.ts new file mode 100644 index 000000000..a15dc08d7 --- /dev/null +++ b/packages/core/solidity/src/zip-hardhat-tron.test.ts @@ -0,0 +1,161 @@ +import type { ExecutionContext } from 'ava'; +import test from 'ava'; + +import { zipHardhatTron } from './zip-hardhat-tron'; + +import { buildERC20 } from './erc20'; +import { buildERC721 } from './erc721'; +import { buildERC1155 } from './erc1155'; +import { buildGovernor } from './governor'; +import type { Contract } from './contract'; +import type { JSZipObject } from 'jszip'; +import type JSZip from 'jszip'; +import type { GenericOptions } from './build-generic'; + +// The TRON download cannot run `npm install` end-to-end yet because +// @openzeppelin/hardhat-tron and @openzeppelin/tron-contracts are not +// published to npm at the time of this test. These tests therefore only +// verify the file layout and snapshot the contents. + +test.serial('erc20 basic - layout & contents', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc20 full (mintable, pausable, permit, votes, flashmint)', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + premint: '2000', + access: 'roles', + burnable: true, + mintable: true, + pausable: true, + permit: true, + votes: true, + flashmint: true, + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc721 basic', async t => { + const opts: GenericOptions = { + kind: 'ERC721', + name: 'My NFT', + symbol: 'MNFT', + }; + const c = buildERC721(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc1155 basic', async t => { + const opts: GenericOptions = { + kind: 'ERC1155', + name: 'My Multi', + uri: 'ipfs://example/{id}', + }; + const c = buildERC1155(opts); + await runSnapshotTest(c, t, opts); +}); + +// Upgradeable contracts deploy behind a manually-deployed proxy (the OZ Upgrades +// plugins don't target TRON), so the project also ships a `contracts/Proxy.sol`. +test.serial('erc20 uups upgradeable - proxy deploy scaffolding', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + mintable: true, + access: 'ownable', + upgradeable: 'uups', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc20 transparent upgradeable - proxy deploy scaffolding', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + mintable: true, + access: 'ownable', + upgradeable: 'transparent', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('governor uups upgradeable - non-address init args are gated', async t => { + const opts: GenericOptions = { + kind: 'Governor', + name: 'My Governor', + delay: '1 day', + period: '1 week', + votes: 'erc20votes', + timelock: 'openzeppelin', + upgradeable: 'uups', + }; + const c = buildGovernor(opts); + await runSnapshotTest(c, t, opts); +}); + +async function runSnapshotTest(c: Contract, t: ExecutionContext, opts: GenericOptions) { + const zip = await zipHardhatTron(c, opts); + + assertLayout(zip, c, t); + await assertContents(zip, c, t); +} + +function assertLayout(zip: JSZip, c: Contract, t: ExecutionContext) { + const sorted = Object.keys(zip.files).sort(); + const expected = [ + '.gitignore', + 'README.md', + 'contracts/', + `contracts/${c.name}.sol`, + ...(c.upgradeable ? ['contracts/Proxy.sol'] : []), + 'hardhat.config.ts', + 'package.json', + 'scripts/', + 'scripts/deploy.ts', + 'test/', + 'test/test.ts', + 'tsconfig.json', + ].sort(); + t.deepEqual(sorted, expected); +} + +async function assertContents(zip: JSZip, c: Contract, t: ExecutionContext) { + const contentComparison = [ + await getItemString(zip, `contracts/${c.name}.sol`), + ...(c.upgradeable ? [await getItemString(zip, 'contracts/Proxy.sol')] : []), + await getItemString(zip, 'hardhat.config.ts'), + await getItemString(zip, 'package.json'), + await getItemString(zip, 'scripts/deploy.ts'), + await getItemString(zip, 'test/test.ts'), + await getItemString(zip, 'README.md'), + await getItemString(zip, '.gitignore'), + ]; + + t.snapshot(contentComparison); +} + +async function getItemString(zip: JSZip, key: string) { + const obj = zip.files[key]; + if (obj === undefined) { + throw Error(`Item ${key} not found in zip`); + } + return `${key}:\n${await asString(obj)}`; +} + +async function asString(item: JSZipObject) { + return Buffer.from(await item.async('arraybuffer')).toString(); +} diff --git a/packages/core/solidity/src/zip-hardhat-tron.test.ts.md b/packages/core/solidity/src/zip-hardhat-tron.test.ts.md new file mode 100644 index 000000000..d079ce894 --- /dev/null +++ b/packages/core/solidity/src/zip-hardhat-tron.test.ts.md @@ -0,0 +1,1427 @@ +# Snapshot report for `src/zip-hardhat-tron.test.ts` + +The actual snapshot is saved in `zip-hardhat-tron.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## erc20 basic - layout & contents + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol";␊ + import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol";␊ + ␊ + contract MyToken is TRC20, TRC20Permit {␊ + constructor() TRC20("My Token", "MTK") TRC20Permit("My Token") {}␊ + }␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + async function main() {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + ␊ + const instance = await ContractFactory.deploy();␊ + await instance.waitForDeployment();␊ + ␊ + console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + const instance = await ContractFactory.deploy();␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.name()).to.equal("My Token");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyToken)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyToken\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## erc20 full (mintable, pausable, permit, votes, flashmint) + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol";␊ + import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol";␊ + import {TRC20Burnable} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Burnable.sol";␊ + import {TRC20FlashMint} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20FlashMint.sol";␊ + import {TRC20Pausable} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Pausable.sol";␊ + import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol";␊ + import {TRC20Votes} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Votes.sol";␊ + import {Nonces} from "@openzeppelin/tron-contracts/utils/Nonces.sol";␊ + ␊ + contract MyToken is TRC20, TRC20Burnable, TRC20Pausable, AccessControl, TRC20Permit, TRC20Votes, TRC20FlashMint {␊ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ + ␊ + constructor(address recipient, address defaultAdmin, address pauser, address minter)␊ + TRC20("My Token", "MTK")␊ + TRC20Permit("My Token")␊ + {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ + _grantRole(PAUSER_ROLE, pauser);␊ + _grantRole(MINTER_ROLE, minter);␊ + }␊ + ␊ + function pause() public onlyRole(PAUSER_ROLE) {␊ + _pause();␊ + }␊ + ␊ + function unpause() public onlyRole(PAUSER_ROLE) {␊ + _unpause();␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {␊ + _mint(to, amount);␊ + }␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function _update(address from, address to, uint256 value)␊ + internal␊ + override(TRC20, TRC20Pausable, TRC20Votes)␊ + {␊ + super._update(from, to, value);␊ + }␊ + ␊ + function nonces(address owner)␊ + public␊ + view␊ + override(TRC20Permit, Nonces)␊ + returns (uint256)␊ + {␊ + return super.nonces(owner);␊ + }␊ + }␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + async function main() {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + // TODO: Set values for the constructor arguments below␊ + const instance = await ContractFactory.deploy(recipient, defaultAdmin, pauser, minter);␊ + await instance.waitForDeployment();␊ + ␊ + console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + const recipient = (await ethers.getSigners())[0].address;␊ + const defaultAdmin = (await ethers.getSigners())[1].address;␊ + const pauser = (await ethers.getSigners())[2].address;␊ + const minter = (await ethers.getSigners())[3].address;␊ + ␊ + const instance = await ContractFactory.deploy(recipient, defaultAdmin, pauser, minter);␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.name()).to.equal("My Token");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyToken)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyToken\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## erc721 basic + +> Snapshot 1 + + [ + `contracts/MyNFT.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol";␊ + ␊ + contract MyNFT is TRC721 {␊ + constructor() TRC721("My NFT", "MNFT") {}␊ + }␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + async function main() {␊ + const ContractFactory = await ethers.getContractFactory("MyNFT");␊ + ␊ + ␊ + const instance = await ContractFactory.deploy();␊ + await instance.waitForDeployment();␊ + ␊ + console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyNFT", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyNFT");␊ + ␊ + const instance = await ContractFactory.deploy();␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.name()).to.equal("My NFT");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyNFT)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyNFT\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## erc1155 basic + +> Snapshot 1 + + [ + `contracts/MyMulti.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {Ownable} from "@openzeppelin/tron-contracts/access/Ownable.sol";␊ + import {TRC1155} from "@openzeppelin/tron-contracts/token/TRC1155/TRC1155.sol";␊ + ␊ + contract MyMulti is TRC1155, Ownable {␊ + constructor(address initialOwner)␊ + TRC1155("ipfs://example/{id}")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + function setURI(string memory newuri) public onlyOwner {␊ + _setURI(newuri);␊ + }␊ + }␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + async function main() {␊ + const ContractFactory = await ethers.getContractFactory("MyMulti");␊ + ␊ + // TODO: Set values for the constructor arguments below␊ + const instance = await ContractFactory.deploy(initialOwner);␊ + await instance.waitForDeployment();␊ + ␊ + console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyMulti", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyMulti");␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + ␊ + const instance = await ContractFactory.deploy(initialOwner);␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.uri(0)).to.equal("ipfs://example/{id}");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyMulti)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyMulti\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## erc20 uups upgradeable - proxy deploy scaffolding + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {OwnableUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/access/OwnableUpgradeable.sol";␊ + import {TRC20Upgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/TRC20Upgradeable.sol";␊ + import {TRC20PermitUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/extensions/TRC20PermitUpgradeable.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + import {UUPSUpgradeable} from "@openzeppelin/tron-contracts/proxy/utils/UUPSUpgradeable.sol";␊ + ␊ + contract MyToken is Initializable, TRC20Upgradeable, OwnableUpgradeable, TRC20PermitUpgradeable, UUPSUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(address initialOwner) public initializer {␊ + __TRC20_init("My Token", "MTK");␊ + __Ownable_init(initialOwner);␊ + __TRC20Permit_init("My Token");␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + ␊ + function _authorizeUpgrade(address newImplementation)␊ + internal␊ + override␊ + onlyOwner␊ + {}␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyToken. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyToken behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TRC1967Proxy} from "@openzeppelin/tron-contracts/proxy/TRC1967/TRC1967Proxy.sol";␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + // OpenZeppelin's Hardhat Upgrades plugin does not support TRON, so this script␊ + // deploys the proxy manually: deploy the implementation, then deploy a␊ + // TRC1967Proxy that delegates to it and runs initialize() atomically.␊ + async function main() {␊ + // 1. Deploy the implementation. It is never called directly — all calls go␊ + // through the proxy — and it cannot be initialized on its own because its␊ + // constructor runs _disableInitializers().␊ + const Implementation = await ethers.getContractFactory("MyToken");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + console.log(\`Implementation deployed to ${implementationAddress}\`);␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + ␊ + // 2. ABI-encode the initializer so it runs in the proxy's storage on deploy.␊ + const initData = Implementation.interface.encodeFunctionData("initialize", [initialOwner]);␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + const Proxy = await ethers.getContractFactory("TRC1967Proxy");␊ + const proxy = await Proxy.deploy(implementationAddress, initData);␊ + await proxy.waitForDeployment();␊ + ␊ + console.log(\`MyToken (proxy) deployed to ${await proxy.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("deploys behind a proxy and initializes", async function () {␊ + const Implementation = await ethers.getContractFactory("MyToken");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + const initData = Implementation.interface.encodeFunctionData("initialize", [initialOwner]);␊ + ␊ + const Proxy = await ethers.getContractFactory("TRC1967Proxy");␊ + const proxy = await Proxy.deploy(implementationAddress, initData);␊ + await proxy.waitForDeployment();␊ + ␊ + // Interact with the proxy through the implementation's ABI.␊ + const instance = await ethers.getContractAt("MyToken", await proxy.getAddress());␊ + expect(await instance.name()).to.equal("My Token");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyToken)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyToken\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's Hardhat Upgrades plugin targets EVM chains and does not deploy to TRON, so \`scripts/deploy.ts\` deploys the proxy by hand: it deploys the \`MyToken\` implementation, then a \`TRC1967Proxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address it prints, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## erc20 transparent upgradeable - proxy deploy scaffolding + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {OwnableUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/access/OwnableUpgradeable.sol";␊ + import {TRC20Upgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/TRC20Upgradeable.sol";␊ + import {TRC20PermitUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/extensions/TRC20PermitUpgradeable.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + ␊ + contract MyToken is Initializable, TRC20Upgradeable, OwnableUpgradeable, TRC20PermitUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(address initialOwner) public initializer {␊ + __TRC20_init("My Token", "MTK");␊ + __Ownable_init(initialOwner);␊ + __TRC20Permit_init("My Token");␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyToken. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyToken behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TransparentUpgradeableProxy} from "@openzeppelin/tron-contracts/proxy/transparent/TransparentUpgradeableProxy.sol";␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + // OpenZeppelin's Hardhat Upgrades plugin does not support TRON, so this script␊ + // deploys the proxy manually: deploy the implementation, then deploy a␊ + // TransparentUpgradeableProxy that delegates to it and runs initialize() atomically.␊ + async function main() {␊ + // 1. Deploy the implementation. It is never called directly — all calls go␊ + // through the proxy — and it cannot be initialized on its own because its␊ + // constructor runs _disableInitializers().␊ + const Implementation = await ethers.getContractFactory("MyToken");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + console.log(\`Implementation deployed to ${implementationAddress}\`);␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + ␊ + // The transparent proxy's admin owner — it alone can upgrade the proxy.␊ + const proxyAdminOwner = (await ethers.getSigners())[0].address;␊ + ␊ + // 2. ABI-encode the initializer so it runs in the proxy's storage on deploy.␊ + const initData = Implementation.interface.encodeFunctionData("initialize", [initialOwner]);␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + const Proxy = await ethers.getContractFactory("TransparentUpgradeableProxy");␊ + const proxy = await Proxy.deploy(implementationAddress, proxyAdminOwner, initData);␊ + await proxy.waitForDeployment();␊ + ␊ + console.log(\`MyToken (proxy) deployed to ${await proxy.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("deploys behind a proxy and initializes", async function () {␊ + const Implementation = await ethers.getContractFactory("MyToken");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + const proxyAdminOwner = (await ethers.getSigners())[0].address;␊ + const initData = Implementation.interface.encodeFunctionData("initialize", [initialOwner]);␊ + ␊ + const Proxy = await ethers.getContractFactory("TransparentUpgradeableProxy");␊ + const proxy = await Proxy.deploy(implementationAddress, proxyAdminOwner, initData);␊ + await proxy.waitForDeployment();␊ + ␊ + // Interact with the proxy through the implementation's ABI.␊ + const instance = await ethers.getContractAt("MyToken", await proxy.getAddress());␊ + expect(await instance.name()).to.equal("My Token");␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyToken)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyToken\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's Hardhat Upgrades plugin targets EVM chains and does not deploy to TRON, so \`scripts/deploy.ts\` deploys the proxy by hand: it deploys the \`MyToken\` implementation, then a \`TransparentUpgradeableProxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address it prints, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] + +## governor uups upgradeable - non-address init args are gated + +> Snapshot 1 + + [ + `contracts/MyGovernor.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {GovernorUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/GovernorUpgradeable.sol";␊ + import {TimelockControllerUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/TimelockControllerUpgradeable.sol";␊ + import {GovernorCountingSimpleUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorCountingSimpleUpgradeable.sol";␊ + import {GovernorSettingsUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol";␊ + import {GovernorTimelockControlUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol";␊ + import {GovernorVotesQuorumFractionUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol";␊ + import {GovernorVotesUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol";␊ + import {IVotes} from "@openzeppelin/tron-contracts/governance/utils/IVotes.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + import {UUPSUpgradeable} from "@openzeppelin/tron-contracts/proxy/utils/UUPSUpgradeable.sol";␊ + ␊ + contract MyGovernor is Initializable, GovernorUpgradeable, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorVotesUpgradeable, GovernorVotesQuorumFractionUpgradeable, GovernorTimelockControlUpgradeable, UUPSUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(IVotes _token, TimelockControllerUpgradeable _timelock)␊ + public␊ + initializer␊ + {␊ + __Governor_init("My Governor");␊ + __GovernorSettings_init(7200 /* 1 day */, 50400 /* 1 week */, 0);␊ + __GovernorCountingSimple_init();␊ + __GovernorVotes_init(_token);␊ + __GovernorVotesQuorumFraction_init(4);␊ + __GovernorTimelockControl_init(_timelock);␊ + }␊ + ␊ + function _authorizeUpgrade(address newImplementation)␊ + internal␊ + override␊ + onlyGovernance␊ + {}␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function state(uint256 proposalId)␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (ProposalState)␊ + {␊ + return super.state(proposalId);␊ + }␊ + ␊ + function proposalNeedsQueuing(uint256 proposalId)␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (bool)␊ + {␊ + return super.proposalNeedsQueuing(proposalId);␊ + }␊ + ␊ + function proposalThreshold()␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorSettingsUpgradeable)␊ + returns (uint256)␊ + {␊ + return super.proposalThreshold();␊ + }␊ + ␊ + function _queueOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (uint48)␊ + {␊ + return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _executeOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + {␊ + super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (uint256)␊ + {␊ + return super._cancel(targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _executor()␊ + internal␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (address)␊ + {␊ + return super._executor();␊ + }␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyGovernor. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyGovernor behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TRC1967Proxy} from "@openzeppelin/tron-contracts/proxy/TRC1967/TRC1967Proxy.sol";␊ + `, + `hardhat.config.ts:␊ + import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-ethers";␊ + import "@nomicfoundation/hardhat-chai-matchers";␊ + import "@openzeppelin/hardhat-tron";␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.26",␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + viaIR: true,␊ + // Embed source as literal text in metadata so verification␊ + // services (Sourcify, etc.) can reconstruct it deterministically.␊ + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },␊ + },␊ + },␊ + tre: {␊ + autoStart: true,␊ + image: 'tronbox/tre:dev',␊ + compiler: { target: 'tron' },␊ + },␊ + defaultNetwork: 'tre',␊ + networks: {␊ + tre: {␊ + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',␊ + tron: true,␊ + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network.␊ + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `package.json:␊ + {␊ + "name": "hardhat-tron-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-chai-matchers": "^2.0.6",␊ + "@nomicfoundation/hardhat-ethers": "^3.0.9",␊ + "@openzeppelin/hardhat-tron": "^0.1.0",␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "ethers": "^6.14.0",␊ + "hardhat": "^2.26.0",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `scripts/deploy.ts:␊ + import { ethers } from "hardhat";␊ + ␊ + // OpenZeppelin's Hardhat Upgrades plugin does not support TRON, so this script␊ + // deploys the proxy manually: deploy the implementation, then deploy a␊ + // TRC1967Proxy that delegates to it and runs initialize() atomically.␊ + async function main() {␊ + // 1. Deploy the implementation. It is never called directly — all calls go␊ + // through the proxy — and it cannot be initialized on its own because its␊ + // constructor runs _disableInitializers().␊ + const Implementation = await ethers.getContractFactory("MyGovernor");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + console.log(\`Implementation deployed to ${implementationAddress}\`);␊ + ␊ + // TODO: Set the initialize() argument "_token".␊ + // const _token = ...;␊ + // TODO: Set the initialize() argument "_timelock".␊ + // const _timelock = ...;␊ + ␊ + // 2. ABI-encode the initializer so it runs in the proxy's storage on deploy.␊ + // TODO: Uncomment the lines below once the initialize() arguments above are set.␊ + // const initData = Implementation.interface.encodeFunctionData("initialize", [_token, _timelock]);␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + // const Proxy = await ethers.getContractFactory("TRC1967Proxy");␊ + // const proxy = await Proxy.deploy(implementationAddress, initData);␊ + // await proxy.waitForDeployment();␊ + ␊ + // console.log(\`MyGovernor (proxy) deployed to ${await proxy.getAddress()}\`);␊ + }␊ + ␊ + // We recommend this pattern to be able to use async/await everywhere␊ + // and properly handle errors.␊ + main().catch((error) => {␊ + console.error(error);␊ + process.exitCode = 1;␊ + });␊ + `, + `test/test.ts:␊ + import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyGovernor", function () {␊ + it("deploys behind a proxy and initializes", async function () {␊ + const Implementation = await ethers.getContractFactory("MyGovernor");␊ + const implementation = await Implementation.deploy();␊ + await implementation.waitForDeployment();␊ + const implementationAddress = await implementation.getAddress();␊ + ␊ + // TODO: Set the initialize() argument "_token".␊ + // const _token = ...;␊ + // TODO: Set the initialize() argument "_timelock".␊ + // const _timelock = ...;␊ + // TODO: Uncomment the lines below once the initialize() arguments above are set.␊ + // const initData = Implementation.interface.encodeFunctionData("initialize", [_token, _timelock]);␊ + ␊ + // const Proxy = await ethers.getContractFactory("TRC1967Proxy");␊ + // const proxy = await Proxy.deploy(implementationAddress, initData);␊ + // await proxy.waitForDeployment();␊ + ␊ + // Interact with the proxy through the implementation's ABI.␊ + // const instance = await ethers.getContractAt("MyGovernor", await proxy.getAddress());␊ + });␊ + });␊ + `, + `README.md:␊ + # Sample TRON Hardhat Project (MyGovernor)␊ + ␊ + This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`MyGovernor\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script.␊ + ␊ + ## Prerequisites␊ + ␊ + Ensure you have the following installed:␊ + - [Node.js 20+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`.␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## TRON Runtime Environment␊ + ␊ + \`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required.␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + npm test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + \`\`\`␊ + npx hardhat run --network tre scripts/deploy.ts␊ + \`\`\`␊ + ␊ + The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's Hardhat Upgrades plugin targets EVM chains and does not deploy to TRON, so \`scripts/deploy.ts\` deploys the proxy by hand: it deploys the \`MyGovernor\` implementation, then a \`TRC1967Proxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address it prints, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + `, + `.gitignore:␊ + node_modules␊ + .env␊ + coverage␊ + coverage.json␊ + typechain␊ + typechain-types␊ + ␊ + # Hardhat files␊ + cache␊ + artifacts␊ + `, + ] diff --git a/packages/core/solidity/src/zip-hardhat-tron.test.ts.snap b/packages/core/solidity/src/zip-hardhat-tron.test.ts.snap new file mode 100644 index 000000000..3befdebb6 Binary files /dev/null and b/packages/core/solidity/src/zip-hardhat-tron.test.ts.snap differ diff --git a/packages/core/solidity/src/zip-hardhat-tron.ts b/packages/core/solidity/src/zip-hardhat-tron.ts new file mode 100644 index 000000000..026dc1d4d --- /dev/null +++ b/packages/core/solidity/src/zip-hardhat-tron.ts @@ -0,0 +1,316 @@ +import JSZip from 'jszip'; +import type { Contract } from './contract'; +import { HardhatZipGenerator } from './zip-hardhat'; +import type { GenericOptions } from './build-generic'; +import { printContract } from './print'; +import { tronPrintProfile, TRON_SOLIDITY_VERSION } from './utils/transform-tron'; +import { tronProxyFor, tronProxyHelperSource, hasUnsetInitArgs } from './utils/tron-upgradeable'; + +// `tron-solc` (TRON_SOLIDITY_VERSION) + cancun + viaIR is what the TRON +// Democritus hardfork (post-GreatVoyage 4.7) targets, and matches the README of +// OpenZeppelin/hardhat-tron. Sourced from the same constant as the printed pragma. + +class HardhatTronZipGenerator extends HardhatZipGenerator { + protected override getAdditionalHardhatImports(): string[] { + // hardhat-tron does NOT use @nomicfoundation/hardhat-toolbox; the plugin + // composes the smaller ethers + chai-matchers plugins that it actually needs. + return ['@nomicfoundation/hardhat-ethers', '@nomicfoundation/hardhat-chai-matchers', '@openzeppelin/hardhat-tron']; + } + + protected override getHardhatConfigJsonString(): string { + return `\ +{ + solidity: { + version: "${TRON_SOLIDITY_VERSION}", + settings: { + optimizer: { enabled: true, runs: 200 }, + evmVersion: 'cancun', + viaIR: true, + // Embed source as literal text in metadata so verification + // services (Sourcify, etc.) can reconstruct it deterministically. + metadata: { bytecodeHash: 'ipfs', useLiteralContent: true }, + }, + }, + tre: { + autoStart: true, + image: 'tronbox/tre:dev', + compiler: { target: 'tron' }, + }, + defaultNetwork: 'tre', + networks: { + tre: { + url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc', + tron: true, + // Default well-known TRE dev key — fine for local tests, NEVER use on a real network. + accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'], + }, + }, +}`; + } + + protected override getHardhatConfig(_upgradeable: boolean): string { + // hardhat-tron-based projects use a non-upgradeable single config; the + // `@openzeppelin/hardhat-upgrades` plugin is not used here. The hardhat + // config is also emitted as a CommonJS .cjs file in the README sample, + // but the TypeScript .ts variant works just as well with hardhat-tron. + const additionalImports = this.getAdditionalHardhatImports(); + const importsSection = additionalImports.map(imp => `import "${imp}";`).join('\n'); + + return `\ +import { HardhatUserConfig } from "hardhat/config"; +${importsSection} + +const config: HardhatUserConfig = ${this.getHardhatConfigJsonString()}; + +export default config; +`; + } + + protected override async getPackageJson(c: Contract): Promise { + const { default: packageJson } = await import('./environments/hardhat/tron/package.json'); + // Build a fresh object so we never mutate the shared module-level import. + const devDependencies: Record = { ...packageJson.devDependencies }; + if (c.upgradeable) { + // Upgradeable contracts pull their transpiled `*Upgradeable` parents from + // tron-contracts-upgradeable; tron-contracts (already present) stays on as + // its peer for the proxy utilities and interfaces. + devDependencies['@openzeppelin/tron-contracts-upgradeable'] = '^0.0.1'; + } + return { ...packageJson, license: c.license, devDependencies }; + } + + protected override async getPackageLock(_c: Contract): Promise { + // Not used. The TRON variant skips emitting a package-lock.json because + // @openzeppelin/hardhat-tron and @openzeppelin/tron-contracts are not yet + // published to the npm registry, so a lockfile would dangle. `npm install` + // resolves the dependencies fresh when the packages are available. + return undefined; + } + + protected override getReadmePrerequisitesSection(): string { + return `\ +## Prerequisites + +Ensure you have the following installed: +- [Node.js 20+](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/get-docker/) — the TRON Runtime Environment (\`tronbox/tre:dev\`) is spawned as a Docker container by \`@openzeppelin/hardhat-tron\`. + +`; + } + + protected override getReadmeTestingEnvironmentSetupSection(): string { + return `\ +## TRON Runtime Environment + +\`@openzeppelin/hardhat-tron\` spawns a local \`java-tron\` node (TRE) in Docker the first time you run \`npx hardhat test\` and tears it down on exit. No manual setup is required. + +`; + } + + protected override getGitIgnoreHardhatIgnition(): string { + // hardhat-ignition is not in scope for the TRON variant — we emit a plain + // ethers script instead. Nothing to gitignore here. + return ''; + } + + protected override getPrintContract(c: Contract): string { + return printContract(c, tronPrintProfile); + } + + protected override getReadme(c: Contract): string { + return `\ +# Sample TRON Hardhat Project (${c.name}) + +This project demonstrates a TRON-targeted Hardhat use case using \`@openzeppelin/hardhat-tron\`. It comes with the \`${c.name}\` contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a deploy script. + +${this.getReadmePrerequisitesSection()}## Installing dependencies + +> :warning: Temporary limitation: this template depends on \`@openzeppelin/hardhat-tron\` and \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for either package, retry after they are published, or install them from a local checkout / git URL in the meantime. + +\`\`\` +npm install +\`\`\` + +${this.getReadmeTestingEnvironmentSetupSection()}## Testing the contract + +\`\`\` +npm test +\`\`\` + +## Deploying the contract + +\`\`\` +npx hardhat run --network tre scripts/deploy.ts +\`\`\` + +The default \`tre\` network in \`hardhat.config.ts\` points at a local TRON Runtime Environment (Docker container, spawned automatically by \`@openzeppelin/hardhat-tron\`). For Shasta, Nile, or mainnet, add a network entry and pass \`--network \`. +${ + c.upgradeable + ? ` +> :information_source: This is an upgradeable contract. OpenZeppelin's Hardhat Upgrades plugin targets EVM chains and does not deploy to TRON, so \`scripts/deploy.ts\` deploys the proxy by hand: it deploys the \`${c.name}\` implementation, then a \`${tronProxyFor(c).contractName}\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address it prints, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable). +` + : '' +}`; + } + + // hardhat-tron does not bundle `@openzeppelin/hardhat-upgrades` (the Upgrades + // plugins don't support TRON), so the deploy script/test never reference an + // `upgrades` object. Override the base list to drop it even when upgradeable. + public override getHardhatPlugins(_c: Contract): string[] { + return ['ethers']; + } + + protected override getScript(c: Contract): string { + // For upgradeable contracts we deploy the proxy by hand (see header note on + // `tron-upgradeable.ts`); the base `upgrades.deployProxy` flow is EVM-only. + return c.upgradeable ? this.getUpgradeableScript(c) : super.getScript(c); + } + + protected override getTest(c: Contract, opts?: GenericOptions): string { + return c.upgradeable ? this.getUpgradeableTest(c, opts) : super.getTest(c, opts); + } + + // Declares the `initialize(...)` arguments above the proxy deployment. Address + // args default to a local signer; non-address args are left as commented-out + // TODOs (there's no safe default), which also flips `hasUnsetInitArgs`. + private declareInitArgs(c: Contract): string[] { + return c.constructorArgs.flatMap((arg, i) => { + if (arg.type === 'address') { + return [` const ${arg.name} = (await ethers.getSigners())[${i}].address;`]; + } + return [` // TODO: Set the initialize() argument "${arg.name}".`, ` // const ${arg.name} = ...;`]; + }); + } + + private getUpgradeableScript(c: Contract): string { + const proxy = tronProxyFor(c); + const gated = hasUnsetInitArgs(c); + const g = gated ? '// ' : ''; + + const argDecls = this.declareInitArgs(c); + const argList = c.constructorArgs.map(a => a.name).join(', '); + const adminDecl = proxy.isTransparent + ? ` // The transparent proxy's admin owner — it alone can upgrade the proxy.\n const proxyAdminOwner = (await ethers.getSigners())[0].address;\n\n` + : ''; + const proxyArgs = proxy.isTransparent + ? 'implementationAddress, proxyAdminOwner, initData' + : 'implementationAddress, initData'; + + return `\ +import { ethers } from "hardhat"; + +// OpenZeppelin's Hardhat Upgrades plugin does not support TRON, so this script +// deploys the proxy manually: deploy the implementation, then deploy a +// ${proxy.contractName} that delegates to it and runs initialize() atomically. +async function main() { + // 1. Deploy the implementation. It is never called directly — all calls go + // through the proxy — and it cannot be initialized on its own because its + // constructor runs _disableInitializers(). + const Implementation = await ethers.getContractFactory("${c.name}"); + const implementation = await Implementation.deploy(); + await implementation.waitForDeployment(); + const implementationAddress = await implementation.getAddress(); + console.log(\`Implementation deployed to \${implementationAddress}\`); + +${argDecls.length > 0 ? argDecls.join('\n') + '\n\n' : ''}${adminDecl} // 2. ABI-encode the initializer so it runs in the proxy's storage on deploy. +${gated ? ' // TODO: Uncomment the lines below once the initialize() arguments above are set.\n' : ''} ${g}const initData = Implementation.interface.encodeFunctionData("initialize", [${argList}]); + + // 3. Deploy the proxy pointing at the implementation. + ${g}const Proxy = await ethers.getContractFactory("${proxy.contractName}"); + ${g}const proxy = await Proxy.deploy(${proxyArgs}); + ${g}await proxy.waitForDeployment(); + + ${g}console.log(\`${c.name} (proxy) deployed to \${await proxy.getAddress()}\`); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); +`; + } + + private getUpgradeableTest(c: Contract, opts?: GenericOptions): string { + const proxy = tronProxyFor(c); + const gated = hasUnsetInitArgs(c); + const g = gated ? '// ' : ''; + + const argDecls = this.declareInitArgs(c).map(line => ' ' + line); + const argList = c.constructorArgs.map(a => a.name).join(', '); + const adminDecl = proxy.isTransparent + ? ` const proxyAdminOwner = (await ethers.getSigners())[0].address;\n` + : ''; + const proxyArgs = proxy.isTransparent + ? 'implementationAddress, proxyAdminOwner, initData' + : 'implementationAddress, initData'; + + let assertion = ''; + if (opts !== undefined) { + switch (opts.kind) { + case 'ERC20': + case 'ERC721': + assertion = ` ${g}expect(await instance.name()).to.equal(${JSON.stringify(opts.name)});`; + break; + case 'ERC1155': + assertion = ` ${g}expect(await instance.uri(0)).to.equal(${JSON.stringify(opts.uri)});`; + break; + default: + break; + } + } + + return `\ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("${c.name}", function () { + it("deploys behind a proxy and initializes", async function () { + const Implementation = await ethers.getContractFactory("${c.name}"); + const implementation = await Implementation.deploy(); + await implementation.waitForDeployment(); + const implementationAddress = await implementation.getAddress(); + +${argDecls.length > 0 ? argDecls.join('\n') + '\n' : ''}${adminDecl}${gated ? ' // TODO: Uncomment the lines below once the initialize() arguments above are set.\n' : ''} ${g}const initData = Implementation.interface.encodeFunctionData("initialize", [${argList}]); + + ${g}const Proxy = await ethers.getContractFactory("${proxy.contractName}"); + ${g}const proxy = await Proxy.deploy(${proxyArgs}); + ${g}await proxy.waitForDeployment(); + + // Interact with the proxy through the implementation's ABI. + ${g}const instance = await ethers.getContractAt("${c.name}", await proxy.getAddress()); +${assertion ? assertion + '\n' : ''} }); +}); +`; + } + + override async zipHardhat(c: Contract, opts?: GenericOptions): Promise { + const zip = new JSZip(); + + const packageJson = await this.getPackageJson(c); + + zip.file(`contracts/${c.name}.sol`, this.getPrintContract(c)); + if (c.upgradeable) { + // Pull the proxy into the build so the deploy script can load its artifact. + zip.file('contracts/Proxy.sol', tronProxyHelperSource(c, TRON_SOLIDITY_VERSION)); + } + zip.file('test/test.ts', this.getTest(c, opts)); + // No hardhat-ignition: the TRON dev flow is a plain ethers deploy script, + // matching the @openzeppelin/hardhat-tron README. + zip.file('scripts/deploy.ts', this.getScript(c)); + + zip.file('.gitignore', this.getGitIgnore()); + zip.file('hardhat.config.ts', this.getHardhatConfig(c.upgradeable)); + zip.file('package.json', JSON.stringify(packageJson, null, 2)); + // package-lock.json is intentionally omitted; see getPackageLock(). + zip.file('README.md', this.getReadme(c)); + zip.file('tsconfig.json', this.getTsConfig()); + + return zip; + } +} + +export async function zipHardhatTron(c: Contract, opts?: GenericOptions): Promise { + return new HardhatTronZipGenerator().zipHardhat(c, opts); +} diff --git a/packages/core/solidity/src/zip-tronbox.test.ts b/packages/core/solidity/src/zip-tronbox.test.ts new file mode 100644 index 000000000..af6fa060b --- /dev/null +++ b/packages/core/solidity/src/zip-tronbox.test.ts @@ -0,0 +1,159 @@ +import type { ExecutionContext } from 'ava'; +import test from 'ava'; + +import { zipTronbox } from './zip-tronbox'; + +import { buildERC20 } from './erc20'; +import { buildERC721 } from './erc721'; +import { buildERC1155 } from './erc1155'; +import { buildGovernor } from './governor'; +import type { Contract } from './contract'; +import type { JSZipObject } from 'jszip'; +import type JSZip from 'jszip'; +import type { GenericOptions } from './build-generic'; + +// TronBox is not installed as part of the wizard tests (the binary requires +// global install and a local TRE Docker container). These tests therefore +// only verify file layout and snapshot the contents. + +test.serial('erc20 basic', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc20 mintable+burnable+ownable', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + premint: '1000', + access: 'ownable', + burnable: true, + mintable: true, + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc721 basic', async t => { + const opts: GenericOptions = { + kind: 'ERC721', + name: 'My NFT', + symbol: 'MNFT', + }; + const c = buildERC721(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc1155 basic', async t => { + const opts: GenericOptions = { + kind: 'ERC1155', + name: 'My Multi', + uri: 'ipfs://example/{id}', + }; + const c = buildERC1155(opts); + await runSnapshotTest(c, t, opts); +}); + +// Upgradeable contracts deploy behind a manually-deployed proxy (the OZ Upgrades +// plugins don't target TRON), so the project also ships a `contracts/Proxy.sol`. +test.serial('erc20 uups upgradeable - proxy migration scaffolding', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + mintable: true, + access: 'ownable', + upgradeable: 'uups', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('erc20 transparent upgradeable - proxy migration scaffolding', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My Token', + symbol: 'MTK', + mintable: true, + access: 'ownable', + upgradeable: 'transparent', + }; + const c = buildERC20(opts); + await runSnapshotTest(c, t, opts); +}); + +test.serial('governor uups upgradeable - non-address init args are gated', async t => { + const opts: GenericOptions = { + kind: 'Governor', + name: 'My Governor', + delay: '1 day', + period: '1 week', + votes: 'erc20votes', + timelock: 'openzeppelin', + upgradeable: 'uups', + }; + const c = buildGovernor(opts); + await runSnapshotTest(c, t, opts); +}); + +async function runSnapshotTest(c: Contract, t: ExecutionContext, opts: GenericOptions) { + const zip = await zipTronbox(c, opts); + + assertLayout(zip, c, t); + await assertContents(zip, c, t); +} + +function assertLayout(zip: JSZip, c: Contract, t: ExecutionContext) { + const sorted = Object.keys(zip.files).sort(); + const expected = [ + '.gitignore', + 'README.md', + 'contracts/', + 'contracts/Migrations.sol', + `contracts/${c.name}.sol`, + ...(c.upgradeable ? ['contracts/Proxy.sol'] : []), + 'migrations/', + 'migrations/1_initial_migration.js', + `migrations/2_deploy_${c.name}.js`, + 'package.json', + 'test/', + `test/${c.name}.js`, + 'tronbox-config.js', + ].sort(); + t.deepEqual(sorted, expected); +} + +async function assertContents(zip: JSZip, c: Contract, t: ExecutionContext) { + const contentComparison = [ + await getItemString(zip, `contracts/${c.name}.sol`), + await getItemString(zip, 'contracts/Migrations.sol'), + ...(c.upgradeable ? [await getItemString(zip, 'contracts/Proxy.sol')] : []), + await getItemString(zip, 'migrations/1_initial_migration.js'), + await getItemString(zip, `migrations/2_deploy_${c.name}.js`), + await getItemString(zip, `test/${c.name}.js`), + await getItemString(zip, 'tronbox-config.js'), + await getItemString(zip, 'package.json'), + await getItemString(zip, 'README.md'), + await getItemString(zip, '.gitignore'), + ]; + + t.snapshot(contentComparison); +} + +async function getItemString(zip: JSZip, key: string) { + const obj = zip.files[key]; + if (obj === undefined) { + throw Error(`Item ${key} not found in zip`); + } + return `${key}:\n${await asString(obj)}`; +} + +async function asString(item: JSZipObject) { + return Buffer.from(await item.async('arraybuffer')).toString(); +} diff --git a/packages/core/solidity/src/zip-tronbox.test.ts.md b/packages/core/solidity/src/zip-tronbox.test.ts.md new file mode 100644 index 000000000..326c5f7a4 --- /dev/null +++ b/packages/core/solidity/src/zip-tronbox.test.ts.md @@ -0,0 +1,1683 @@ +# Snapshot report for `src/zip-tronbox.test.ts` + +The actual snapshot is saved in `zip-tronbox.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## erc20 basic + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol";␊ + import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol";␊ + ␊ + contract MyToken is TRC20, TRC20Permit {␊ + constructor() TRC20("My Token", "MTK") TRC20Permit("My Token") {}␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(MyToken);␊ + };␊ + `, + `test/MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network.␊ + contract('MyToken', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + instance = await MyToken.deployed();␊ + });␊ + ␊ + it('is deployed', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Contract address should be defined');␊ + });␊ + ␊ + it('sets the expected name', async function () {␊ + assert.equal(await instance.name(), "My Token");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyToken.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## erc20 mintable+burnable+ownable + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {Ownable} from "@openzeppelin/tron-contracts/access/Ownable.sol";␊ + import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol";␊ + import {TRC20Burnable} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Burnable.sol";␊ + import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol";␊ + ␊ + contract MyToken is TRC20, TRC20Burnable, Ownable, TRC20Permit {␊ + constructor(address recipient, address initialOwner)␊ + TRC20("My Token", "MTK")␊ + Ownable(initialOwner)␊ + TRC20Permit("My Token")␊ + {␊ + _mint(recipient, 1000 * 10 ** decimals());␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + ␊ + module.exports = function (deployer) {␊ + // TODO: Replace with a real address (e.g. tronWeb.defaultAddress.base58).␊ + const recipient = '';␊ + // TODO: Replace with a real address (e.g. tronWeb.defaultAddress.base58).␊ + const initialOwner = '';␊ + ␊ + deployer.deploy(MyToken, recipient, initialOwner);␊ + };␊ + `, + `test/MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network.␊ + // NOTE: this contract has constructor arguments. Update the placeholders in␊ + // migrations/2_deploy_MyToken.js before running 'tronbox test'.␊ + contract('MyToken', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + instance = await MyToken.deployed();␊ + });␊ + ␊ + it('is deployed', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Contract address should be defined');␊ + });␊ + ␊ + it('sets the expected name', async function () {␊ + assert.equal(await instance.name(), "My Token");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyToken.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## erc721 basic + +> Snapshot 1 + + [ + `contracts/MyNFT.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol";␊ + ␊ + contract MyNFT is TRC721 {␊ + constructor() TRC721("My NFT", "MNFT") {}␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyNFT.js:␊ + const MyNFT = artifacts.require('./MyNFT.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(MyNFT);␊ + };␊ + `, + `test/MyNFT.js:␊ + const MyNFT = artifacts.require('./MyNFT.sol');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network.␊ + contract('MyNFT', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + instance = await MyNFT.deployed();␊ + });␊ + ␊ + it('is deployed', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Contract address should be defined');␊ + });␊ + ␊ + it('sets the expected name', async function () {␊ + assert.equal(await instance.name(), "My NFT");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyNFT.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## erc1155 basic + +> Snapshot 1 + + [ + `contracts/MyMulti.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {Ownable} from "@openzeppelin/tron-contracts/access/Ownable.sol";␊ + import {TRC1155} from "@openzeppelin/tron-contracts/token/TRC1155/TRC1155.sol";␊ + ␊ + contract MyMulti is TRC1155, Ownable {␊ + constructor(address initialOwner)␊ + TRC1155("ipfs://example/{id}")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + function setURI(string memory newuri) public onlyOwner {␊ + _setURI(newuri);␊ + }␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyMulti.js:␊ + const MyMulti = artifacts.require('./MyMulti.sol');␊ + ␊ + module.exports = function (deployer) {␊ + // TODO: Replace with a real address (e.g. tronWeb.defaultAddress.base58).␊ + const initialOwner = '';␊ + ␊ + deployer.deploy(MyMulti, initialOwner);␊ + };␊ + `, + `test/MyMulti.js:␊ + const MyMulti = artifacts.require('./MyMulti.sol');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network.␊ + // NOTE: this contract has constructor arguments. Update the placeholders in␊ + // migrations/2_deploy_MyMulti.js before running 'tronbox test'.␊ + contract('MyMulti', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + instance = await MyMulti.deployed();␊ + });␊ + ␊ + it('is deployed', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Contract address should be defined');␊ + });␊ + ␊ + it('sets the expected URI', async function () {␊ + assert.equal(await instance.uri(0), "ipfs://example/{id}");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyMulti.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## erc20 uups upgradeable - proxy migration scaffolding + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {OwnableUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/access/OwnableUpgradeable.sol";␊ + import {TRC20Upgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/TRC20Upgradeable.sol";␊ + import {TRC20PermitUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/extensions/TRC20PermitUpgradeable.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + import {UUPSUpgradeable} from "@openzeppelin/tron-contracts/proxy/utils/UUPSUpgradeable.sol";␊ + ␊ + contract MyToken is Initializable, TRC20Upgradeable, OwnableUpgradeable, TRC20PermitUpgradeable, UUPSUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(address initialOwner) public initializer {␊ + __TRC20_init("My Token", "MTK");␊ + __Ownable_init(initialOwner);␊ + __TRC20Permit_init("My Token");␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + ␊ + function _authorizeUpgrade(address newImplementation)␊ + internal␊ + override␊ + onlyOwner␊ + {}␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyToken. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyToken behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TRC1967Proxy} from "@openzeppelin/tron-contracts/proxy/TRC1967/TRC1967Proxy.sol";␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + const TRC1967Proxy = artifacts.require('TRC1967Proxy');␊ + ␊ + // OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to␊ + // TRON, so this migration deploys the proxy by hand: deploy the implementation,␊ + // then a TRC1967Proxy that delegates to it and runs initialize()␊ + // atomically. Interact with the proxy address, never the implementation.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + module.exports = async function (deployer, network, accounts) {␊ + // 1. Deploy the implementation. It is never called directly and cannot be␊ + // initialized on its own (its constructor runs _disableInitializers()).␊ + await deployer.deploy(MyToken);␊ + const implementation = await MyToken.deployed();␊ + ␊ + const initialOwner = accounts[0];␊ + ␊ + // 2. ABI-encode the initializer call. TronBox is Truffle-derived; if your␊ + // version doesn't expose \`.contract.methods\`, encode the initialize(...)␊ + // call with tronWeb's ABI utilities instead.␊ + const initData = implementation.contract.methods.initialize(initialOwner).encodeABI();␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + await deployer.deploy(TRC1967Proxy, implementation.address, initData);␊ + };␊ + `, + `test/MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + const TRC1967Proxy = artifacts.require('TRC1967Proxy');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network. The migration␊ + // must have deployed the proxy (fill in any initialize() arguments first).␊ + contract('MyToken', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + // Interact with the proxy address using the implementation's ABI.␊ + const proxy = await TRC1967Proxy.deployed();␊ + instance = await MyToken.at(proxy.address);␊ + });␊ + ␊ + it('is deployed behind a proxy', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Proxy address should be defined');␊ + });␊ + ␊ + it('sets the expected name', async function () {␊ + assert.equal(await instance.name(), "My Token");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to TRON, so \`migrations/2_deploy_MyToken.js\` deploys the proxy by hand: it deploys the \`MyToken\` implementation, then a \`TRC1967Proxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyToken.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## erc20 transparent upgradeable - proxy migration scaffolding + +> Snapshot 1 + + [ + `contracts/MyToken.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {OwnableUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/access/OwnableUpgradeable.sol";␊ + import {TRC20Upgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/TRC20Upgradeable.sol";␊ + import {TRC20PermitUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC20/extensions/TRC20PermitUpgradeable.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + ␊ + contract MyToken is Initializable, TRC20Upgradeable, OwnableUpgradeable, TRC20PermitUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(address initialOwner) public initializer {␊ + __TRC20_init("My Token", "MTK");␊ + __Ownable_init(initialOwner);␊ + __TRC20Permit_init("My Token");␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyToken. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyToken behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TransparentUpgradeableProxy} from "@openzeppelin/tron-contracts/proxy/transparent/TransparentUpgradeableProxy.sol";␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');␊ + ␊ + // OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to␊ + // TRON, so this migration deploys the proxy by hand: deploy the implementation,␊ + // then a TransparentUpgradeableProxy that delegates to it and runs initialize()␊ + // atomically. Interact with the proxy address, never the implementation.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + module.exports = async function (deployer, network, accounts) {␊ + // 1. Deploy the implementation. It is never called directly and cannot be␊ + // initialized on its own (its constructor runs _disableInitializers()).␊ + await deployer.deploy(MyToken);␊ + const implementation = await MyToken.deployed();␊ + ␊ + const initialOwner = accounts[0];␊ + ␊ + const proxyAdminOwner = accounts[0];␊ + ␊ + // 2. ABI-encode the initializer call. TronBox is Truffle-derived; if your␊ + // version doesn't expose \`.contract.methods\`, encode the initialize(...)␊ + // call with tronWeb's ABI utilities instead.␊ + const initData = implementation.contract.methods.initialize(initialOwner).encodeABI();␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + await deployer.deploy(TransparentUpgradeableProxy, implementation.address, proxyAdminOwner, initData);␊ + };␊ + `, + `test/MyToken.js:␊ + const MyToken = artifacts.require('./MyToken.sol');␊ + const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network. The migration␊ + // must have deployed the proxy (fill in any initialize() arguments first).␊ + contract('MyToken', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + // Interact with the proxy address using the implementation's ABI.␊ + const proxy = await TransparentUpgradeableProxy.deployed();␊ + instance = await MyToken.at(proxy.address);␊ + });␊ + ␊ + it('is deployed behind a proxy', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Proxy address should be defined');␊ + });␊ + ␊ + it('sets the expected name', async function () {␊ + assert.equal(await instance.name(), "My Token");␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to TRON, so \`migrations/2_deploy_MyToken.js\` deploys the proxy by hand: it deploys the \`MyToken\` implementation, then a \`TransparentUpgradeableProxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyToken.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] + +## governor uups upgradeable - non-address init args are gated + +> Snapshot 1 + + [ + `contracts/MyGovernor.sol:␊ + // SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.26;␊ + ␊ + import {GovernorUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/GovernorUpgradeable.sol";␊ + import {TimelockControllerUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/TimelockControllerUpgradeable.sol";␊ + import {GovernorCountingSimpleUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorCountingSimpleUpgradeable.sol";␊ + import {GovernorSettingsUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol";␊ + import {GovernorTimelockControlUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol";␊ + import {GovernorVotesQuorumFractionUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol";␊ + import {GovernorVotesUpgradeable} from "@openzeppelin/tron-contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol";␊ + import {IVotes} from "@openzeppelin/tron-contracts/governance/utils/IVotes.sol";␊ + import {Initializable} from "@openzeppelin/tron-contracts/proxy/utils/Initializable.sol";␊ + import {UUPSUpgradeable} from "@openzeppelin/tron-contracts/proxy/utils/UUPSUpgradeable.sol";␊ + ␊ + contract MyGovernor is Initializable, GovernorUpgradeable, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorVotesUpgradeable, GovernorVotesQuorumFractionUpgradeable, GovernorTimelockControlUpgradeable, UUPSUpgradeable {␊ + /// @custom:oz-upgrades-unsafe-allow constructor␊ + constructor() {␊ + _disableInitializers();␊ + }␊ + ␊ + function initialize(IVotes _token, TimelockControllerUpgradeable _timelock)␊ + public␊ + initializer␊ + {␊ + __Governor_init("My Governor");␊ + __GovernorSettings_init(7200 /* 1 day */, 50400 /* 1 week */, 0);␊ + __GovernorCountingSimple_init();␊ + __GovernorVotes_init(_token);␊ + __GovernorVotesQuorumFraction_init(4);␊ + __GovernorTimelockControl_init(_timelock);␊ + }␊ + ␊ + function _authorizeUpgrade(address newImplementation)␊ + internal␊ + override␊ + onlyGovernance␊ + {}␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function state(uint256 proposalId)␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (ProposalState)␊ + {␊ + return super.state(proposalId);␊ + }␊ + ␊ + function proposalNeedsQueuing(uint256 proposalId)␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (bool)␊ + {␊ + return super.proposalNeedsQueuing(proposalId);␊ + }␊ + ␊ + function proposalThreshold()␊ + public␊ + view␊ + override(GovernorUpgradeable, GovernorSettingsUpgradeable)␊ + returns (uint256)␊ + {␊ + return super.proposalThreshold();␊ + }␊ + ␊ + function _queueOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (uint48)␊ + {␊ + return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _executeOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + {␊ + super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)␊ + internal␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (uint256)␊ + {␊ + return super._cancel(targets, values, calldatas, descriptionHash);␊ + }␊ + ␊ + function _executor()␊ + internal␊ + view␊ + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)␊ + returns (address)␊ + {␊ + return super._executor();␊ + }␊ + }␊ + `, + `contracts/Migrations.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + contract Migrations {␊ + address public owner = msg.sender;␊ + uint public last_completed_migration;␊ + ␊ + modifier restricted() {␊ + require(msg.sender == owner, "Restricted to owner");␊ + _;␊ + }␊ + ␊ + function setCompleted(uint completed) public restricted {␊ + last_completed_migration = completed;␊ + }␊ + }␊ + `, + `contracts/Proxy.sol:␊ + // SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.26;␊ + ␊ + // This file is NOT imported by MyGovernor. It exists only so the toolchain␊ + // compiles the proxy contract that the deploy script puts MyGovernor behind.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + import {TRC1967Proxy} from "@openzeppelin/tron-contracts/proxy/TRC1967/TRC1967Proxy.sol";␊ + `, + `migrations/1_initial_migration.js:␊ + const Migrations = artifacts.require('./Migrations.sol');␊ + ␊ + module.exports = function (deployer) {␊ + deployer.deploy(Migrations);␊ + };␊ + `, + `migrations/2_deploy_MyGovernor.js:␊ + const MyGovernor = artifacts.require('./MyGovernor.sol');␊ + const TRC1967Proxy = artifacts.require('TRC1967Proxy');␊ + ␊ + // OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to␊ + // TRON, so this migration deploys the proxy by hand: deploy the implementation,␊ + // then a TRC1967Proxy that delegates to it and runs initialize()␊ + // atomically. Interact with the proxy address, never the implementation.␊ + // See https://github.com/OpenZeppelin/tron-contracts-upgradeable␊ + module.exports = async function (deployer, network, accounts) {␊ + // 1. Deploy the implementation. It is never called directly and cannot be␊ + // initialized on its own (its constructor runs _disableInitializers()).␊ + await deployer.deploy(MyGovernor);␊ + const implementation = await MyGovernor.deployed();␊ + ␊ + // TODO: Set the initialize() argument "_token".␊ + // const _token = ...;␊ + // TODO: Set the initialize() argument "_timelock".␊ + // const _timelock = ...;␊ + ␊ + // 2. ABI-encode the initializer call. TronBox is Truffle-derived; if your␊ + // version doesn't expose \`.contract.methods\`, encode the initialize(...)␊ + // call with tronWeb's ABI utilities instead.␊ + // TODO: Uncomment the lines below once the initialize() arguments above are set.␊ + // const initData = implementation.contract.methods.initialize(_token, _timelock).encodeABI();␊ + ␊ + // 3. Deploy the proxy pointing at the implementation.␊ + // await deployer.deploy(TRC1967Proxy, implementation.address, initData);␊ + };␊ + `, + `test/MyGovernor.js:␊ + const MyGovernor = artifacts.require('./MyGovernor.sol');␊ + const TRC1967Proxy = artifacts.require('TRC1967Proxy');␊ + ␊ + // These tests require TronBox >= 4.1.x and the TronBox Runtime Environment␊ + // (https://hub.docker.com/r/tronbox/tre) as your private network. The migration␊ + // must have deployed the proxy (fill in any initialize() arguments first).␊ + contract('MyGovernor', function (accounts) {␊ + let instance;␊ + ␊ + before(async function () {␊ + // Interact with the proxy address using the implementation's ABI.␊ + const proxy = await TRC1967Proxy.deployed();␊ + instance = await MyGovernor.at(proxy.address);␊ + });␊ + ␊ + it('is deployed behind a proxy', async function () {␊ + assert.isTrue(accounts.length >= 1, 'At least one account is required.');␊ + assert.isOk(instance.address, 'Proxy address should be defined');␊ + });␊ + });␊ + `, + `tronbox-config.js:␊ + // tronbox-config.js␊ + //␊ + // TronBox configuration for projects targeting TRON. Run with one of:␊ + //␊ + // tronbox migrate --network development # local TRE in Docker␊ + // tronbox migrate --network shasta # Shasta testnet␊ + // tronbox migrate --network nile # Nile testnet␊ + // tronbox migrate --network mainnet # TRON mainnet␊ + //␊ + // Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying␊ + // to any non-development network.␊ + ␊ + module.exports = {␊ + networks: {␊ + development: {␊ + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre␊ + privateKey: '0000000000000000000000000000000000000000000000000000000000000001',␊ + userFeePercentage: 0,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'http://127.0.0.1:9090',␊ + network_id: '9',␊ + },␊ + shasta: {␊ + privateKey: process.env.PRIVATE_KEY_SHASTA,␊ + userFeePercentage: 50,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.shasta.trongrid.io',␊ + network_id: '2',␊ + },␊ + nile: {␊ + privateKey: process.env.PRIVATE_KEY_NILE,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://nile.trongrid.io',␊ + network_id: '3',␊ + },␊ + mainnet: {␊ + privateKey: process.env.PRIVATE_KEY_MAINNET,␊ + userFeePercentage: 100,␊ + feeLimit: 1000 * 1e6,␊ + fullHost: 'https://api.trongrid.io',␊ + network_id: '1',␊ + },␊ + },␊ + compilers: {␊ + solc: {␊ + version: '0.8.26',␊ + settings: {␊ + optimizer: { enabled: true, runs: 200 },␊ + evmVersion: 'cancun',␊ + },␊ + },␊ + },␊ + };␊ + `, + `package.json:␊ + {␊ + "name": "tronbox-sample",␊ + "version": "0.0.1",␊ + "description": "Sample TronBox project generated by OpenZeppelin Contracts Wizard",␊ + "license": "MIT",␊ + "scripts": {␊ + "compile": "tronbox compile",␊ + "migrate": "tronbox migrate",␊ + "test": "tronbox test",␊ + "console": "tronbox console"␊ + },␊ + "devDependencies": {␊ + "tronbox": "^4.1.0"␊ + },␊ + "dependencies": {␊ + "@openzeppelin/tron-contracts": "^0.0.1",␊ + "@openzeppelin/tron-contracts-upgradeable": "^0.0.1"␊ + }␊ + }`, + `README.md:␊ + # Sample TronBox Project␊ + ␊ + This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test.␊ + ␊ + ## Prerequisites␊ + ␊ + - [Node.js 18+](https://nodejs.org/en/download/)␊ + - [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`)␊ + - Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\`␊ + ␊ + ## Installing dependencies␊ + ␊ + > :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime.␊ + ␊ + \`\`\`␊ + npm install␊ + \`\`\`␊ + ␊ + ## Running a local TRON node␊ + ␊ + In a separate terminal:␊ + ␊ + \`\`\`␊ + docker run --rm -p 9090:9090 tronbox/tre␊ + \`\`\`␊ + ␊ + ## Compiling␊ + ␊ + \`\`\`␊ + tronbox compile␊ + \`\`\`␊ + ␊ + ## Deploying␊ + ␊ + \`\`\`␊ + tronbox migrate --network development␊ + \`\`\`␊ + ␊ + For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`.␊ + ␊ + > :information_source: This is an upgradeable contract. OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to TRON, so \`migrations/2_deploy_MyGovernor.js\` deploys the proxy by hand: it deploys the \`MyGovernor\` implementation, then a \`TRC1967Proxy\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable).␊ + ␊ + ## Testing␊ + ␊ + \`\`\`␊ + tronbox test␊ + \`\`\`␊ + ␊ + This will run the Mocha test in \`test/MyGovernor.js\` against the configured network.␊ + `, + `.gitignore:␊ + node_modules␊ + build␊ + .env␊ + `, + ] diff --git a/packages/core/solidity/src/zip-tronbox.test.ts.snap b/packages/core/solidity/src/zip-tronbox.test.ts.snap new file mode 100644 index 000000000..f38be1692 Binary files /dev/null and b/packages/core/solidity/src/zip-tronbox.test.ts.snap differ diff --git a/packages/core/solidity/src/zip-tronbox.ts b/packages/core/solidity/src/zip-tronbox.ts new file mode 100644 index 000000000..c1b8cfc62 --- /dev/null +++ b/packages/core/solidity/src/zip-tronbox.ts @@ -0,0 +1,396 @@ +import JSZip from 'jszip'; +import type { GenericOptions } from './build-generic'; +import type { Contract } from './contract'; +import { printContract } from './print'; +import { tronPrintProfile, TRON_SOLIDITY_VERSION } from './utils/transform-tron'; +import { stringifyUnicodeSafe } from './utils/sanitize'; +import { tronProxyFor, tronProxyHelperSource } from './utils/tron-upgradeable'; + +// TronBox is a Truffle-derived framework for the TRON Virtual Machine. The +// download bundles: +// - the contract source (rewritten for @openzeppelin/tron-contracts), +// - migrations (`migrations/1_initial_migration.js`, `migrations/2_deploy_.js`), +// - a Mocha-based test using `artifacts.require()`, +// - `tronbox-config.js` configured for local TRE + Shasta/Nile/mainnet, +// - `package.json` with TronBox + the OZ TRON contracts library. + +// TRON_SOLIDITY_VERSION (imported) matches the printed pragma and the +// @openzeppelin/hardhat-tron README, so both download flavours stay aligned. + +function getDeploymentArgs(c: Contract): string[] { + // The arg name doubles as the local variable identifier declared above the + // deploy call (see declareArgPlaceholders). + return c.constructorArgs.map(arg => arg.name); +} + +// Non-address args have no usable placeholder value, so the user must fill them +// in before deploying. Address args get the obviously-invalid '' +// sentinel, which compiles but fails loudly at deploy time if left unedited. +function hasUnsetArgs(c: Contract): boolean { + return c.constructorArgs.some(arg => arg.type !== 'address'); +} + +function declareArgPlaceholders(c: Contract): string[] { + return c.constructorArgs.map(arg => { + if (arg.type === 'address') { + return `// TODO: Replace with a real address (e.g. tronWeb.defaultAddress.base58).\n const ${arg.name} = '';`; + } + // No safe default — emit a commented-out declaration so the migration can + // never silently deploy an `undefined` value. + return `// TODO: Set the ${arg.name} constructor argument, then uncomment it and the deploy call below.\n // const ${arg.name} = /* ... */;`; + }); +} + +const migrationsContract = `\ +// SPDX-License-Identifier: MIT +pragma solidity ^${TRON_SOLIDITY_VERSION}; + +contract Migrations { + address public owner = msg.sender; + uint public last_completed_migration; + + modifier restricted() { + require(msg.sender == owner, "Restricted to owner"); + _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } +} +`; + +const initialMigration = `\ +const Migrations = artifacts.require('./Migrations.sol'); + +module.exports = function (deployer) { + deployer.deploy(Migrations); +}; +`; + +function deployMigration(c: Contract): string { + const argDecls = declareArgPlaceholders(c); + const argList = getDeploymentArgs(c); + + const declarations = argDecls.length > 0 ? argDecls.join('\n ') + '\n\n ' : ''; + + let deployCall: string; + if (argList.length === 0) { + deployCall = `deployer.deploy(${c.name});`; + } else if (hasUnsetArgs(c)) { + // At least one argument has no usable placeholder; leave the deploy call + // commented out so an unedited `tronbox migrate` is a no-op rather than + // deploying with missing values. + deployCall = `// TODO: Uncomment once the constructor arguments above are set.\n // deployer.deploy(${c.name}, ${argList.join(', ')});`; + } else { + deployCall = `deployer.deploy(${c.name}, ${argList.join(', ')});`; + } + + return `\ +const ${c.name} = artifacts.require('./${c.name}.sol'); + +module.exports = function (deployer) { + ${declarations}${deployCall} +}; +`; +} + +// Deploys an upgradeable contract behind a proxy. OpenZeppelin's upgrades +// tooling targets EVM chains and does not deploy to TRON, so the proxy is +// deployed by hand per https://github.com/OpenZeppelin/tron-contracts-upgradeable. +function deployUpgradeableMigration(c: Contract): string { + const proxy = tronProxyFor(c); + const gated = hasUnsetArgs(c); + const g = gated ? '// ' : ''; + + const argDecls = c.constructorArgs.flatMap((arg, i) => { + if (arg.type === 'address') { + return [` const ${arg.name} = accounts[${i}];`]; + } + return [` // TODO: Set the initialize() argument "${arg.name}".`, ` // const ${arg.name} = ...;`]; + }); + const argList = c.constructorArgs.map(a => a.name).join(', '); + const adminDecl = proxy.isTransparent ? ` const proxyAdminOwner = accounts[0];\n\n` : ''; + const proxyArgs = proxy.isTransparent + ? `implementation.address, proxyAdminOwner, initData` + : `implementation.address, initData`; + + return `\ +const ${c.name} = artifacts.require('./${c.name}.sol'); +const ${proxy.contractName} = artifacts.require('${proxy.contractName}'); + +// OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to +// TRON, so this migration deploys the proxy by hand: deploy the implementation, +// then a ${proxy.contractName} that delegates to it and runs initialize() +// atomically. Interact with the proxy address, never the implementation. +// See https://github.com/OpenZeppelin/tron-contracts-upgradeable +module.exports = async function (deployer, network, accounts) { + // 1. Deploy the implementation. It is never called directly and cannot be + // initialized on its own (its constructor runs _disableInitializers()). + await deployer.deploy(${c.name}); + const implementation = await ${c.name}.deployed(); + +${argDecls.length > 0 ? argDecls.join('\n') + '\n\n' : ''}${adminDecl} // 2. ABI-encode the initializer call. TronBox is Truffle-derived; if your + // version doesn't expose \`.contract.methods\`, encode the initialize(...) + // call with tronWeb's ABI utilities instead. +${gated ? ' // TODO: Uncomment the lines below once the initialize() arguments above are set.\n' : ''} ${g}const initData = implementation.contract.methods.initialize(${argList}).encodeABI(); + + // 3. Deploy the proxy pointing at the implementation. + ${g}await deployer.deploy(${proxy.contractName}, ${proxyArgs}); +}; +`; +} + +function kindAssertion(opts?: GenericOptions): string { + if (opts !== undefined) { + switch (opts.kind) { + case 'ERC20': + case 'ERC721': + return ` + + it('sets the expected name', async function () { + assert.equal(await instance.name(), ${stringifyUnicodeSafe(opts.name)}); + });`; + case 'ERC1155': + return ` + + it('sets the expected URI', async function () { + assert.equal(await instance.uri(0), ${stringifyUnicodeSafe(opts.uri)}); + });`; + default: + break; + } + } + return ''; +} + +// For upgradeable contracts the deployed `${c.name}` artifact is the +// implementation; the initialized state lives in the proxy, so the test reads +// through the proxy address using the implementation's ABI. +function testFileUpgradeable(c: Contract, opts?: GenericOptions): string { + const proxy = tronProxyFor(c); + const assertion = kindAssertion(opts); + + return `\ +const ${c.name} = artifacts.require('./${c.name}.sol'); +const ${proxy.contractName} = artifacts.require('${proxy.contractName}'); + +// These tests require TronBox >= 4.1.x and the TronBox Runtime Environment +// (https://hub.docker.com/r/tronbox/tre) as your private network. The migration +// must have deployed the proxy (fill in any initialize() arguments first). +contract('${c.name}', function (accounts) { + let instance; + + before(async function () { + // Interact with the proxy address using the implementation's ABI. + const proxy = await ${proxy.contractName}.deployed(); + instance = await ${c.name}.at(proxy.address); + }); + + it('is deployed behind a proxy', async function () { + assert.isTrue(accounts.length >= 1, 'At least one account is required.'); + assert.isOk(instance.address, 'Proxy address should be defined'); + });${assertion} +}); +`; +} + +function testFile(c: Contract, opts?: GenericOptions): string { + const assertion = kindAssertion(opts); + + const constructorArgNote = + c.constructorArgs.length > 0 + ? `// NOTE: this contract has constructor arguments. Update the placeholders in +// migrations/2_deploy_${c.name}.js before running 'tronbox test'. +` + : ''; + + return `\ +const ${c.name} = artifacts.require('./${c.name}.sol'); + +// These tests require TronBox >= 4.1.x and the TronBox Runtime Environment +// (https://hub.docker.com/r/tronbox/tre) as your private network. +${constructorArgNote}contract('${c.name}', function (accounts) { + let instance; + + before(async function () { + instance = await ${c.name}.deployed(); + }); + + it('is deployed', async function () { + assert.isTrue(accounts.length >= 1, 'At least one account is required.'); + assert.isOk(instance.address, 'Contract address should be defined'); + });${assertion} +}); +`; +} + +const tronboxConfig = `\ +// tronbox-config.js +// +// TronBox configuration for projects targeting TRON. Run with one of: +// +// tronbox migrate --network development # local TRE in Docker +// tronbox migrate --network shasta # Shasta testnet +// tronbox migrate --network nile # Nile testnet +// tronbox migrate --network mainnet # TRON mainnet +// +// Create a .env file (gitignored!) with PRIVATE_KEY_* values before deploying +// to any non-development network. + +module.exports = { + networks: { + development: { + // For tronbox/tre docker image: https://hub.docker.com/r/tronbox/tre + privateKey: '0000000000000000000000000000000000000000000000000000000000000001', + userFeePercentage: 0, + feeLimit: 1000 * 1e6, + fullHost: 'http://127.0.0.1:9090', + network_id: '9', + }, + shasta: { + privateKey: process.env.PRIVATE_KEY_SHASTA, + userFeePercentage: 50, + feeLimit: 1000 * 1e6, + fullHost: 'https://api.shasta.trongrid.io', + network_id: '2', + }, + nile: { + privateKey: process.env.PRIVATE_KEY_NILE, + userFeePercentage: 100, + feeLimit: 1000 * 1e6, + fullHost: 'https://nile.trongrid.io', + network_id: '3', + }, + mainnet: { + privateKey: process.env.PRIVATE_KEY_MAINNET, + userFeePercentage: 100, + feeLimit: 1000 * 1e6, + fullHost: 'https://api.trongrid.io', + network_id: '1', + }, + }, + compilers: { + solc: { + version: '${TRON_SOLIDITY_VERSION}', + settings: { + optimizer: { enabled: true, runs: 200 }, + evmVersion: 'cancun', + }, + }, + }, +}; +`; + +function packageJson(c: Contract): unknown { + // Upgradeable contracts pull their transpiled `*Upgradeable` parents from + // tron-contracts-upgradeable; tron-contracts stays on as its peer (it also + // provides the proxy the migration deploys). + const dependencies: Record = { '@openzeppelin/tron-contracts': '^0.0.1' }; + if (c.upgradeable) { + dependencies['@openzeppelin/tron-contracts-upgradeable'] = '^0.0.1'; + } + return { + name: 'tronbox-sample', + version: '0.0.1', + description: 'Sample TronBox project generated by OpenZeppelin Contracts Wizard', + license: c.license, + scripts: { + compile: 'tronbox compile', + migrate: 'tronbox migrate', + test: 'tronbox test', + console: 'tronbox console', + }, + devDependencies: { + tronbox: '^4.1.0', + }, + dependencies, + }; +} + +const gitignore = `\ +node_modules +build +.env +`; + +function readme(c: Contract): string { + return `\ +# Sample TronBox Project + +This project demonstrates a basic TronBox use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a migration that deploys it, and a Mocha test. + +## Prerequisites + +- [Node.js 18+](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/get-docker/) — runs the local TRON Runtime Environment (\`tronbox/tre\`) +- Install [TronBox](https://tronbox.io/docs/) globally: \`npm install -g tronbox\` + +## Installing dependencies + +> :warning: Temporary limitation: this template depends on \`@openzeppelin/tron-contracts\`, which may not yet be available on the public npm registry. If \`npm install\` fails with a 404 for that package, retry after it is published, or install it from a local checkout / git URL in the meantime. + +\`\`\` +npm install +\`\`\` + +## Running a local TRON node + +In a separate terminal: + +\`\`\` +docker run --rm -p 9090:9090 tronbox/tre +\`\`\` + +## Compiling + +\`\`\` +tronbox compile +\`\`\` + +## Deploying + +\`\`\` +tronbox migrate --network development +\`\`\` + +For Shasta/Nile/mainnet, set the corresponding \`PRIVATE_KEY_*\` env var in a \`.env\` file and pass \`--network \`. +${ + c.upgradeable + ? ` +> :information_source: This is an upgradeable contract. OpenZeppelin's upgrades tooling targets EVM chains and does not deploy to TRON, so \`migrations/2_deploy_${c.name}.js\` deploys the proxy by hand: it deploys the \`${c.name}\` implementation, then a \`${tronProxyFor(c).contractName}\` that delegates to it and runs \`initialize()\` atomically. Interact with the **proxy** address, never the implementation. See the [upgradeable contracts guide](https://github.com/OpenZeppelin/tron-contracts-upgradeable). +` + : '' +} +## Testing + +\`\`\` +tronbox test +\`\`\` + +This will run the Mocha test in \`test/${c.name}.js\` against the configured network. +`; +} + +export async function zipTronbox(c: Contract, opts?: GenericOptions): Promise { + const zip = new JSZip(); + + zip.file(`contracts/${c.name}.sol`, printContract(c, tronPrintProfile)); + zip.file('contracts/Migrations.sol', migrationsContract); + if (c.upgradeable) { + // Pull the proxy into the build so the migration can deploy ${c.name} behind it. + zip.file('contracts/Proxy.sol', tronProxyHelperSource(c, TRON_SOLIDITY_VERSION)); + } + + zip.file('migrations/1_initial_migration.js', initialMigration); + zip.file(`migrations/2_deploy_${c.name}.js`, c.upgradeable ? deployUpgradeableMigration(c) : deployMigration(c)); + + zip.file(`test/${c.name}.js`, c.upgradeable ? testFileUpgradeable(c, opts) : testFile(c, opts)); + + zip.file('tronbox-config.js', tronboxConfig); + zip.file('package.json', JSON.stringify(packageJson(c), null, 2)); + zip.file('.gitignore', gitignore); + zip.file('README.md', readme(c)); + + return zip; +} diff --git a/packages/core/solidity/zip-env-hardhat-tron.js b/packages/core/solidity/zip-env-hardhat-tron.js new file mode 100644 index 000000000..8c40f9753 --- /dev/null +++ b/packages/core/solidity/zip-env-hardhat-tron.js @@ -0,0 +1 @@ +module.exports = require('./dist/zip-hardhat-tron'); diff --git a/packages/core/solidity/zip-env-hardhat-tron.ts b/packages/core/solidity/zip-env-hardhat-tron.ts new file mode 100644 index 000000000..0519a259c --- /dev/null +++ b/packages/core/solidity/zip-env-hardhat-tron.ts @@ -0,0 +1 @@ +export * from './src/zip-hardhat-tron'; diff --git a/packages/core/solidity/zip-env-tronbox.js b/packages/core/solidity/zip-env-tronbox.js new file mode 100644 index 000000000..e3dd5397d --- /dev/null +++ b/packages/core/solidity/zip-env-tronbox.js @@ -0,0 +1 @@ +module.exports = require('./dist/zip-tronbox'); diff --git a/packages/core/solidity/zip-env-tronbox.ts b/packages/core/solidity/zip-env-tronbox.ts new file mode 100644 index 000000000..666dd7703 --- /dev/null +++ b/packages/core/solidity/zip-env-tronbox.ts @@ -0,0 +1 @@ +export * from './src/zip-tronbox'; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 7b8013797..be7757b0e 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -3,4 +3,5 @@ export { registerCairoTools } from './cairo/tools'; export { registerConfidentialTools } from './confidential/tools'; export { registerStellarTools } from './stellar/tools'; export { registerStylusTools } from './stylus/tools'; +export { registerTronTools } from './tron/tools'; export { registerUniswapHooksTools } from './uniswap-hooks/tools'; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index dd786937f..1bc7dedfe 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -4,6 +4,7 @@ import { registerCairoTools } from './cairo/tools.js'; import { registerConfidentialTools } from './confidential/tools.js'; import { registerStellarTools } from './stellar/tools.js'; import { registerStylusTools } from './stylus/tools.js'; +import { registerTronTools } from './tron/tools.js'; import { registerUniswapHooksTools } from './uniswap-hooks/tools.js'; import { version } from '../package.json'; @@ -28,6 +29,7 @@ If the user asks to modify an existing smart contract, use these tools to determ registerConfidentialTools(server); registerStellarTools(server); registerStylusTools(server); + registerTronTools(server); registerUniswapHooksTools(server); return server; diff --git a/packages/mcp/src/tron/tools.ts b/packages/mcp/src/tron/tools.ts new file mode 100644 index 000000000..f8d579aaa --- /dev/null +++ b/packages/mcp/src/tron/tools.ts @@ -0,0 +1,19 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronTRC20 } from './tools/erc20.js'; +import { registerTronTRC721 } from './tools/erc721.js'; +import { registerTronTRC1155 } from './tools/erc1155.js'; +import { registerTronGovernor } from './tools/governor.js'; +import { registerTronCustom } from './tools/custom.js'; + +// TRON tools reuse the Solidity schemas (options are identical) and render the +// output through `tronPrintProfile` so token standards become TRC* and imports +// resolve from `@openzeppelin/tron-contracts`. Excluded on purpose: Account +// (ERC-4337 EntryPoint out of scope) and Stablecoin / RealWorldAsset (they +// depend on @openzeppelin/community-contracts, which is not ported to TRON). +export function registerTronTools(server: McpServer) { + registerTronTRC20(server); + registerTronTRC721(server); + registerTronTRC1155(server); + registerTronGovernor(server); + registerTronCustom(server); +} diff --git a/packages/mcp/src/tron/tools/custom.test.ts b/packages/mcp/src/tron/tools/custom.test.ts new file mode 100644 index 000000000..7d2dab540 --- /dev/null +++ b/packages/mcp/src/tron/tools/custom.test.ts @@ -0,0 +1,55 @@ +import type { TestFn, ExecutionContext } from 'ava'; +import _test from 'ava'; +import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronCustom } from './custom'; +import type { DeepRequired } from '../../helpers.test'; +import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; +import type { CustomOptions } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { solidityCustomSchema } from '@openzeppelin/wizard-common/schemas'; +import { z } from 'zod'; + +interface Context { + tool: RegisteredTool; + schema: z.ZodObject; +} + +const test = _test as TestFn; + +test.before(t => { + t.context.tool = registerTronCustom(new McpServer(testMcpInfo)); + t.context.schema = z.object(solidityCustomSchema); +}); + +function assertHasAllSupportedFields( + t: ExecutionContext, + params: DeepRequired>, +) { + const _: DeepRequired = params; + t.pass(); +} + +const tronPrint = (opts: CustomOptions) => printContract(buildGeneric({ kind: 'Custom', ...opts }), tronPrintProfile); + +test('basic', async t => { + const params: z.infer = { + name: 'MyCustom', + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('all', async t => { + const params: DeepRequired> = { + name: 'MyCustom', + pausable: true, + access: 'roles', + upgradeable: 'uups', + info: { + license: 'MIT', + securityContact: 'security@example.com', + }, + }; + assertHasAllSupportedFields(t, params); + await assertAPIEquivalence(t, params, tronPrint); +}); diff --git a/packages/mcp/src/tron/tools/custom.ts b/packages/mcp/src/tron/tools/custom.ts new file mode 100644 index 000000000..f106f906e --- /dev/null +++ b/packages/mcp/src/tron/tools/custom.ts @@ -0,0 +1,33 @@ +import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { CustomOptions } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; +import { solidityCustomSchema } from '@openzeppelin/wizard-common/schemas'; +import { tronPrompts } from '@openzeppelin/wizard-common'; + +export function registerTronCustom(server: McpServer): RegisteredTool { + return server.tool( + 'tron-custom', + makeDetailedPrompt(tronPrompts.Custom), + solidityCustomSchema, + async ({ name, pausable, access, upgradeable, info }) => { + const opts: CustomOptions = { + name, + pausable, + access, + upgradeable, + info, + }; + return { + content: [ + { + type: 'text', + text: safePrintSolidityCodeBlock(() => + printContract(buildGeneric({ kind: 'Custom', ...opts }), tronPrintProfile), + ), + }, + ], + }; + }, + ); +} diff --git a/packages/mcp/src/tron/tools/erc1155.test.ts b/packages/mcp/src/tron/tools/erc1155.test.ts new file mode 100644 index 000000000..5001d491e --- /dev/null +++ b/packages/mcp/src/tron/tools/erc1155.test.ts @@ -0,0 +1,63 @@ +import type { TestFn, ExecutionContext } from 'ava'; +import _test from 'ava'; +import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronTRC1155 } from './erc1155'; +import type { DeepRequired } from '../../helpers.test'; +import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; +import type { ERC1155Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { solidityERC1155Schema } from '@openzeppelin/wizard-common/schemas'; +import { z } from 'zod'; + +interface Context { + tool: RegisteredTool; + schema: z.ZodObject; +} + +const test = _test as TestFn; + +test.before(t => { + t.context.tool = registerTronTRC1155(new McpServer(testMcpInfo)); + t.context.schema = z.object(solidityERC1155Schema); +}); + +function assertHasAllSupportedFields( + t: ExecutionContext, + params: DeepRequired>, +) { + const _: DeepRequired = params; + t.pass(); +} + +const tronPrint = (opts: ERC1155Options) => printContract(buildGeneric({ kind: 'ERC1155', ...opts }), tronPrintProfile); + +test('basic', async t => { + const params: z.infer = { + name: 'TestMulti', + uri: 'ipfs://example/{id}', + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('all', async t => { + const params: DeepRequired> = { + name: 'TestMulti', + uri: 'ipfs://example/{id}', + burnable: true, + pausable: true, + mintable: true, + supply: true, + updatableUri: true, + access: 'roles', + upgradeable: 'transparent', + info: { + license: 'MIT', + securityContact: 'security@example.com', + }, + }; + + assertHasAllSupportedFields(t, params); + + await assertAPIEquivalence(t, params, tronPrint); +}); diff --git a/packages/mcp/src/tron/tools/erc1155.ts b/packages/mcp/src/tron/tools/erc1155.ts new file mode 100644 index 000000000..0267da198 --- /dev/null +++ b/packages/mcp/src/tron/tools/erc1155.ts @@ -0,0 +1,38 @@ +import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ERC1155Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; +import { solidityERC1155Schema } from '@openzeppelin/wizard-common/schemas'; +import { tronPrompts } from '@openzeppelin/wizard-common'; + +export function registerTronTRC1155(server: McpServer): RegisteredTool { + return server.tool( + 'tron-trc1155', + makeDetailedPrompt(tronPrompts.TRC1155), + solidityERC1155Schema, + async ({ name, uri, burnable, pausable, mintable, supply, updatableUri, access, upgradeable, info }) => { + const opts: ERC1155Options = { + name, + uri, + burnable, + pausable, + mintable, + supply, + updatableUri, + access, + upgradeable, + info, + }; + return { + content: [ + { + type: 'text', + text: safePrintSolidityCodeBlock(() => + printContract(buildGeneric({ kind: 'ERC1155', ...opts }), tronPrintProfile), + ), + }, + ], + }; + }, + ); +} diff --git a/packages/mcp/src/tron/tools/erc20.test.ts b/packages/mcp/src/tron/tools/erc20.test.ts new file mode 100644 index 000000000..bba952690 --- /dev/null +++ b/packages/mcp/src/tron/tools/erc20.test.ts @@ -0,0 +1,86 @@ +import type { TestFn, ExecutionContext } from 'ava'; +import _test from 'ava'; +import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronTRC20 } from './erc20'; +import type { DeepRequired } from '../../helpers.test'; +import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; +import type { ERC20Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { solidityERC20Schema } from '@openzeppelin/wizard-common/schemas'; +import { z } from 'zod'; + +interface Context { + tool: RegisteredTool; + schema: z.ZodObject; +} + +const test = _test as TestFn; + +test.before(t => { + t.context.tool = registerTronTRC20(new McpServer(testMcpInfo)); + t.context.schema = z.object(solidityERC20Schema); +}); + +function assertHasAllSupportedFields( + t: ExecutionContext, + params: DeepRequired>, +) { + const _: DeepRequired = params; + t.pass(); +} + +// The TRON tool renders the Solidity ERC20 contract through the TRON library +// profile, so the expected output goes through the same structured transform. +const tronPrint = (opts: ERC20Options) => printContract(buildGeneric({ kind: 'ERC20', ...opts }), tronPrintProfile); + +test('basic', async t => { + const params: z.infer = { + name: 'TestToken', + symbol: 'TST', + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('renames ERC20 to TRC20 and rewrites imports', async t => { + // assertAPIEquivalence verifies the MCP tool output contains the result of + // tronPrint(params) — which has already been rewritten to TRC20 + + // @openzeppelin/tron-contracts. So if this passes, the tool is rewriting. + const params: z.infer = { + name: 'TestToken', + symbol: 'TST', + burnable: true, + permit: true, + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('all', async t => { + const params: DeepRequired> = { + name: 'TestToken', + symbol: 'TST', + decimals: '6', + burnable: true, + pausable: true, + premint: '1000000', + premintChainId: '1', + mintable: true, + callback: true, + permit: true, + votes: 'blocknumber', + flashmint: true, + crossChainBridging: 'custom', + crossChainLinkAllowOverride: false, + access: 'roles', + upgradeable: 'transparent', + namespacePrefix: 'myProject', + info: { + license: 'MIT', + securityContact: 'security@example.com', + }, + }; + + assertHasAllSupportedFields(t, params); + + await assertAPIEquivalence(t, params, tronPrint); +}); diff --git a/packages/mcp/src/tron/tools/erc20.ts b/packages/mcp/src/tron/tools/erc20.ts new file mode 100644 index 000000000..f880a552d --- /dev/null +++ b/packages/mcp/src/tron/tools/erc20.ts @@ -0,0 +1,63 @@ +import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ERC20Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile, sanitizeTronOptions } from '@openzeppelin/wizard'; +import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; +import { solidityERC20Schema } from '@openzeppelin/wizard-common/schemas'; +import { tronPrompts } from '@openzeppelin/wizard-common'; + +export function registerTronTRC20(server: McpServer): RegisteredTool { + return server.tool( + 'tron-trc20', + makeDetailedPrompt(tronPrompts.TRC20), + solidityERC20Schema, + async ({ + name, + symbol, + decimals, + burnable, + pausable, + premint, + premintChainId, + mintable, + callback, + permit, + votes, + flashmint, + crossChainBridging, + crossChainLinkAllowOverride, + access, + upgradeable, + info, + }) => { + const opts: ERC20Options = { + name, + symbol, + decimals, + burnable, + pausable, + premint, + premintChainId, + mintable, + callback, + permit, + votes, + flashmint, + crossChainBridging, + crossChainLinkAllowOverride, + access, + upgradeable, + info, + }; + return { + content: [ + { + type: 'text', + text: safePrintSolidityCodeBlock(() => + printContract(buildGeneric({ kind: 'ERC20', ...sanitizeTronOptions(opts) }), tronPrintProfile), + ), + }, + ], + }; + }, + ); +} diff --git a/packages/mcp/src/tron/tools/erc721.test.ts b/packages/mcp/src/tron/tools/erc721.test.ts new file mode 100644 index 000000000..b286cb37a --- /dev/null +++ b/packages/mcp/src/tron/tools/erc721.test.ts @@ -0,0 +1,67 @@ +import type { TestFn, ExecutionContext } from 'ava'; +import _test from 'ava'; +import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronTRC721 } from './erc721'; +import type { DeepRequired } from '../../helpers.test'; +import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; +import type { ERC721Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { solidityERC721Schema } from '@openzeppelin/wizard-common/schemas'; +import { z } from 'zod'; + +interface Context { + tool: RegisteredTool; + schema: z.ZodObject; +} + +const test = _test as TestFn; + +test.before(t => { + t.context.tool = registerTronTRC721(new McpServer(testMcpInfo)); + t.context.schema = z.object(solidityERC721Schema); +}); + +function assertHasAllSupportedFields( + t: ExecutionContext, + params: DeepRequired>, +) { + const _: DeepRequired = params; + t.pass(); +} + +const tronPrint = (opts: ERC721Options) => printContract(buildGeneric({ kind: 'ERC721', ...opts }), tronPrintProfile); + +test('basic', async t => { + const params: z.infer = { + name: 'TestNFT', + symbol: 'TNFT', + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('all', async t => { + const params: DeepRequired> = { + name: 'TestNFT', + symbol: 'TNFT', + baseUri: 'https://example.com/', + enumerable: true, + uriStorage: true, + burnable: true, + pausable: true, + mintable: true, + incremental: true, + votes: 'blocknumber', + access: 'roles', + upgradeable: 'transparent', + namespacePrefix: 'myProject', + info: { + license: 'MIT', + securityContact: 'security@example.com', + }, + }; + + assertHasAllSupportedFields(t, params); + + await assertAPIEquivalence(t, params, tronPrint); +}); diff --git a/packages/mcp/src/tron/tools/erc721.ts b/packages/mcp/src/tron/tools/erc721.ts new file mode 100644 index 000000000..30174ca06 --- /dev/null +++ b/packages/mcp/src/tron/tools/erc721.ts @@ -0,0 +1,57 @@ +import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ERC721Options } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile } from '@openzeppelin/wizard'; +import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; +import { solidityERC721Schema } from '@openzeppelin/wizard-common/schemas'; +import { tronPrompts } from '@openzeppelin/wizard-common'; + +export function registerTronTRC721(server: McpServer): RegisteredTool { + return server.tool( + 'tron-trc721', + makeDetailedPrompt(tronPrompts.TRC721), + solidityERC721Schema, + async ({ + name, + symbol, + baseUri, + enumerable, + uriStorage, + burnable, + pausable, + mintable, + incremental, + votes, + access, + upgradeable, + namespacePrefix, + info, + }) => { + const opts: ERC721Options = { + name, + symbol, + baseUri, + enumerable, + uriStorage, + burnable, + pausable, + mintable, + incremental, + votes, + access, + upgradeable, + namespacePrefix, + info, + }; + return { + content: [ + { + type: 'text', + text: safePrintSolidityCodeBlock(() => + printContract(buildGeneric({ kind: 'ERC721', ...opts }), tronPrintProfile), + ), + }, + ], + }; + }, + ); +} diff --git a/packages/mcp/src/tron/tools/governor.test.ts b/packages/mcp/src/tron/tools/governor.test.ts new file mode 100644 index 000000000..7e0bf883b --- /dev/null +++ b/packages/mcp/src/tron/tools/governor.test.ts @@ -0,0 +1,72 @@ +import type { TestFn, ExecutionContext } from 'ava'; +import _test from 'ava'; +import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerTronGovernor } from './governor'; +import type { DeepRequired } from '../../helpers.test'; +import { testMcpInfo, assertAPIEquivalence } from '../../helpers.test'; +import type { GovernorOptions } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile, TRON_DEFAULT_BLOCK_TIME } from '@openzeppelin/wizard'; +import { solidityGovernorSchema } from '@openzeppelin/wizard-common/schemas'; +import { z } from 'zod'; + +interface Context { + tool: RegisteredTool; + schema: z.ZodObject; +} + +const test = _test as TestFn; + +test.before(t => { + t.context.tool = registerTronGovernor(new McpServer(testMcpInfo)); + t.context.schema = z.object(solidityGovernorSchema); +}); + +function assertHasAllSupportedFields( + t: ExecutionContext, + params: DeepRequired>, +) { + const _: DeepRequired> = params; + t.pass(); +} + +const tronPrint = (opts: GovernorOptions) => + printContract( + buildGeneric({ kind: 'Governor', ...opts, blockTime: opts.blockTime ?? TRON_DEFAULT_BLOCK_TIME }), + tronPrintProfile, + ); + +test('basic', async t => { + const params: z.infer = { + name: 'MyGovernor', + delay: '1 day', + period: '1 week', + }; + await assertAPIEquivalence(t, params, tronPrint); +}); + +test('all', async t => { + const params: DeepRequired> = { + name: 'MyGovernor', + delay: '1 day', + period: '1 week', + votes: 'erc20votes', + clockMode: 'blocknumber', + timelock: 'openzeppelin', + blockTime: 12, + decimals: 18, + proposalThreshold: '1', + quorumMode: 'absolute', + quorumPercent: 0, + quorumAbsolute: '5', + storage: true, + settings: true, + upgradeable: 'uups', + info: { + license: 'MIT', + securityContact: 'security@example.com', + }, + }; + assertHasAllSupportedFields(t, params); + await assertAPIEquivalence(t, params, tronPrint); +}); diff --git a/packages/mcp/src/tron/tools/governor.ts b/packages/mcp/src/tron/tools/governor.ts new file mode 100644 index 000000000..0e3c45529 --- /dev/null +++ b/packages/mcp/src/tron/tools/governor.ts @@ -0,0 +1,64 @@ +import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { GovernorOptions } from '@openzeppelin/wizard'; +import { buildGeneric, printContract, tronPrintProfile, TRON_DEFAULT_BLOCK_TIME } from '@openzeppelin/wizard'; +import { safePrintSolidityCodeBlock, makeDetailedPrompt } from '../../utils'; +import { tronGovernorSchema } from '@openzeppelin/wizard-common/schemas'; +import { tronPrompts } from '@openzeppelin/wizard-common'; + +export function registerTronGovernor(server: McpServer): RegisteredTool { + return server.tool( + 'tron-governor', + makeDetailedPrompt(tronPrompts.Governor), + tronGovernorSchema, + async ({ + name, + delay, + period, + votes, + clockMode, + timelock, + blockTime, + decimals, + proposalThreshold, + quorumMode, + quorumPercent, + quorumAbsolute, + storage, + settings, + upgradeable, + info, + }) => { + const opts: GovernorOptions = { + name, + delay, + period, + votes, + clockMode, + timelock, + // TRON produces blocks every ~3s (vs ~12s on Ethereum); apply that + // default when the caller hasn't supplied one so the generated + // voting delay/period in blocks matches TRON's chain. + blockTime: blockTime ?? TRON_DEFAULT_BLOCK_TIME, + decimals, + proposalThreshold, + quorumMode, + quorumPercent, + quorumAbsolute, + storage, + settings, + upgradeable, + info, + }; + return { + content: [ + { + type: 'text', + text: safePrintSolidityCodeBlock(() => + printContract(buildGeneric({ kind: 'Governor', ...opts }), tronPrintProfile), + ), + }, + ], + }; + }, + ); +} diff --git a/packages/ui/api/ai-assistant/function-definitions/tron.ts b/packages/ui/api/ai-assistant/function-definitions/tron.ts new file mode 100644 index 000000000..a63227deb --- /dev/null +++ b/packages/ui/api/ai-assistant/function-definitions/tron.ts @@ -0,0 +1,18 @@ +import { + solidityERC20AIFunctionDefinition, + solidityERC721AIFunctionDefinition, + solidityERC1155AIFunctionDefinition, + solidityGovernorAIFunctionDefinition, + solidityCustomAIFunctionDefinition, +} from './solidity.ts'; + +// The TRON AI assistant uses the same option shape as Solidity, less Account +// (ERC-4337 out of scope) and Stablecoin / RealWorldAsset (they depend on +// @openzeppelin/community-contracts, which is not ported to TRON). Function +// definitions are reused directly; the language identifier ('tron') is what +// differentiates assistant context. +export const tronERC20AIFunctionDefinition = solidityERC20AIFunctionDefinition; +export const tronERC721AIFunctionDefinition = solidityERC721AIFunctionDefinition; +export const tronERC1155AIFunctionDefinition = solidityERC1155AIFunctionDefinition; +export const tronGovernorAIFunctionDefinition = solidityGovernorAIFunctionDefinition; +export const tronCustomAIFunctionDefinition = solidityCustomAIFunctionDefinition; diff --git a/packages/ui/api/ai-assistant/types/languages.ts b/packages/ui/api/ai-assistant/types/languages.ts index 3467acc97..cb93bbe94 100644 --- a/packages/ui/api/ai-assistant/types/languages.ts +++ b/packages/ui/api/ai-assistant/types/languages.ts @@ -42,6 +42,9 @@ export type LanguagesContractsOptions = { cairoAlpha: CairoAlphaKindedOptions; confidential: ConfidentialKindedOptions; polkadot: Omit; + // Stablecoin + RealWorldAsset depend on @openzeppelin/community-contracts, which + // is not ported to TRON; Account's ERC-4337 EntryPoint is out of TRON scope. + tron: Omit; stellar: Omit & { Fungible: StellarKindedOptions['Fungible'] & StellarCommonContractOptions; NonFungible: StellarKindedOptions['NonFungible'] & StellarCommonContractOptions; diff --git a/packages/ui/api/ai.ts b/packages/ui/api/ai.ts index 73a4d35d2..97e9df81e 100644 --- a/packages/ui/api/ai.ts +++ b/packages/ui/api/ai.ts @@ -4,6 +4,7 @@ import * as cairoFunctions from './ai-assistant/function-definitions/cairo.ts'; import * as cairoAlphaFunctions from './ai-assistant/function-definitions/cairo-alpha.ts'; import * as stellarFunctions from './ai-assistant/function-definitions/stellar.ts'; import * as stylusFunctions from './ai-assistant/function-definitions/stylus.ts'; +import * as tronFunctions from './ai-assistant/function-definitions/tron.ts'; import * as confidentialFunctions from './ai-assistant/function-definitions/confidential.ts'; import * as uniswapHooksFunctions from './ai-assistant/function-definitions/uniswap-hooks.ts'; import { saveChatInRedisIfDoesNotExist } from './services/redis.ts'; @@ -27,6 +28,7 @@ const getFunctionsContext = polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/confidential.html b/packages/ui/public/confidential.html index 103464417..393699fb7 100644 --- a/packages/ui/public/confidential.html +++ b/packages/ui/public/confidential.html @@ -69,6 +69,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/icons/tron.svg b/packages/ui/public/icons/tron.svg new file mode 100644 index 000000000..a2adb2e0a --- /dev/null +++ b/packages/ui/public/icons/tron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/public/icons/tron_active.svg b/packages/ui/public/icons/tron_active.svg new file mode 100644 index 000000000..cf19a5148 --- /dev/null +++ b/packages/ui/public/icons/tron_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/public/index.html b/packages/ui/public/index.html index 998333320..3b7a7c90c 100644 --- a/packages/ui/public/index.html +++ b/packages/ui/public/index.html @@ -101,6 +101,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/polkadot.html b/packages/ui/public/polkadot.html index 12693e274..6c91e063e 100644 --- a/packages/ui/public/polkadot.html +++ b/packages/ui/public/polkadot.html @@ -70,6 +70,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/stellar.html b/packages/ui/public/stellar.html index 9df3d3277..66533991e 100644 --- a/packages/ui/public/stellar.html +++ b/packages/ui/public/stellar.html @@ -69,6 +69,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/stylus.html b/packages/ui/public/stylus.html index 75ce2a8a9..ce150fda5 100644 --- a/packages/ui/public/stylus.html +++ b/packages/ui/public/stylus.html @@ -69,6 +69,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/public/tron.html b/packages/ui/public/tron.html new file mode 100644 index 000000000..cf8840ea2 --- /dev/null +++ b/packages/ui/public/tron.html @@ -0,0 +1,135 @@ + + + + + + OpenZeppelin Contracts Wizard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ Forum + Docs + GitHub OpenZeppelin + Twitter/X +
+
+ + +
+ +
+ + + + + + + + + + + + + diff --git a/packages/ui/public/uniswap-hooks.html b/packages/ui/public/uniswap-hooks.html index d5ba1c021..5c7b4d61b 100644 --- a/packages/ui/public/uniswap-hooks.html +++ b/packages/ui/public/uniswap-hooks.html @@ -99,6 +99,7 @@ polkadotPolkadot stellarStellar stylusStylus + tronTRON uniswap-hooksUniswap Hooks diff --git a/packages/ui/src/common/languages-types.ts b/packages/ui/src/common/languages-types.ts index 375e93dd8..f8a1f55b5 100644 --- a/packages/ui/src/common/languages-types.ts +++ b/packages/ui/src/common/languages-types.ts @@ -23,4 +23,5 @@ export type Language = | 'polkadot-solidity' | 'stellar' | 'stylus' + | 'tron-solidity' | 'uniswap-hooks-solidity'; diff --git a/packages/ui/src/common/post-config.ts b/packages/ui/src/common/post-config.ts index 5b4e1eb15..f62a03423 100644 --- a/packages/ui/src/common/post-config.ts +++ b/packages/ui/src/common/post-config.ts @@ -10,6 +10,7 @@ export type DownloadAction = | 'download-file' | 'download-hardhat' | 'download-foundry' + | 'download-tronbox' | 'download-scaffold' | 'download-rust-stellar'; diff --git a/packages/ui/src/common/styles/vars.css b/packages/ui/src/common/styles/vars.css index 78b499dda..fd30d2106 100644 --- a/packages/ui/src/common/styles/vars.css +++ b/packages/ui/src/common/styles/vars.css @@ -41,6 +41,8 @@ --uniswap-pink: #f50eb4; + --tron-red: #ff060a; + /* Dimensions (scale taken from Tailwind) */ --size-1: 0.25rem; diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index 635fdc0d6..21101c25b 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -7,6 +7,7 @@ import ConfidentialApp from './confidential/App.svelte'; import PolkadotApp from './polkadot/App.svelte'; import StellarApp from './stellar/App.svelte'; import StylusApp from './stylus/App.svelte'; +import TronApp from './tron/App.svelte'; import UniswapHooksApp from './uniswap-hooks/App.svelte'; import VersionedApp from './common/VersionedApp.svelte'; import { postMessage } from './common/post-message'; @@ -23,6 +24,7 @@ import { compatibleContractsSemver as stellarSemver } from '@openzeppelin/wizard import { compatibleContractsSemver as stylusSemver } from '@openzeppelin/wizard-stylus'; import { compatibleContractsSemver as uniswapHooksSemver } from '@openzeppelin/wizard-uniswap-hooks'; import type { InitialOptions } from './common/initial-options'; +import { tronKindToUrlTab, tronUrlTabToKind } from './tron/url-tab-alias'; function postResize() { const { height } = document.documentElement.getBoundingClientRect(); @@ -48,7 +50,16 @@ const initialOpts: InitialOptions = { interface CompatibleSelection { compatible: true; - appType: 'solidity' | 'cairo' | 'cairo_alpha' | 'confidential' | 'polkadot' | 'stellar' | 'stylus' | 'uniswap-hooks'; + appType: + | 'solidity' + | 'cairo' + | 'cairo_alpha' + | 'confidential' + | 'polkadot' + | 'stellar' + | 'stylus' + | 'tron' + | 'uniswap-hooks'; } interface IncompatibleSelection { @@ -99,6 +110,14 @@ function evaluateSelection( return { compatible: false, compatibleVersionsSemver: soliditySemver }; } } + case 'tron': { + // Use Solidity Contracts semver — `@openzeppelin/tron-contracts` mirrors `@openzeppelin/contracts`. + if (requestedVersion === undefined || semver.satisfies(requestedVersion, soliditySemver)) { + return { compatible: true, appType: 'tron' }; + } else { + return { compatible: false, compatibleVersionsSemver: soliditySemver }; + } + } case 'stellar': { if (requestedVersion === undefined || semver.satisfies(requestedVersion, stellarSemver)) { return { compatible: true, appType: 'stellar' }; @@ -181,6 +200,10 @@ if (!selection.compatible) { case 'stylus': app = new StylusApp({ target: document.body, props: { initialTab, initialOpts } }); break; + case 'tron': + // TRON URLs use TRC-branded tab tokens (e.g. `#trc20`); map back to the internal kind. + app = new TronApp({ target: document.body, props: { initialTab: tronUrlTabToKind(initialTab), initialOpts } }); + break; case 'confidential': app = new ConfidentialApp({ target: document.body, props: { initialTab, initialOpts } }); break; @@ -201,7 +224,12 @@ if (!selection.compatible) { } app.$on('tab-change', (e: CustomEvent) => { - postMessage({ kind: 'oz-wizard-tab-change', tab: e.detail.toLowerCase() }); + let tab: string = e.detail; + if (selection.compatible && selection.appType === 'tron') { + // TRON surfaces the TRC-branded token in the URL/host (e.g. `ERC20` -> `trc20`). + tab = tronKindToUrlTab(tab); + } + postMessage({ kind: 'oz-wizard-tab-change', tab: tab.toLowerCase() }); }); export default app; diff --git a/packages/ui/src/polkadot/App.svelte b/packages/ui/src/polkadot/App.svelte index 0a2164905..d335886d8 100644 --- a/packages/ui/src/polkadot/App.svelte +++ b/packages/ui/src/polkadot/App.svelte @@ -22,7 +22,7 @@ const overrides: Overrides = { omitTabs: ['Account'], omitFeatures: defineOmitFeatures(), - omitZipFoundry: true, + omitZipFoundry: () => true, omitZipHardhat: (opts?: GenericOptions) => { return !!opts?.upgradeable; }, diff --git a/packages/ui/src/solidity/App.svelte b/packages/ui/src/solidity/App.svelte index 4e85f9908..fef7fdb91 100644 --- a/packages/ui/src/solidity/App.svelte +++ b/packages/ui/src/solidity/App.svelte @@ -57,6 +57,10 @@ export let tab: Kind = sanitizeKind(initialTab); $: { tab = sanitizeKind(tab); + // A `?tab=`/`#kind` URL can select a tab this ecosystem omits; clamp it back. + if (overrides.omitTabs.includes(tab)) { + tab = 'ERC20'; + } allowRendering(); dispatch('tab-change', tab); } @@ -119,7 +123,7 @@ } } - $: code = printContract(contract); + $: code = printContract(contract, overrides.printOptions); $: highlightedCode = injectHyperlinks(hljs.highlight('solidity', code).value); $: hasErrors = errors[tab] !== undefined; @@ -153,9 +157,12 @@ if (overrides.omitZipHardhat(opts)) { result.downloadHardhat = false; } - if (overrides.omitZipFoundry) { + if (overrides.omitZipFoundry(opts)) { result.downloadFoundry = false; } + if (overrides.omitOpenInRemix) { + result.openInRemix = false; + } return result; }; @@ -183,7 +190,9 @@ e.preventDefault(); if ((e.target as Element)?.classList.contains('disabled')) return; - const remappings = getVersionedRemappings(opts); + const remappings = overrides.overrideVersionedRemappings + ? overrides.overrideVersionedRemappings(opts) + : getVersionedRemappings(opts); window.open(remixURL(code, remappings, !!opts?.upgradeable).toString(), '_blank', 'noopener,noreferrer'); if (opts) { await postConfig(opts, 'remix', language); @@ -216,12 +225,17 @@ const zipFoundryModule = import('@openzeppelin/wizard/zip-env-foundry'); const downloadFoundryHandler = async () => { - const { zipFoundry } = await zipFoundryModule; - const zip = await zipFoundry(contract, opts); + const zip = + overrides.overrideZipFoundry !== undefined + ? await overrides.overrideZipFoundry(contract, opts) + : await (async () => { + const { zipFoundry } = await zipFoundryModule; + return zipFoundry(contract, opts); + })(); const blob = await zip.generateAsync({ type: 'blob' }); saveAs(blob, 'project.zip'); if (opts) { - await postConfig(opts, 'download-foundry', language); + await postConfig(opts, overrides.secondaryDownloadAction ?? 'download-foundry', language); } }; @@ -246,13 +260,23 @@
- - - - - + + + {#if !overrides.omitTabs.includes('Stablecoin')} + + {/if} + {#if !overrides.omitTabs.includes('RealWorldAsset')} + + {/if} {#if !overrides.omitTabs.includes('Account')} {/if} @@ -331,8 +355,11 @@ {/if} @@ -376,7 +403,11 @@
- +
diff --git a/packages/ui/src/solidity/GovernorControls.svelte b/packages/ui/src/solidity/GovernorControls.svelte index 821dbba04..0384b0ad7 100644 --- a/packages/ui/src/solidity/GovernorControls.svelte +++ b/packages/ui/src/solidity/GovernorControls.svelte @@ -13,9 +13,17 @@ const defaults = governor.defaults; + // Ecosystems that inherit the Solidity Wizard (e.g. TRON) can override the + // assumed block time so the voting delay/period are computed against their + // chain's cadence (TRON ~3s vs Ethereum ~12s). + export let defaultBlockTime: number | undefined = undefined; + + const effectiveBlockTime = defaultBlockTime ?? defaults.blockTime; + export let opts: Required = { kind: 'Governor', ...defaults, + blockTime: effectiveBlockTime, proposalThreshold: '', // default to empty in UI quorumAbsolute: '', // default to empty in UI info: { ...infoDefaults }, // create new object since Info is nested @@ -203,7 +211,7 @@ $1$2$3"`, - ); + let result = code + .replace( + importContractsRegex, + `"$1$2$3"`, + ) + .replace( + importTronContractsUpgradeableRegex, + `"$1$2$3"`, + ) + .replace( + importTronContractsRegex, + `"$1$2$3"`, + ); if (compatibleCommunityContractsGitCommit !== undefined) { result = result diff --git a/packages/ui/src/solidity/overrides.ts b/packages/ui/src/solidity/overrides.ts index 4fa2fc8e3..679f4ec2a 100644 --- a/packages/ui/src/solidity/overrides.ts +++ b/packages/ui/src/solidity/overrides.ts @@ -1,7 +1,8 @@ -import type { Contract, GenericOptions, Kind } from '@openzeppelin/wizard'; +import type { Contract, GenericOptions, Kind, Options as PrintOptions } from '@openzeppelin/wizard'; import type { ComponentType } from 'svelte'; import type { SupportedLanguage } from '../../api/ai-assistant/types/languages'; import type { Language } from '../common/languages-types'; +import type { DownloadAction } from '../common/post-config'; import type JSZip from 'jszip'; /** @@ -13,6 +14,14 @@ export interface Overrides { */ omitTabs: Kind[]; + /** + * Display-only labels for the kind tabs. Internal kind values stay the same + * (ERC20/ERC721/ERC1155/...) — only what users see changes. Used for + * ecosystems whose contract library renames the token standards + * (e.g. TRON uses TRC20/TRC721/TRC1155). + */ + tabLabels?: Partial>; + /** * Map from contract kind to features to omit */ @@ -29,9 +38,58 @@ export interface Overrides { overrideZipHardhat: ((c: Contract, opts?: GenericOptions) => Promise) | undefined; /** - * Whether to omit the Download Foundry package feature + * Whether to omit the Download Foundry package feature. + * Accepts the current generic options so ecosystems can gate on, for example, + * `opts.upgradeable` when their downstream toolchain doesn't support it + * (matches the shape of `omitZipHardhat`). + */ + omitZipFoundry: (opts?: GenericOptions) => boolean; + + /** + * Override for the second download tab (originally "Foundry"). When set, + * this function is called instead of the default `zipFoundry`; the tab + * label can be customized via `secondaryDownloadLabel`. + */ + overrideZipFoundry?: (c: Contract, opts?: GenericOptions) => Promise; + + /** + * Label overrides for the secondary (originally "Foundry") download tab. + * Set when an ecosystem replaces Foundry with a different toolchain + * (e.g. TronBox). + */ + secondaryDownloadLabel?: { + title: string; + description: string; + }; + + /** + * Analytics action emitted when the secondary download tab is used. + * Defaults to `'download-foundry'` to preserve the existing telemetry. */ - omitZipFoundry: boolean; + secondaryDownloadAction?: DownloadAction; + + /** + * Whether to omit the "Open in Remix" action. Useful for ecosystems whose + * import paths or contracts library Remix cannot resolve. + */ + 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`). + */ + overrideVersionedRemappings?: (opts?: GenericOptions) => 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 + * structured library profile (e.g. TRON's TRC* names + import paths) instead + * of post-processing rendered text. Each ecosystem zip generator applies the + * same profile internally; this hook only affects the UI-side rendering. + */ + printOptions?: PrintOptions; /** * A function to sanitize omitted features from the Solidity Wizard options. @@ -51,15 +109,30 @@ export interface Overrides { svelteComponent: ComponentType; language: SupportedLanguage; }; + + /** + * Override the default `blockTime` (seconds per block) that the Governor + * controls display and use for block-number-based voting durations. When + * unset, the Solidity default (12) is used. TRON sets this to 3. + */ + defaultBlockTime?: number; } export const defaultOverrides: Overrides = { omitTabs: [], + tabLabels: undefined, omitFeatures: new Map(), omitZipHardhat: () => false, overrideZipHardhat: undefined, - omitZipFoundry: false, + omitZipFoundry: () => false, + overrideZipFoundry: undefined, + secondaryDownloadLabel: undefined, + secondaryDownloadAction: undefined, + omitOpenInRemix: false, + overrideVersionedRemappings: undefined, + printOptions: undefined, sanitizeOmittedFeatures: (_: GenericOptions) => {}, postConfigLanguage: undefined, aiAssistant: undefined, + defaultBlockTime: undefined, }; diff --git a/packages/ui/src/standalone.css b/packages/ui/src/standalone.css index c8b8b122b..b5391ab0c 100644 --- a/packages/ui/src/standalone.css +++ b/packages/ui/src/standalone.css @@ -145,6 +145,10 @@ body { --color-2: #e3126f; } +.nav .switch.switch-tron.active { + --color-2: var(--tron-red); +} + .nav .switch.switch-stellar.active { --color-2: #0f0f0f; } diff --git a/packages/ui/src/tron/App.svelte b/packages/ui/src/tron/App.svelte new file mode 100644 index 000000000..6884c30d5 --- /dev/null +++ b/packages/ui/src/tron/App.svelte @@ -0,0 +1,88 @@ + + +
+ dispatch('tab-change', event.detail)} /> +
+ + diff --git a/packages/ui/src/tron/handle-unsupported-features.ts b/packages/ui/src/tron/handle-unsupported-features.ts new file mode 100644 index 000000000..eafe1a9ae --- /dev/null +++ b/packages/ui/src/tron/handle-unsupported-features.ts @@ -0,0 +1,24 @@ +import type { GenericOptions, Kind } from '@openzeppelin/wizard'; +import { sanitizeTronOptions } from '@openzeppelin/wizard'; + +/** + * Features that don't apply on TRON. + * + * `superchain` cross-chain bridging is OP Stack-specific (not relevant for TRON). + */ +export function defineOmitFeatures(): Map { + const omitFeatures: Map = new Map(); + // ERC20 is the only TRON tab with a `crossChainBridging` field (Stablecoin and + // RealWorldAsset are hidden — they depend on @openzeppelin/community-contracts). + omitFeatures.set('ERC20', ['superchain']); + return omitFeatures; +} + +// Shared with the CLI and MCP TRON surfaces via `@openzeppelin/wizard` so all +// three gate `superchain` the same way. The kind guard narrows the type for +// `sanitizeTronOptions` and skips kinds without a `crossChainBridging` field. +export function sanitizeOmittedFeatures(opts: GenericOptions) { + if (opts.kind === 'ERC20') { + sanitizeTronOptions(opts); + } +} diff --git a/packages/ui/src/tron/url-tab-alias.ts b/packages/ui/src/tron/url-tab-alias.ts new file mode 100644 index 000000000..d0c5eaff2 --- /dev/null +++ b/packages/ui/src/tron/url-tab-alias.ts @@ -0,0 +1,30 @@ +// Aliases the internal Solidity contract `Kind` to the TRON-branded token shown +// in the URL fragment, and back. This lets `/tron` URLs read `#trc20` (matching +// the tab labels and generated code) while the wizard keeps reusing the Solidity +// `ERC20` kind internally — TRON renames only the standard *names*, not the kinds. +// +// Only the standards TRON rebrands are aliased; kinds without a TRC counterpart +// (Governor, Custom) pass through unchanged. Legacy `#erc20`-style fragments also +// pass through and still resolve via `sanitizeKind`, so existing links keep working. + +// Internal kind -> lowercase URL token. Keep in sync with the `tabLabels` +// override in `tron/App.svelte`. +const KIND_TO_URL_TAB: Record = { + ERC20: 'trc20', + ERC721: 'trc721', + ERC1155: 'trc1155', +}; + +const URL_TAB_TO_KIND: Record = Object.fromEntries( + Object.entries(KIND_TO_URL_TAB).map(([kind, urlTab]) => [urlTab, kind]), +); + +/** Internal kind (e.g. `ERC20`) -> URL token (e.g. `trc20`). Unmapped kinds pass through. */ +export function tronKindToUrlTab(kind: string): string { + return KIND_TO_URL_TAB[kind] ?? kind; +} + +/** URL token (e.g. `trc20`) -> kind (e.g. `ERC20`). Unmapped/legacy tokens (e.g. `erc20`) pass through. */ +export function tronUrlTabToKind(urlTab: string | undefined): string | undefined { + return urlTab === undefined ? undefined : (URL_TAB_TO_KIND[urlTab] ?? urlTab); +}