/**
* @file deploy-marketplace.ts
* @author nirholas
* @copyright (c) 2026 nichxbt
* @repository universal-crypto-mcp
* @version 0.4.14.3
*
* Deployment script for marketplace contracts to Arbitrum Sepolia
*
* Usage:
* npx ts-node scripts/deploy/deploy-marketplace.ts
*
* Environment variables:
* DEPLOYER_PRIVATE_KEY - Private key for deployment
* ARBITRUM_SEPOLIA_RPC - RPC URL (optional, has default)
* ARBISCAN_API_KEY - For contract verification
*/
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
type Hash,
type Address,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
// ═══════════════════════════════════════════════════════════════
// Configuration
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
// Network
chainId: 421614,
rpcUrl: process.env.ARBITRUM_SEPOLIA_RPC || 'https://sepolia-rollup.arbitrum.io/rpc',
// Contract parameters
minimumStake: parseUnits('10', 18), // 10 USDs minimum stake
platformFeeBps: 250n, // 2.5% platform fee
minimumPayout: parseUnits('1', 18), // 1 USDs minimum payout
votingDuration: 7n * 24n * 60n * 60n, // 7 days
quorumVotes: parseUnits('100', 18), // 100 USDs worth of votes
// Note: On mainnet, use the official USDs token address
// On testnet, this flag determines if we deploy a test ERC20 token
deployMockUsds: true,
};
// ═══════════════════════════════════════════════════════════════
// Contract Bytecode (would be compiled from Foundry/Hardhat)
// ═══════════════════════════════════════════════════════════════
// NOTE: In production, these would be imported from compiled artifacts
interface DeploymentResult {
toolRegistry: Address;
revenueRouter: Address;
toolStaking: Address;
usdsToken: Address;
deployer: Address;
blockNumber: bigint;
timestamp: Date;
}
// ═══════════════════════════════════════════════════════════════
// Deployment Functions
// ═══════════════════════════════════════════════════════════════
async function main() {
console.log('═'.repeat(60));
console.log(' Universal Crypto MCP - Marketplace Deployment');
console.log(' @author nirholas | @nichxbt');
console.log('═'.repeat(60));
console.log('');
// Validate environment
const privateKey = process.env.DEPLOYER_PRIVATE_KEY;
if (!privateKey) {
console.error('❌ DEPLOYER_PRIVATE_KEY environment variable required');
process.exit(1);
}
// Setup clients
const account = privateKeyToAccount(privateKey as `0x${string}`);
const publicClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(CONFIG.rpcUrl),
});
const walletClient = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(CONFIG.rpcUrl),
});
console.log(`📍 Network: Arbitrum Sepolia (${CONFIG.chainId})`);
console.log(`👤 Deployer: ${account.address}`);
console.log('');
// Check balance
const balance = await publicClient.getBalance({ address: account.address });
console.log(`💰 Balance: ${Number(balance) / 1e18} ETH`);
if (balance < parseUnits('0.01', 18)) {
console.error('❌ Insufficient ETH for deployment. Need at least 0.01 ETH');
process.exit(1);
}
console.log('');
console.log('🚀 Starting deployment...');
console.log('');
// Deployment tracking
const deployments: Partial<DeploymentResult> = {
deployer: account.address,
};
try {
// Step 1: Deploy Test USDs Token (testnet only)
if (CONFIG.deployMockUsds) {
console.log('1️⃣ Deploying Test USDs Token...');
// Deploy mock ERC20 token for testing
const usdsAddress = await deployMockERC20(
walletClient,
publicClient,
'Stable Dollar',
'USDs',
6 // 6 decimals for stablecoin
);
deployments.usdsToken = usdsAddress;
console.log(` ✅ USDs deployed at: ${usdsAddress}`);
console.log('');
} else {
// Use existing USDs token address on mainnet
deployments.usdsToken = '0x820C137fa70C8691f0e44Dc420a5e53c168921Dc' as Address; // Example mainnet address
console.log(` ℹ️ Using existing USDs at: ${deployments.usdsToken}`);
console.log('');
}
// Step 2: Deploy ToolStaking
console.log('2️⃣ Deploying ToolStaking...');
const stakingAddress = await deployToolStaking(
walletClient,
publicClient,
deployments.usdsToken!,
CONFIG.minimumStake,
CONFIG.minimumPayout
);
deployments.toolStaking = stakingAddress;
console.log(` ✅ ToolStaking deployed at: ${stakingAddress}`);
console.log('');
// Step 3: Deploy ToolRegistry
console.log('3️⃣ Deploying ToolRegistry...');
const registryAddress = await deployToolRegistry(
walletClient,
publicClient,
stakingAddress,
CONFIG.minimumStake
);
deployments.toolRegistry = registryAddress;
console.log(` ✅ ToolRegistry deployed at: ${registryAddress}`);
console.log('');
// Step 4: Deploy RevenueRouter
console.log('4️⃣ Deploying RevenueRouter...');
const revenueRouterAddress = await deployRevenueRouter(
walletClient,
publicClient,
registryAddress,
deployments.usdsToken!,
CONFIG.platformFeeBps
);
deployments.revenueRouter = revenueRouterAddress;
console.log(` ✅ RevenueRouter deployed at: ${revenueRouterAddress}`);
console.log('');
// Step 5: Grant roles
console.log('5️⃣ Configuring roles...');
await grantRevenueRouterRole(walletClient, publicClient, registryAddress, revenueRouterAddress);
console.log(' • Granted REVENUE_ROUTER_ROLE to RevenueRouter');
await setStakingContract(walletClient, publicClient, registryAddress, stakingAddress);
console.log(' • Set staking contract in Registry');
console.log(' ✅ Roles configured');
// Get deployment block
const block = await publicClient.getBlock();
deployments.blockNumber = block.number;
deployments.timestamp = new Date(Number(block.timestamp) * 1000);
// Output results
console.log('');
console.log('═'.repeat(60));
console.log(' ✅ Deployment Complete!');
console.log('═'.repeat(60));
console.log('');
console.log('📋 Contract Addresses:');
console.log(` USDs Token: ${deployments.usdsToken}`);
console.log(` ToolStaking: ${deployments.toolStaking}`);
console.log(` ToolRegistry: ${deployments.toolRegistry}`);
console.log(` RevenueRouter: ${deployments.revenueRouter}`);
console.log('');
console.log(`📦 Block Number: ${deployments.blockNumber}`);
console.log(`⏰ Timestamp: ${deployments.timestamp?.toISOString()}`);
console.log('');
// Write deployment file
const deploymentJson = {
network: 'arbitrum-sepolia',
chainId: CONFIG.chainId,
addresses: {
usdsToken: deployments.usdsToken,
toolStaking: deployments.toolStaking,
toolRegistry: deployments.toolRegistry,
revenueRouter: deployments.revenueRouter,
},
deployer: deployments.deployer,
blockNumber: String(deployments.blockNumber),
timestamp: deployments.timestamp?.toISOString(),
config: {
minimumStake: String(CONFIG.minimumStake),
platformFeeBps: String(CONFIG.platformFeeBps),
minimumPayout: String(CONFIG.minimumPayout),
},
};
console.log('📄 Deployment JSON:');
console.log(JSON.stringify(deploymentJson, null, 2));
console.log('');
console.log('💡 Next steps:');
console.log(' 1. Verify contracts on Arbiscan');
console.log(' 2. Update src/modules/tool-marketplace/contracts/addresses.ts');
console.log(' 3. Test with npx ts-node scripts/deploy/test-deployment.ts');
console.log('');
} catch (error) {
console.error('❌ Deployment failed:', error);
process.exit(1);
}
}
// ═══════════════════════════════════════════════════════════════
// Verification Helper
// ═══════════════════════════════════════════════════════════════
async function verifyContract(
address: Address,
constructorArgs: any[],
contractPath: string
) {
const apiKey = process.env.ARBISCAN_API_KEY;
if (!apiKey) {
console.log('⚠️ ARBISCAN_API_KEY not set, skipping verification');
return;
}
console.log(`🔍 Verifying ${contractPath} at ${address}...`);
// In production: use @nomicfoundation/hardhat-verify or similar
// For now, output the verification command
console.log(` forge verify-contract ${address} ${contractPath} \\`);
console.log(` --chain-id ${CONFIG.chainId} \\`);
console.log(` --etherscan-api-key ${apiKey}`);
}
// Run deployment
main().catch(console.error);
// EOF - nicholas | ucm:n1ch-deploy