Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ simply copy paste the path):
$ bun run test:coverage:report
```

## ABI Management

This repository includes automated ABI management that syncs contract ABIs to a checkout of[`doc-abis`](https://github.com/berachain/doc-abis) repository, expected to be found at `../doc-abis`, next to the `contracts` checkout.

### Commands

```sh
# Sync all ABI files (update existing + create missing, excludes test/mock contracts)
$ npm run abis:sync
```

### Adding New Contracts

ABIs are automatically organized into directories:

- **`core/`** - Protocol contracts (BeraChef, BGT, RewardVault, etc.)
- **`gov/`** - Governance contracts (BerachainGovernance, Timelock)
- **`bex/`** - BEX/Balancer contracts (interfaces starting with `I`)
- **`misc/`** - Other contracts (ERC20, utilities)

To add a new contract category, update the `directoryMapping` in [`manage-abis.js`](./manage-abis.js).

## Related Efforts

- [abigger87/femplate](https://github.com/abigger87/femplate)
Expand Down
341 changes: 341 additions & 0 deletions manage-abis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

/**
* Script to build contracts and manage ABI files
*
* Expects directory structure:
* contracts/ (this directory)
* ../doc-abis/ (the doc-abis repository)
*
* Operation:
* - Syncs only explicitly categorized contract ABIs (updates existing, creates missing)
* - Excludes blacklisted files (test, mock, deployment contracts)
* - Only processes contracts listed in directoryMapping configuration
*
* Usage:
* npm run abis:sync
*/

// Configuration
const CONFIG = {
contractsDir: __dirname,
abisDir: path.resolve(__dirname, '../doc-abis'),
outDir: path.join(__dirname, 'out'),

// Directory mapping for organizing ABIs
directoryMapping: {
// BEX-related contracts
'bex': [
'IAuthentication', 'IAuthorizer', 'IAuthorizerAdaptor', 'IAuthorizerAdaptorEntrypoint',
'IBalancerHelpers', 'IBalancerMinter', 'IBalancerQueries', 'IBalancerRelayer', 'IBalancerToken',
'IBasePool', 'IBasePoolController', 'IBasePoolFactory', 'IComposableStablePoolFactoryCreateV6',
'IComposableStablePoolRates', 'IExternalWeightedMath', 'IFeeDistributor', 'IFlashLoanRecipient',
'IGeneralPool', 'ILinearPool', 'IManagedPool', 'IMinimalSwapInfoPool', 'IPoolCreationHelper',
'IProtocolFeePercentagesProvider', 'IProtocolFeeSplitter', 'IProtocolFeesCollector',
'IProtocolFeesWithdrawer', 'IRateProvider', 'IRateProviderPool', 'ITimelockAuthorizer',
'IVault', 'IWeightedPoolFactory'
],

// Core protocol contracts
'core': [
'BeaconDeposit', 'BeraChef', 'BGT', 'BGTIncentiveDistributor', 'BGTStaker',
'BlockRewardController', 'Distributor', 'FeeCollector', 'HONEY', 'HoneyFactory',
'HoneyFactoryReader', 'Multicall3', 'RewardVault', 'RewardVaultFactory', 'StakingRewards', 'WBERA',
'WBERAStakerVault', 'BGTIncentiveDistributor', 'BGTIncentiveFeeCollector'
],

// Governance contracts
'gov': [
'BerachainGovernance', 'Timelock'
],

// Miscellaneous contracts
'misc': [
'ERC20'
]
},

// Blacklist for copy mode - contracts to never copy
blacklist: [
'Test', 'Mock', 'Helper', 'Script', 'Deploy', '.t.sol', '.s.sol'
]
};

/**
* Utility functions
*/
function log(message, level = 'info') {
const timestamp = new Date().toISOString();
const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : level === 'success' ? '✅' : 'ℹ️';
console.log(`${prefix} [${timestamp}] ${message}`);
}

function ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
log(`Created directory: ${dirPath}`);
}
}

function isBlacklisted(contractName) {
return CONFIG.blacklist.some(pattern => contractName.includes(pattern));
}

function getTargetDirectory(contractName) {
for (const [dir, contracts] of Object.entries(CONFIG.directoryMapping)) {
if (contracts.includes(contractName)) {
return { directory: dir, categorized: true };
}
}

return null; // Don't process uncategorized contracts
}

/**
* Build contracts using Foundry
*/
function buildContracts() {
log('Building contracts with Foundry...');
try {
// Use the build-extra-output script which generates ABIs
execSync('npm run build-extra-output', {
cwd: CONFIG.contractsDir,
stdio: 'inherit'
});
log('Contracts built successfully', 'success');
} catch (error) {
log(`Failed to build contracts: ${error.message}`, 'error');
process.exit(1);
}
}

/**
* Extract ABI from build artifact
*/
function extractABI(buildArtifactPath) {
try {
const artifact = JSON.parse(fs.readFileSync(buildArtifactPath, 'utf8'));
return artifact.abi || null;
} catch (error) {
log(`Failed to extract ABI from ${buildArtifactPath}: ${error.message}`, 'error');
return null;
}
}

/**
* Get all contract build artifacts
*/
function getContractArtifacts() {
const artifacts = [];

if (!fs.existsSync(CONFIG.outDir)) {
log('Build output directory not found. Make sure contracts are built first.', 'error');
return artifacts;
}

function scanDirectory(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (entry.name.endsWith('.json') && !entry.name.includes('.sol')) {
// This is a build artifact JSON file
const contractName = path.basename(entry.name, '.json');

// Skip test and deployment scripts
if (isBlacklisted(contractName)) {
continue;
}

const targetInfo = getTargetDirectory(contractName);

// Only include contracts that are explicitly categorized
if (targetInfo) {
artifacts.push({
name: contractName,
path: fullPath,
targetDir: targetInfo.directory,
categorized: targetInfo.categorized
});
}
}
}
}

scanDirectory(CONFIG.outDir);
return artifacts;
}

/**
* Check if ABI file exists in target location
*/
function abiFileExists(contractName, targetDir) {
const fileName = contractName.startsWith('I') ? `${contractName}.abi.json` : `${contractName}.json`;
const targetPath = path.join(CONFIG.abisDir, targetDir, fileName);
return fs.existsSync(targetPath);
}

/**
* Check if jq is available for JSON formatting
*/
function checkJqAvailable() {
try {
execSync('which jq', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}

/**
* Format JSON using jq if available, otherwise use built-in formatting
*/
function formatJSON(obj) {
const jsonString = JSON.stringify(obj, null, 2);

if (checkJqAvailable()) {
try {
return execSync('jq .', { input: jsonString, encoding: 'utf8' });
} catch (error) {
log(`Warning: jq formatting failed, using built-in formatting`, 'warn');
return jsonString;
}
}

return jsonString;
}

/**
* Check if ABI content has changed
*/
function hasABIChanged(contractName, abi, targetDir) {
const fileName = contractName.startsWith('I') ? `${contractName}.abi.json` : `${contractName}.json`;
const targetPath = path.join(CONFIG.abisDir, targetDir, fileName);

if (!fs.existsSync(targetPath)) {
return true; // File doesn't exist, so it's a change
}

try {
const existingContent = fs.readFileSync(targetPath, 'utf8');
const newContent = formatJSON(abi);
return existingContent.trim() !== newContent.trim();
} catch (error) {
return true; // If we can't read the existing file, assume it needs updating
}
}

/**
* Write ABI to target location
*/
function writeABI(contractName, abi, targetDir, forceLog = false) {
const fileName = contractName.startsWith('I') ? `${contractName}.abi.json` : `${contractName}.json`;
const targetPath = path.join(CONFIG.abisDir, targetDir, fileName);
const targetDirPath = path.join(CONFIG.abisDir, targetDir);

ensureDirectoryExists(targetDirPath);

const formattedContent = formatJSON(abi);
const hasChanged = hasABIChanged(contractName, abi, targetDir);

fs.writeFileSync(targetPath, formattedContent);

if (hasChanged || forceLog) {
log(`✓ ${targetDir}/${fileName}`, 'success');
return true; // File was updated
}

return false; // File was not changed
}

/**
* Sync mode - update existing and copy missing ABI files (only for explicitly categorized contracts)
*/
function syncMode() {
log('Syncing ABI files for explicitly categorized contracts...');

const artifacts = getContractArtifacts();
let processed = 0;
let updated = 0;
let created = 0;

for (const artifact of artifacts) {
const abi = extractABI(artifact.path);
if (!abi) continue;

processed++;

const fileExists = abiFileExists(artifact.name, artifact.targetDir);
const wasUpdated = writeABI(artifact.name, abi, artifact.targetDir, !fileExists);

if (wasUpdated) {
if (fileExists) {
updated++;
} else {
created++;
}
}
}

log(`Processed ${processed} explicitly categorized files: ${updated} updated, ${created} created`, 'success');
}

/**
* Main execution
*/
function main() {
const mode = process.argv[2];

if (mode && mode !== 'sync') {
console.log(`
Usage: node manage-abis.js [sync]

Operation:
sync - Update existing and create missing ABI files (excludes blacklisted)
- This is the default operation when no mode is specified

Examples:
node manage-abis.js
node manage-abis.js sync
npm run abis:sync
`);
if (!['sync', 'help', '--help', '-h'].includes(mode)) {
process.exit(1);
}
if (mode !== 'sync') {
process.exit(0);
}
}

// Ensure we're in the contracts directory
if (!fs.existsSync(path.join(CONFIG.contractsDir, 'foundry.toml'))) {
log('This script must be run from the contracts directory', 'error');
process.exit(1);
}

// Build contracts first
buildContracts();

// Execute sync operation
syncMode();

log('ABI management completed!', 'success');
}

// Run the script
if (require.main === module) {
main();
}

module.exports = {
buildContracts,
syncMode,
CONFIG
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"solstat": "RUST_BACKTRACE=full solstat -p ./src",
"test": "forge test",
"test:coverage": "forge coverage",
"test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage"
"test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage",
"abis:sync": "node manage-abis.js sync"
}
}
Loading