packages/solidity-devops/js/utils/deployments.js
const fs = require('fs')
const path = require('path')
const { getChainId, hasCode } = require('./chain.js')
const { readConfigValue } = require('./config.js')
const { logSuccess, logInfo, logError, logWarning } = require('./logger.js')
const { areEqualAddresses, createDir } = require('./utils.js')
const { assertCondition } = require('./utils.js')
/**
* Creates and initializes the deployment directories for the chain, if they don't exist.
*
* @param {string} chainName - The name of the chain
*/
const createDeploymentDirs = (chainName) => {
const freshDeployments = readConfigValue('freshDeployments')
createDir(freshDeployments, chainName)
const deployments = readConfigValue('deployments')
createDir(deployments, chainName)
createChainIdFile(deployments, chainName)
}
const createChainIdFile = (deployments, chainName) => {
const chainIdFile = `${deployments}/${chainName}/.chainId`
// Exit if the chain ID file already exists
if (fs.existsSync(chainIdFile)) {
return
}
const chainId = getChainId(chainName)
fs.writeFileSync(chainIdFile, chainId)
}
const saveNewDeployment = (chainName, contractAlias, potentialReceipts) => {
// No-op if artifact has already been saved
const deploymentFN = getDeploymentFN(chainName, contractAlias)
if (fs.existsSync(deploymentFN)) {
logInfo(`Deployment artifact already exists for ${contractAlias}`)
return
}
const artifact = getConfirmedFreshDeployment(chainName, contractAlias)
if (!artifact) {
return
}
// Find the matching receipt
const receipt = potentialReceipts.find((r) =>
areEqualAddresses(r.address, artifact.address)
)
if (!receipt) {
logInfo(`No receipt found for ${contractAlias} at ${artifact.address}`)
return
}
// Add receipt.hash and receipt.blockNumber to the artifact, but don't add receipt.address
artifact.receipt = {
hash: receipt.hash,
blockNumber: receipt.blockNumber,
}
// Add ABI from the build artifact
const buildArtifact = getBuildArtifact(contractAlias)
if (!buildArtifact || !buildArtifact.abi) {
logInfo(`No ABI found for ${contractAlias}`)
} else {
artifact.abi = buildArtifact.abi
}
// Save the artifact
saveDeploymentArtifact(chainName, contractAlias, artifact)
}
const saveDeploymentArtifact = (chainName, contractAlias, artifact) => {
const deploymentFN = getDeploymentFN(chainName, contractAlias)
fs.writeFileSync(deploymentFN, JSON.stringify(artifact, null, 2))
}
/**
* Extracts the contract name from the contract alias: everything before the first dot.
* Example: 'LinkedPool' from 'LinkedPool.USDC'
*
* @param {string} contractAlias - The contract alias
* @returns {string} The contract name
*/
const getContractName = (contractAlias) => {
return contractAlias.split('.')[0]
}
const getDeploymentFN = (chainName, contractAlias) => {
const deployments = readConfigValue('deployments')
return `${deployments}/${chainName}/${contractAlias}.json`
}
const getFreshDeploymentFN = (chainName, contractAlias) => {
const freshDeployments = readConfigValue('freshDeployments')
return `${freshDeployments}/${chainName}/${contractAlias}.json`
}
const getBuildArtifactFN = (contractAlias) => {
const contractName = getContractName(contractAlias)
const forgeArtifacts = readConfigValue('forgeArtifacts')
return `${forgeArtifacts}/${contractName}.sol/${contractName}.json`
}
const getBuildArtifact = (contractAlias) => {
const artifactFN = getBuildArtifactFN(contractAlias)
// Silent exit if the artifact file does not exist
if (!fs.existsSync(artifactFN)) {
logError(`No artifact file found for ${contractAlias} at ${artifactFN}`)
return null
}
return JSON.parse(fs.readFileSync(artifactFN))
}
const getConfirmedFreshDeployment = (chainName, contractAlias) => {
const freshDeploymentFN = getFreshDeploymentFN(chainName, contractAlias)
// Silent exit if the fresh deployment file does not exist
if (!fs.existsSync(freshDeploymentFN)) {
logError(
`No fresh deployment file found for ${contractAlias} at ${freshDeploymentFN}`
)
return null
}
const artifact = JSON.parse(fs.readFileSync(freshDeploymentFN))
assertCondition(
'address' in artifact,
`Missing address in fresh deployment file ${freshDeploymentFN}`
)
const address = artifact.address
// Silent exit if the contract is not deployed
if (!hasCode(chainName, address)) {
logWarning(`${contractAlias} is NOT deployed at ${address} on ${chainName}`)
return null
}
logSuccess(`${contractAlias} is deployed at ${address} on ${chainName}`)
return artifact
}
const getSavedDeployment = (chainName, contractAlias) => {
const deploymentFN = getDeploymentFN(chainName, contractAlias)
// Silent exit if the deployment file does not exist
if (!fs.existsSync(deploymentFN)) {
logError(`No deployment file found for ${contractAlias} at ${deploymentFN}`)
return null
}
return JSON.parse(fs.readFileSync(deploymentFN))
}
const getNewDeployments = (chainName, timestamp) => {
const freshDeployments = readConfigValue('freshDeployments')
const chainDir = `${freshDeployments}/${chainName}`
// Looks for files created after the given timestamp
// Then remove the extension and return the list of file names
const files = fs.readdirSync(chainDir)
return files
.filter((file) => {
const stats = fs.statSync(`${chainDir}/${file}`)
return file.endsWith('.json') && stats.mtimeMs > timestamp
})
.map((file) => file.slice(0, -5))
}
const getAllDeploymentReceipts = (chainName) => {
// Get the list of script-related directories in the 'broadcast' directory, excluding files
const scriptDirs = fs
.readdirSync('broadcast', { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
const chainId = getChainId(chainName)
// We are interested in all 'broadcast/scriptDir/chainId/*.json' files for every scriptDir in the list
return scriptDirs.flatMap((scriptDir) => {
const scriptDirPath = path.join('broadcast', scriptDir, chainId)
if (!fs.existsSync(scriptDirPath)) {
return []
}
return fs
.readdirSync(scriptDirPath)
.filter((f) => f.endsWith('.json'))
.flatMap((broadcastFN) =>
extractDeploymentReceipts(path.join(scriptDirPath, broadcastFN))
)
})
}
const getNewDeploymentReceipts = (chainName, scriptFN, timestamp) => {
const chainId = getChainId(chainName)
const scriptBaseName = path.basename(scriptFN)
const broadcastDir = path.join('broadcast', scriptBaseName, chainId)
// Silent exit if the broadcast directory does not exist
if (!fs.existsSync(broadcastDir)) {
logError(
`No broadcast directory found for ${scriptBaseName} at ${broadcastDir}`
)
return []
}
// Look for "*-latest.json" files created after the given timestamp.
// These are named after the script entry function, which is usually "run", but could be different.
// In practice there should be only one file, but we implement a generic logic just in case.
return fs
.readdirSync(broadcastDir)
.filter((fn) => {
if (!fn.endsWith('-latest.json')) {
return false
}
const stats = fs.statSync(path.join(broadcastDir, fn))
return stats.mtimeMs > timestamp
})
.flatMap((fn) => extractDeploymentReceipts(path.join(broadcastDir, fn)))
}
const extractDeploymentReceipts = (broadcastFN) => {
// Silent exit if the broadcast file does not exist
if (!fs.existsSync(broadcastFN)) {
return []
}
const broadcast = JSON.parse(fs.readFileSync(broadcastFN))
const transactions = broadcast.transactions || []
const receipts = broadcast.receipts || []
// Direct deployment transactions have transactionType = CREATE
// Save hash and contractAddress
const directDeployments = transactions
.filter((tx) => tx.transactionType === 'CREATE')
.map((tx) => ({
hash: tx.hash,
address: tx.contractAddress,
}))
// Indirect deployments are in additionalContracts field
// Save tx.hash and tx.additionalContracts[i].address
const indirectDeployments = transactions
.filter((tx) => tx.additionalContracts)
.map((tx) =>
tx.additionalContracts.map((c) => ({
hash: tx.hash,
address: c.address,
}))
)
.flat()
// Merge two arrays
const allDeployments = directDeployments.concat(indirectDeployments)
// Find the block number for each deployment in the receipts
// Discard the deployment if the receipt is not found
return allDeployments
.map((d) => {
const receipt = receipts.find((r) => r.transactionHash === d.hash)
if (!receipt) {
return null
}
return {
address: d.address,
blockNumber: receipt.blockNumber.startsWith('0x')
? parseInt(receipt.blockNumber, 16)
: receipt.blockNumber,
hash: d.hash,
}
})
.filter((d) => d)
}
module.exports = {
createDeploymentDirs,
saveNewDeployment,
saveDeploymentArtifact,
getContractName,
getBuildArtifact,
getConfirmedFreshDeployment,
getSavedDeployment,
getNewDeployments,
getNewDeploymentReceipts,
getAllDeploymentReceipts,
}