Skip to content

Commit

Permalink
chore: hooks, init skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddentao committed Aug 20, 2023
1 parent 83362d7 commit a7ba4be
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 46 deletions.
4 changes: 2 additions & 2 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023-∞ Ramesh Nair
Copyright (c) 2023 [Ramesh Nair](https://hiddentao.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
# gemforge

Command-line tool for building, deploying and upgrading Diamond Standard contracts on EVM chains.
> Command-line tool for building, deploying and upgrading Diamond Standard contracts on EVM chains.
## Why



**gemforge** makes it easy to



## Features

* Auto-generates Diamond proxy code.
* Auto-calculates facet deployment and upgrades.
* Records diamond addresses to JSON file for history tracking.
* Supports [Foundry](https://github.com/foundry-rs/foundry) and [Hardhat](https://hardhat.org/) environments.
* Fully configurable for each project.

## Installation

_[Node.js](https://nodejs.org/) 16+ is required to run `gemforge`. We recommend using [nvm](https://github.com/nvm-sh/nvm) to handle different Node versions._

We recommend installing `gemforge` globally:

* pnpm: `pnpm add --global gemforge`
* npm: `npm install --global gemforge`
* yarn: `yarn global add gemforge`

## Usage

## Contributing



## License

MIT - see [LICENSE.md]
MIT - see [LICENSE.md](LICENSE.md)
12 changes: 12 additions & 0 deletions src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const command = () =>
.action(async args => {
const ctx = await getContext(args)

// run prebuild hook
if (ctx.config.hooks.preBuild) {
info('Running pre-build hook...')
await $$`${ctx.config.hooks.preBuild}`
}

const generatedSolidityPath = path.resolve(ctx.folder, ctx.config.paths.generated.solidity)
const generatedSupportPath = path.resolve(ctx.folder, ctx.config.paths.generated.support)

Expand Down Expand Up @@ -58,6 +64,12 @@ export const command = () =>
info('Running build...')
await $$`${ctx.config.commands.build}`

// run post build hook
if (ctx.config.hooks.postBuild) {
info('Running post-build hook...')
await $$`${ctx.config.hooks.postBuild}`
}

logSuccess()
})

Expand Down
45 changes: 32 additions & 13 deletions src/cli/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers } from 'ethers'
import { error, info } from '../shared/log.js'
import { getContext } from '../shared/context.js'
import { FacetDefinition, loadJson, readDeployedAddress, updateDeployedAddress } from '../shared/fs.js'
import { Context, getContext } from '../shared/context.js'
import { $$, FacetDefinition, loadJson, readDeployedAddress, updateDeployedAddress } from '../shared/fs.js'
import path from 'node:path'
import { createCommand, logSuccess } from './common.js'
import { ContractArtifact, OnChainContract, deployContract, execContractMethod, getContractAt, getContractValue, loadContractArtifact, setupNetwork, setupWallet } from '../shared/chain.js'
Expand All @@ -15,6 +15,12 @@ export const command = () =>
.action(async (networkArg, args) => {
const ctx = await getContext(args)

// run pre-deploy hook
if (ctx.config.hooks.preDeploy) {
info('Running pre-deploy hook...')
await $$`${ctx.config.hooks.preDeploy}`
}

info(`Selected network: ${networkArg}`)
const n = ctx.config.networks[networkArg]
if (!n) {
Expand All @@ -34,21 +40,23 @@ export const command = () =>

const generatedSupportPath = path.resolve(ctx.folder, ctx.config.paths.generated.support)
const deployedAddressesJsonPath = path.resolve(ctx.folder, 'gemforge.deployments.json')
const artifactsFolder = path.resolve(ctx.folder, ctx.config.paths.artifacts)

let proxyInterface: OnChainContract

let isNewDeployment = false

if (args.new) {
info('New deployment requested. Skipping any existing deployment...')
proxyInterface = await deployNewDiamond(artifactsFolder, signer)
proxyInterface = await deployNewDiamond(ctx, signer)
isNewDeployment = true
} else {
info(`Load existing deployment ...`)

const existing = readDeployedAddress(deployedAddressesJsonPath, network)
if (existing) {
info(` Existing deployment found at: ${existing}`)
info(`Checking if existing deployment is still valid...`)
proxyInterface = await getContractAt('IDiamondProxy', artifactsFolder, signer, existing)
proxyInterface = await getContractAt(ctx, 'IDiamondProxy', signer, existing)

const isDiamond = await getContractValue(proxyInterface, 'supportsInterface', ['0x01ffc9a7'])
if (!isDiamond) {
Expand All @@ -61,7 +69,8 @@ export const command = () =>
}
} else {
info(` No existing deployment found.`)
proxyInterface = await deployNewDiamond(artifactsFolder, signer)
proxyInterface = await deployNewDiamond(ctx, signer)
isNewDeployment = true
}
}

Expand All @@ -70,11 +79,11 @@ export const command = () =>
const facetContractNames = Object.keys(facets)
info(` ${facetContractNames.length} facets found.`)
const facetArtifacts = facetContractNames.reduce((m, name) => {
m[name] = loadContractArtifact(name, artifactsFolder)
m[name] = loadContractArtifact(ctx, name)
return m
}, {} as Record<string, ContractArtifact>)

info('Resolving what chaned need to be applied ...')
info('Resolving what changes need to be applied ...')
const changes = await resolveUpgrade(facetArtifacts, proxyInterface, signer)
info(` ${changes.facetsToDeploy.length} facets need to be deployed.`)
info(` ${changes.namedCuts.length} facet cuts need to be applied.`)
Expand All @@ -88,29 +97,39 @@ export const command = () =>
info('Deploying facets...')
await Promise.all(changes.facetsToDeploy.map(async name => {
info(` Deploying ${name} ...`)
const contract = await deployContract(name, artifactsFolder, signer)
const contract = await deployContract(ctx, name, signer)
facetContracts[name] = contract
info(` Deployed ${name} at: ${await contract.address}`)
}))
} else {
info('No new facets need to be deployed.')
}

info('Upgrading the diamond proxy...')
if (isNewDeployment && ctx.config.diamond.init) {
info(`Deploying initialization contract...`)
}

info('Calling diamondCut() on the proxy...')
const cuts = getFinalizedFacetCuts(changes.namedCuts, facetContracts)
await execContractMethod(proxyInterface, 'diamondCut', [cuts, ethers.ZeroAddress, '0x'])
}

info(`Saving deployment info...`)
updateDeployedAddress(deployedAddressesJsonPath, network, proxyInterface.address)

// run post-deploy hook
if (ctx.config.hooks.postDeploy) {
info('Running post-deploy hook...')
await $$`${ctx.config.hooks.postDeploy}`
}

logSuccess()
})


const deployNewDiamond = async (artifactsFolder: string, signer: Signer) => {
const deployNewDiamond = async (ctx: Context, signer: Signer) => {
info(`Deploying diamond...`)
const diamond = await deployContract('DiamondProxy', artifactsFolder, signer, await signer.getAddress())
const diamond = await deployContract(ctx, 'DiamondProxy', signer, await signer.getAddress())
info(` DiamondProxy deployed at: ${diamond.address}`)
return await getContractAt('IDiamondProxy', artifactsFolder, signer, diamond.address)
return await getContractAt(ctx, 'IDiamondProxy', signer, diamond.address)
}
30 changes: 23 additions & 7 deletions src/shared/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Contract, ethers, Signer, TransactionResponse } from "ethers";
import { MnemonicWalletConfig, NetworkConfig, WalletConfig } from "./config.js";
import { error, trace } from "./log.js";
import { Provider } from "ethers";
import { loadJson } from "./fs.js";
import { getArtifactsFolderPath, loadJson } from "./fs.js";
import { TransactionReceipt } from "ethers";
import { Fragment } from "ethers";
import { Context } from "./context.js";
import path from "node:path";


export interface Network {
Expand Down Expand Up @@ -50,14 +52,28 @@ export interface ContractArtifact {
deployedBytecode: string,
}

export const loadContractArtifact = (name: string, basePath: string) => {
export const loadContractArtifact = (ctx: Context, name: string) => {
trace(`Loading contract artifact: ${name} ...`)

const artifactsFolder = getArtifactsFolderPath(ctx)
let filePath = ''

switch (ctx.config.artifacts.format) {
case 'foundry':
filePath = `${artifactsFolder}/${name}.sol/${name}.json`
break
case 'hardhat':
filePath = `${artifactsFolder}/${name}.json`
break
default:
error(`Unknown artifacts format: ${ctx.config.artifacts.format}`)
}

const {
abi,
bytecode: { object: bytecode },
deployedBytecode: { object: deployedBytecode },
} = loadJson(`${basePath}/${name}.sol/${name}.json`) as any
} = loadJson(filePath) as any

return { name, abi, bytecode, deployedBytecode } as ContractArtifact
}
Expand All @@ -68,9 +84,9 @@ export interface OnChainContract {
contract: Contract
}

export const getContractAt = async (name: string, artifactsFolder: string, signer: Signer, address: string): Promise<OnChainContract> => {
export const getContractAt = async (ctx: Context, name: string, signer: Signer, address: string): Promise<OnChainContract> => {
try {
const artifact = loadContractArtifact(name, artifactsFolder)
const artifact = loadContractArtifact(ctx, name)
const factory = new ethers.ContractFactory(artifact.abi, artifact.bytecode, signer)

return {
Expand All @@ -97,9 +113,9 @@ export const getContractAtUsingArtifact = async (artifact: ContractArtifact, sig
}
}

export const deployContract = async (name: string, artifactsFolder: string, signer: Signer, ...args: any[]): Promise<OnChainContract> => {
export const deployContract = async (ctx: Context, name: string, signer: Signer, ...args: any[]): Promise<OnChainContract> => {
try {
const artifact = loadContractArtifact(name, artifactsFolder)
const artifact = loadContractArtifact(ctx, name)
const factory = new ethers.ContractFactory(artifact.abi, artifact.bytecode, signer)
trace(`Deployed ${name} ...`)
const tx = await factory.deploy(...args)
Expand Down
54 changes: 37 additions & 17 deletions src/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ export interface GemforgeConfig {
diamond: string,
}
},
facets: {
diamond: {
publicMethods: boolean,
init: string,
},
hooks: {
preBuild: string,
postBuild: string,
preDeploy: string,
postDeploy: string,
},
artifacts: {
format: 'foundry' | 'hardhat',
},
wallets: {
[name: string]: WalletConfig,
Expand All @@ -60,7 +70,7 @@ const ensure = (config: GemforgeConfig, key: string, isValid: (v: any) => boolea
}
}

const ensureExists = (config: GemforgeConfig, key: string) => {
const ensureIsSet = (config: GemforgeConfig, key: string) => {
const val = get(config, key)
if (!val) {
throwError(`Value not found`, key)
Expand All @@ -83,49 +93,59 @@ const ensureBool = (config: GemforgeConfig, key: string) => {

export const sanitizeConfig = (config: GemforgeConfig) => {
// solc
ensureExists(config, 'solc.version')
ensureIsSet(config, 'solc.version')
ensure(config, 'solc.license', (v: any) => spdxLicenseIds.indexOf(v) >= 0, 'Invalid SPDX license ID')

// commands
ensureExists(config, 'commands.build')
ensureIsSet(config, 'commands.build')

// paths
ensureExists(config, 'paths.artifacts')
ensureIsSet(config, 'paths.artifacts')
ensureArray(config, 'paths.src.facets')
ensureExists(config, 'paths.generated.solidity')
ensureExists(config, 'paths.generated.support')
ensureExists(config, 'paths.lib.diamond')

// facets
ensureBool(config, 'facets.publicMethods')

ensureIsSet(config, 'paths.generated.solidity')
ensureIsSet(config, 'paths.generated.support')
ensureIsSet(config, 'paths.lib.diamond')

// diamond
ensureBool(config, 'diamond.publicMethods')
ensure(config, 'diamond.init', (v: any) => typeof v === 'undefined' || typeof v === 'string', 'Invalid init contract value')

// artifacts
ensure(config, 'artifacts.format', (v: any) => ['foundry', 'hardhat'].indexOf(v) >= 0, 'Invalid artifacts format')

// hooks
ensure(config, 'hooks.preBuild', (v: any) => typeof v === 'undefined' || typeof v === 'string', 'Invalid preBuild hook')
ensure(config, 'hooks.postBuild', (v: any) => typeof v === 'undefined' || typeof v === 'string', 'Invalid postBuild hook')
ensure(config, 'hooks.preDeploy', (v: any) => typeof v === 'undefined' || typeof v === 'string', 'Invalid preDeploy hook')
ensure(config, 'hooks.postDeploy', (v: any) => typeof v === 'undefined' || typeof v === 'string', 'Invalid postDeploy hook')

// wallets
ensureExists(config, 'wallets')
ensureIsSet(config, 'wallets')
const walletNames = Object.keys(config.wallets)
if (!walletNames.length) {
throwError(`No value found`, 'wallets')
}
walletNames.forEach(name => {
ensure(config, `wallets.${name}.type`, (v: any) => ['mnemonic'].indexOf(v) >= 0, 'Invalid wallet type')

ensureExists(config, `wallets.${name}.config`)
ensureIsSet(config, `wallets.${name}.config`)

const type = get(config, `wallets.${name}.type`)
switch (type) {
case 'mnemonic':
ensureExists(config, `wallets.${name}.config.words`)
ensureIsSet(config, `wallets.${name}.config.words`)
ensure(config, `wallets.${name}.config.index`, (v: any) => typeof v === 'number' && v >= 0, 'Invalid number')
}
})

// networks
ensureExists(config, 'networks')
ensureIsSet(config, 'networks')
const networkNames = Object.keys(config.networks)
if (!networkNames.length) {
throwError(`No value found`, 'networks')
}
networkNames.forEach(name => {
ensureExists(config, `networks.${name}.rpcUrl`)
ensureIsSet(config, `networks.${name}.rpcUrl`)
ensure(config, `networks.${name}.wallet`, (v: any) => walletNames.indexOf(v) >= 0, 'Invalid wallet')
})
}
Expand Down
8 changes: 6 additions & 2 deletions src/shared/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,12 @@ export interface FacetDefinition {
}[],
}

export const getArtifactsFolderPath = (ctx: Context): string => {
return path.resolve(ctx.folder, ctx.config.paths.artifacts)
}

export const getFacetsAndFunctions = (ctx: Context): FacetDefinition[] => {
if (ctx.config.facets.publicMethods) {
if (ctx.config.diamond.publicMethods) {
trace('Including public methods in facet cuts')
}

Expand Down Expand Up @@ -182,7 +186,7 @@ export const getFacetsAndFunctions = (ctx: Context): FacetDefinition[] => {
functionDefinitions = functionDefinitions
.filter(node => !node.isConstructor && !node.isFallback && !node.isReceiveEther)
.filter(
node => node.visibility === 'external' || (ctx.config.facets.publicMethods && node.visibility === 'public')
node => node.visibility === 'external' || (ctx.config.diamond.publicMethods && node.visibility === 'public')
)

// export declare type TypeName = ElementaryTypeName | UserDefinedTypeName | ArrayTypeName;
Expand Down
Loading

0 comments on commit a7ba4be

Please sign in to comment.