Skip to content

Commit

Permalink
Merge pull request #47 from decentraland/feature/dynamic-contracts
Browse files Browse the repository at this point in the history
feat: Generate contract manifest from the ABIs. Trim artifacts on build.
  • Loading branch information
abarmat authored Feb 22, 2018
2 parents 7e45d29 + 450242b commit 409389d
Show file tree
Hide file tree
Showing 25 changed files with 1,191 additions and 790 deletions.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
"docs": "npx jsdoc -c conf.json -r src/**/*.js",
"lint": "npx eslint .",
"lint:fix": "npx eslint --fix .",
"init": "mkdir dist",
"contracts": "mkdir -p dist/contracts/artifacts && cp -r src/contracts/artifacts dist/contracts",
"init": "mkdir -p dist",
"trim-artifacts": "./scripts/contracts/index.js trim-artifacts --write",
"copy-contracts": "mkdir -p dist/contracts/artifacts && cp -r src/contracts/artifacts dist/contracts",
"contracts-manifest": "./scripts/contracts/index.js generate-manifest --write",
"contracts": "npm run trim-artifacts && npm run copy-contracts && npm run contracts-manifest",
"clean": "rm -rf dist",
"prebuild": "npm run lint && npm run test && npm run clean && npm run init && npm run contracts",
"prebuild": "npm run clean && npm run lint && npm run contracts && npm run test && npm run init",
"build": "babel ./src -d ./dist --ignore specs",
"fastbuild": "npm run clean && npm run init && babel ./src -d ./dist --ignore specs",
"test": "npx mocha -r babel-register -r specs/spec_utils.js specs/*.spec.js",
Expand Down
25 changes: 25 additions & 0 deletions scripts/contracts/Artifact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import path from 'path'
import { fsReadFilePromise } from './utils'

export class Artifact {
static PROPERTY_BLACKLIST = [
'bytecode',
'sourceMap',
'deployedSourceMap',
'sourcePath',
'ast',
'compiler'
]

constructor(filePath) {
this.path = filePath
this.name = path.basename(filePath, path.extname(filePath))
}

async trim() {
let content = await fsReadFilePromise(this.path)
content = JSON.parse(content)
Artifact.PROPERTY_BLACKLIST.forEach(prop => delete content[prop])
return JSON.stringify(content, null, 2)
}
}
44 changes: 44 additions & 0 deletions scripts/contracts/Artifacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import path from 'path'
import { Artifact } from './Artifact'
import { findFolderPath, globPromise, fsWriteFilePromise } from './utils'

const DEFAULT_FOLDER_PATH = 'src/contracts/artifacts'

export class Artifacts {
static PROPERTY_BLACKLIST = [
'bytecode',
'sourceMap',
'deployedSourceMap',
'sourcePath',
'ast',
'compiler'
]

constructor(folderPath) {
folderPath = folderPath || findFolderPath(DEFAULT_FOLDER_PATH)
this.folderPath = folderPath
this.collection = []
}

async buildCollection() {
const paths = await this.getPaths()
this.collection = paths.map(path => new Artifact(path))
return this.collection
}

async getPaths() {
const artifactsPattern = path.join(this.folderPath, 'MANAToken.json')
return await globPromise(artifactsPattern)
}

async trim() {
return await Promise.all(this.collection.map(artifact => artifact.trim()))
}

async write() {
for (const artifact of this.collection) {
const content = await artifact.trim()
await fsWriteFilePromise(artifact.path, content)
}
}
}
59 changes: 59 additions & 0 deletions scripts/contracts/ContractFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import path from 'path'

export class ContractFile {
constructor(filePath) {
this.path = filePath
this.name = path.basename(filePath, path.extname(filePath))
this.abi = this.getAbi()
}

isIndex() {
return this.name === 'index'
}

getAbi() {
if (this.isIndex()) return

try {
const Contract = require(this.path)[this.name]
return Contract.getDefaultAbi()
} catch (error) {
console.log(
`Could not find an artifact for "${this.path}"`,
error.message
)
}
}

getExtensions() {
const extensions = {}

for (const method of this.abi) {
const { name, inputs, stateMutability, type } = method

switch (type) {
case 'function': {
const args = [
...inputs.map((input, index) => input.name || `input${index}`),
'...args'
].join(', ')

if (stateMutability === 'view') {
extensions[name] = `function(${args}) {
return this.call('${name}', ${args})
}`
} else if (stateMutability === 'nonpayable') {
extensions[name] = `function(${args}) {
return this.transaction('${name}', ${args})
}`
}
break
}
default:
break
}
}

return extensions
}
}
25 changes: 25 additions & 0 deletions scripts/contracts/Formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from 'fs'
import prettier from 'prettier'

import { walkUp } from './utils'

export class Formatter {
format(text) {
return prettier.format(text, this.getPrettierOptions())
}

getPrettierOptions() {
let eslintRules = null

walkUp('.eslintrc', function(eslintrcPath) {
if (fs.existsSync(eslintrcPath)) {
const eslintrcText = fs.readFileSync(eslintrcPath, 'utf8')
eslintRules = JSON.parse(eslintrcText).rules

return true
}
})

return eslintRules ? eslintRules['prettier/prettier'][1] : {}
}
}
56 changes: 56 additions & 0 deletions scripts/contracts/IndexFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ContractFile } from './ContractFile'

export class IndexFile {
constructor(contractPaths) {
this.contractPaths = contractPaths
this.contracts = this.instantiateContracts()
}

instantiateContracts() {
return this.contractPaths
.map(path => new ContractFile(path))
.filter(contract => !!contract.abi)
}

build() {
return `
// This is auto-generated code
// Changes here will be lost
${this.buildImports()}
${this.buildDefinitions()}
${this.buildExports()}`
}

buildImports() {
return this.contracts
.map(contract => `import { ${contract.name} } from './${contract.name}'`)
.join('\n')
}

buildDefinitions() {
return this.contracts
.map(contract => {
const extensions = contract.getExtensions()
const extensionObject = Object.keys(extensions)
.map(name => `${name}: ${extensions[name]}`)
.join(',\n\t\t')

return `Object.assign(
${contract.name}.prototype, {
${extensionObject}
},
${contract.name}.prototype
)`
})
.join('\n\n')
}

buildExports() {
return `export {
${this.contracts.map(contract => contract.name)}
}`
}
}
21 changes: 21 additions & 0 deletions scripts/contracts/Manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'path'
import { findFolderPath, globPromise, fsWriteFilePromise } from './utils'

const DEFAULT_FOLDER_PATH = 'src/contracts'

export class Manifest {
constructor(folderPath) {
folderPath = folderPath || findFolderPath(DEFAULT_FOLDER_PATH)
this.folderPath = folderPath
}

getPaths() {
const contractsPattern = path.join(this.folderPath, '*.js')
return globPromise(contractsPattern)
}

write(text = '') {
const manifestPath = path.join(this.folderPath, 'index.js')
return fsWriteFilePromise(manifestPath, text)
}
}
81 changes: 81 additions & 0 deletions scripts/contracts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env babel-node

import program from 'commander'

import { Manifest } from './Manifest'
import { Artifacts } from './Artifacts'
import { IndexFile } from './IndexFile'
import { Formatter } from './Formatter'

export function main() {
program
.command('generate-manifest')
.option(
'--folderPath [folderPath]',
'Folder containing the contract files. By default it will try to find the nearest src/contracts folder'
)
.option(
'--write',
'Whether write the file or just print it. Defaults to false',
false
)
.action(async options => {
const manifest = new Manifest(options.folderPath)
const indexFile = await generateIndex(manifest)

if (options.write) {
await manifest.write(indexFile)
console.log(`index file written on "${manifest.folderPath}"`)
} else {
console.log(indexFile)
}
})

program
.command('trim-artifacts')
.option(
'--folderPath [folderPath]',
'Folder containing the contract files. By default it will try to find the nearest src/contracts folder'
)
.option(
'--write',
'Whether write the file or just print it. Defaults to false',
false
)
.action(async options => {
const artifacts = new Artifacts(options.folderPath)
await artifacts.buildCollection()

console.log(`Trimming artifacts on ${artifacts.folderPath}`)

if (options.write) {
await artifacts.write()
console.log(`artifacts written on "${artifacts.folderPath}"`)
} else {
const content = await artifacts.trim()
console.log(content)
}
})

program.parse(process.argv)
}

async function generateIndex(manifest) {
console.log(`Geneating manifest for "${manifest.folderPath}"`)

const contractPaths = await manifest.getPaths()

console.log(`Found ${contractPaths.length} contracts`)
console.log('Building index file')

const index = new IndexFile(contractPaths)
const indexFile = index.build()

console.log('Index built successufully')

return new Formatter().format(indexFile)
}

if (require.main === module) {
main()
}
Loading

0 comments on commit 409389d

Please sign in to comment.