UNPKG

superfuse-wizard

Version:

Interactive smart contract generator based on Superchain 's interoperability standard.

365 lines (309 loc) 10.4 kB
import type { Contract, Parent, ContractFunction, FunctionArgument, Value, NatspecTag, ImportContract, ContractModifier } from './contract'; import type { Options, Helpers }from './options'; import { withHelpers } from './options'; import type { Lines} from '../utils/format-lines'; import { formatLines, spaceBetween } from '../utils/format-lines'; import { mapValues } from '../utils/map-values'; import SOLIDITY_VERSION from './solidity-version.json' assert { type: "json" }; import { inferTranspiled } from './infer-transpiled'; export function printContract(contract: Contract, opts?: Options): string { const helpers = withHelpers(contract, opts); const fns = mapValues( sortedFunctions(contract), fns => fns.map(fn => printFunction(fn, helpers)), ); const mods = mapValues( sortedModifiers(contract), mods => mods.map(mod => printModifier(mod, helpers)), ); const hasOverrides = fns.override.some(l => l.length > 0); return formatLines( ...spaceBetween( [ `// SPDX-License-Identifier: ${contract.license}`, `pragma solidity ^${SOLIDITY_VERSION};`, ], // contract.imports.map(p => { // const names = p.name.split(', ').map(name => ({ name } as ReferencedContract)); // const transformedNames = helpers.transformNames(names).join(', '); // return transformedNames === '' ? `import "${helpers.transformImport(p).path}";` : `import {${transformedNames}} from "${helpers.transformImport(p).path}";`; // }), printImports(contract.imports, helpers), contract.outsideCode, contract.userDefinedTypes.map(p => `type ${p.newType} is ${p.underlyingType};`), [ ...printNatspecTags(contract.natspecTags), [`contract ${contract.name}`, ...printInheritance(contract, helpers), '{'].join(' '), contract.usings.map(p => `using ${p.library.name} for ${p.usingFor} ;`), spaceBetween( contract.variables, ...mods.code, printConstructor(contract, helpers), printReceive(contract), printFallback(contract), ...fns.code, ...fns.modifiers, hasOverrides ? [`// The following functions are overrides required by Solidity.`] : [], ...fns.override, ), `}`, ], ), ); } function printInheritance(contract: Contract, { transformName }: Helpers): [] | [string] { if (contract.parents.length > 0) { return ['is ' + contract.parents.map(p => transformName(p.contract)).join(', ')]; } else { return []; } } function printImports(imports: ImportContract[], helpers: Helpers): string[] { // Sort imports by name imports.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); const lines: string[] = []; imports.map(p => { const importContract = helpers.transformImport(p); if (importContract.name === ''){ lines.push(`import "${importContract.path}";`); } else{ lines.push(`import {${importContract.name}} from "${importContract.path}";`); } }); return lines; } function printConstructor(contract: Contract, helpers: Helpers): Lines[] { const hasParentParams = contract.parents.some(p => p.params.length > 0); const hasConstructorCode = contract.constructorCode.length > 0; const parentsWithInitializers = contract.parents.filter(hasInitializer); if (hasParentParams || hasConstructorCode || (helpers.upgradeable && parentsWithInitializers.length > 0)) { const parents = parentsWithInitializers .flatMap(p => printParentConstructor(p, helpers)); const modifiers = helpers.upgradeable ? ['initializer public'] : parents; const args = contract.constructorArgs.map(a => printArgument(a, helpers)); const body = helpers.upgradeable ? spaceBetween( parents.map(p => p + ';'), contract.constructorCode, ) : contract.constructorCode; const head = helpers.upgradeable ? 'function initialize' : 'constructor'; const constructor = printFunction2( [], head, args, modifiers, body, ); if (!helpers.upgradeable) { return constructor; } else { return spaceBetween( DISABLE_INITIALIZERS, constructor, ); } } else if (!helpers.upgradeable) { return []; } else { return DISABLE_INITIALIZERS; } } const DISABLE_INITIALIZERS = [ '/// @custom:oz-upgrades-unsafe-allow constructor', 'constructor() {', [ '_disableInitializers();' ], '}' ]; function hasInitializer(parent: Parent) { // CAUTION // This list is validated by compilation of SafetyCheck.sol. // Always keep this list and that file in sync. return !['Initializable'].includes(parent.contract.name); } type SortedModifiers = Record<'code', ContractModifier[]>; function sortedModifiers(contract: Contract): SortedModifiers { const mods: SortedModifiers = { code: [] }; for (const mod of contract.modifiers) { if (mod.code.length > 0) { mods.code.push(mod); } } return mods; } type SortedFunctions = Record<'code' | 'modifiers' | 'override', ContractFunction[]>; // Functions with code first, then those with modifiers, then the rest function sortedFunctions(contract: Contract): SortedFunctions { const fns: SortedFunctions = { code: [], modifiers: [], override: [] }; for (const fn of contract.functions) { if (fn.code.length > 0) { fns.code.push(fn); } else if (fn.modifiers.length > 0) { fns.modifiers.push(fn); } else { fns.override.push(fn); } } return fns; } function printParentConstructor({ contract, params }: Parent, helpers: Helpers): [] | [string] { const useTranspiled = helpers.upgradeable && inferTranspiled(contract); const fn = useTranspiled ? `__${contract.name}_init` : contract.name; if (useTranspiled || params.length > 0) { return [ fn + '(' + params.map(printValue).join(', ') + ')', ]; } else { return []; } } export function printValue(value: Value): string { if (typeof value === 'object') { if ('lit' in value) { return value.lit; } else if ('note' in value) { return `${printValue(value.value)} /* ${value.note} */`; } else { throw Error('Unknown value type'); } } else if (typeof value === 'number') { if (Number.isSafeInteger(value)) { return value.toFixed(0); } else { throw new Error(`Number not representable (${value})`); } } else { return JSON.stringify(value); } } function printReceive(contract: Contract): Lines[] { // const hasParentParams = contract.parents.some(p => p.params.length > 0); const hasReceiveCodeCode = contract.receiveCode.length > 0; // const parentsWithInitializers = contract.parents.filter(hasInitializer); if (hasReceiveCodeCode ) { // const parents = parentsWithInitializers // .flatMap(p => printParentConstructor(p, helpers)); const modifiers: string[] = ['external payable', ...contract.receiveModifiers]; const args: string[] = []; const body = contract.receiveCode; const head = 'receive' const receive = printFunction2( [], head, args, modifiers, body, ); return receive; } else { return []; } } function printFallback(contract: Contract): Lines[] { const hasFallbackCode = contract.fallbackCode.length > 0; if (hasFallbackCode ) { const modifiers = ['external payable']; const args: string[] = []; const body = contract.fallbackCode; const head = 'fallback' const fallback = printFunction2( [], head, args, modifiers, body, ); return fallback; } else { return []; } } function printModifier(mod: ContractModifier, helpers: Helpers): Lines[] { const modifiers: string[] = []; const code = [...mod.code]; if (mod.code.length >= 1) { return printFunction2( [], 'modifier ' + mod.name, mod.args.map(a => printArgument(a, helpers)), modifiers, code, ); } else { return []; } } function printFunction(fn: ContractFunction, helpers: Helpers): Lines[] { const { transformName } = helpers; if (fn.override.size <= 1 && fn.modifiers.length === 0 && fn.code.length === 0 && !fn.final) { return [] } const modifiers: string[] = [fn.kind, ...fn.modifiers]; if (fn.mutability !== 'nonpayable') { modifiers.splice(1, 0, fn.mutability); } if (fn.override.size === 1) { modifiers.push(`override`); } else if (fn.override.size > 1) { modifiers.push(`override(${[...fn.override].map(transformName).join(', ')})`); } if (fn.returns?.length) { modifiers.push(`returns (${fn.returns.join(', ')})`); } const code = [...fn.code]; if (fn.override.size > 0 && !fn.final) { const superCall = `super.${fn.name}(${fn.args.map(a => a.name).join(', ')});`; code.push(fn.returns?.length ? 'return ' + superCall : superCall); } if (modifiers.length + fn.code.length > 1) { return printFunction2( fn.comments, 'function ' + fn.name, fn.args.map(a => printArgument(a, helpers)), modifiers, code, ); } else { return []; } } // generic for functions and constructors // kindedName = 'function foo' or 'constructor' function printFunction2(comments: string[],kindedName: string, args: string[], modifiers: string[], code: Lines[]): Lines[] { const fn: Lines[] = [ ...comments ]; const headingLength = [kindedName, ...args, ...modifiers] .map(s => s.length) .reduce((a, b) => a + b); const braces = code.length > 0 ? '{' : '{}'; if (headingLength <= 72) { fn.push([`${kindedName}(${args.join(', ')})`, ...modifiers, braces].join(' ')); } else { fn.push(`${kindedName}(${args.join(', ')})`, modifiers, braces); } if (code.length > 0) { fn.push(code, '}'); } return fn; } function printArgument(arg: FunctionArgument, { transformName }: Helpers): string { let type: string; if (typeof arg.type === 'string') { if (/^[A-Z]/.test(arg.type)) { `Type ${arg.type} is not a primitive type. Define it as a ContractReference`; } type = arg.type; } else { type = transformName(arg.type); } return [type, arg.name].join(' '); } function printNatspecTags(tags: NatspecTag[]): string[] { return tags.map(({ key, value }) => `/// ${key} ${value}`); }