A comprehensive TypeScript framework for deploying CosmWasm smart contracts with support for multiple deployment strategies, dependency management, and storage tracking.
it works, but it can use a nice overhaul here and there, it does a lot of what is needed and a could be easily extended to automate testing after deploy (kind of on my TODO)
This framework provides a unified, efficient system for deploying CosmWasm contracts with features like:
- Multiple Deployment Strategies: Simple, waterfall, strategy-based, and parallel deployments
- Dependency Management: Automatic resolution and injection of contract dependencies
- Storage Tracking: Individual contract storage with deployment phase tracking
- Wallet Management: Unified wallet and client creation with validation
- WASM Management: Centralized WASM file loading with validation and caching
- Type Safety: Comprehensive TypeScript types for all operations
- Backward Compatibility: Existing deployment scripts continue to work unchanged
The framework has been refactored around a unified core system that eliminates code duplication:
src/core/
βββ types.ts # Unified type definitions
βββ wasm-loader.ts # WASM file loading and validation
βββ wallet-manager.ts # Wallet and client management
βββ deployment-engine.ts # Core deployment operations
βββ storage-manager.ts # Unified storage interface
βββ index.ts # Central export point
All existing deployment systems now use the core modules internally:
deployment-manager.ts- Basic deployment operationswaterfall-deploy.ts- Dependency-aware deploymentsdeploy-strategy.ts- Strategy-based deploymentsdeploy.ts- Simple deployment scripts
Create your environment file (.env.dev, .env.prod, etc.):
# Network Configuration
CHAIN_ID=thorchain-mainnet-v1
RPC_ENDPOINT=https://rpc.thorchain.info
API_ENDPOINT=https://thornode.ninerealms.com
CHAIN_EXPLORER=https://viewblock.io/thorchain
# Wallet Configuration
MNEMONIC="your twelve word mnemonic phrase here"
DEFAULT_ADDRESS=thor1...
# Paths
ARTIFACTS_PATH=./artifacts
CONFIGS_PATH=./configs
DEPLOYMENTS_PATH=./deployed
CONTRACTS_PATH=./contracts
# Gas Settings
GAS_PRICE=0.025uthor
GAS_LIMIT=2000000Contract configurations support all deployment strategies with a unified format. Here's a comprehensive example showing all available options:
// configs/example_contract.json
{
// Basic Required Fields
"name": "MyContract", // Contract identifier name
"wasmFile": "my_contract.wasm", // WASM filename (required)
"initMsg": { // Contract instantiation message (required)
"admin": "thor1admin123...", // Contract admin address
"owner": "thor1owner456...", // Contract owner
"config": {
"fee": "0.003", // Fee configuration
"max_slippage": "0.05", // Maximum slippage allowed
"registry_address": "{rujira_registry.wasm}", // Template placeholder for dependency
"vault_address": "{rujira_vault.wasm}", // Another dependency placeholder
"admin": "{DEPLOYER_ADDRESS}" // Deployer's wallet address
}
},
// Optional Basic Fields
"label": "My Contract v2.0", // Human-readable contract label
"admin": "thor1admin123...", // Admin override (null = deployer becomes admin)
"wasmPath": "../custom/path/my_contract.wasm", // Custom path override (default: artifacts/)
"gasLimit": 3000000, // Custom gas limit override
"uploadOnly": false, // Set to true for library/template contracts
// Dependency Configuration (for complex deployments)
"dependencies": [
// Simple string format (basic deployments)
"registry_contract.wasm",
"utility_contract.wasm",
// Advanced object format (waterfall deployments)
{
"contractName": "rujira_registry", // Dependency contract name
"paramPath": "config.registry_address", // JSON path for address injection
"isOptional": false // Whether dependency is optional
},
{
"contractName": "rujira_vault",
"paramPath": "config.vault_address",
"isOptional": true
}
],
// Waterfall-Specific Fields
"priority": 5 // Deployment order priority (lower = earlier)
}This single configuration format works for all deployment strategies:
- Simple deployments: Use basic fields (
name,wasmFile,initMsg,label) - Library contracts: Add
"uploadOnly": trueto skip instantiation - Custom paths: Use
wasmPathfor non-standard WASM locations - Dependencies: Use string array for simple deps, object array for advanced resolution
- Waterfall deployments: Add
priorityand advanced dependency objects
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | β | Contract identifier name |
wasmFile |
string | β | WASM filename (e.g., "contract.wasm") |
wasmPath |
string | β | Custom path to WASM file (overrides default artifacts path) |
label |
string | β | Human-readable contract label |
admin |
string | null | β | Admin address (null = deployer address) |
initMsg |
object | β | Contract instantiation message |
dependencies |
string[] | ContractDependency[] | β | Contract dependencies |
gasLimit |
number | β | Custom gas limit override |
uploadOnly |
boolean | β | Upload contract code only, skip instantiation |
priority |
number | β | Deployment priority (waterfall only) |
Dependencies: Use string arrays for simple dependencies or object arrays for advanced resolution with specific JSON paths and optional flags.
Template Placeholders: In waterfall deployments, use placeholders for automatic resolution:
{contract_name.wasm}- Resolves to deployed contract address (for dependencies){CODE_ID.contract_name.wasm}- Resolves to contract code ID{CHECKSUM.contract_name.wasm}- Resolves to WASM file checksum{DEPLOYER_ADDRESS}- Resolves to the deployer's wallet address
Example usage:
{
"initMsg": {
"admin": "{DEPLOYER_ADDRESS}", // Admin set to deployer
"registry_address": "{rujira_registry.wasm}", // Dependency address
"code_id": "{CODE_ID.rujira_vault.wasm}", // Dependency code ID
"checksum": "{CHECKSUM.rujira_vault.wasm}" // Dependency checksum
}
}Best Practices: Use descriptive names, set appropriate admins (null = deployer), configure custom gas limits for complex contracts, and organize paths consistently.
import { createWallet, DeploymentEngine, getStorageManager } from "./src/core/index.js";
import { DEPLOYMENT_CONFIG, mnemonic } from "./src/config.js";
async function deployContract() {
// Create wallet and client
const walletInfo = await createWallet(
DEPLOYMENT_CONFIG,
mnemonic
);
// Create deployment engine
const engine = new DeploymentEngine(walletInfo);
// Storage configuration
const storageConfig = {
network: "thorchain",
baseDir: "./deployed"
};
// Deploy contract
const result = await engine.deployContract({
name: "MyContract",
wasmFile: "my_contract.wasm",
initMsg: { admin: walletInfo.address },
label: "My Contract v1.0"
}, {
storageConfig,
skipExisting: true
});
console.log("Deployment result:", result);
}Deploy individual contracts:
npm run deploy:simple -- my_contract.jsonDeploy contracts with dependency resolution:
npm run deploy:waterfall -- --config=waterfall-config.jsonExample waterfall config:
{
"contracts": [
{
"contractName": "Registry",
"wasmFileName": "registry.wasm",
"priority": 1,
"dependencies": [],
"initConfigPath": "registry_init.json"
},
{
"contractName": "Vault",
"wasmFileName": "vault.wasm",
"priority": 2,
"dependencies": [
{
"contractName": "Registry",
"paramPath": "registry_address",
"isOptional": false
}
],
"initConfigPath": "vault_init.json"
}
],
"artifactsDir": "./artifacts",
"configsDir": "./configs",
"storageConfig": {
"network": "thorchain",
"baseDir": "./deployed"
}
}Deploy using predefined strategies:
npm run deploy:strategy -- --strategy=minimal
npm run deploy:strategy -- --strategy=full-ecosystemDeploy multiple contracts in sequence:
const configs = [
{ name: "Contract1", wasmFile: "contract1.wasm", initMsg: {...} },
{ name: "Contract2", wasmFile: "contract2.wasm", initMsg: {...} },
];
const result = await engine.deployContracts(configs, {
storageConfig,
continueOnError: true
});Main deployment orchestration class:
class CoreDeploymentEngine {
constructor(walletInfo: WalletInfo, gasPrice?: string, gasLimit?: number)
// Deploy single contract (upload + instantiate)
async deployContract(config: ContractConfig, options: DeploymentOptions): Promise<ContractDeploymentResult>
// Deploy multiple contracts
async deployContracts(configs: ContractConfig[], options: DeploymentOptions): Promise<BatchDeploymentResult>
// Upload contract code only
async uploadContract(config: UploadConfig): Promise<UploadOperationResult>
// Instantiate uploaded contract
async instantiateContract(config: InstantiateConfig): Promise<InstantiateOperationResult>
// Validate deployment environment
async validateDeployment(configs: ContractConfig[], options: DeploymentOptions): Promise<ValidationResult>
}Wallet and client management:
// Create wallet and client
async function createWalletAndClient(config: NetworkConfig, mnemonic: string, options?: WalletCreationOptions): Promise<WalletInfo>
// Validate wallet connection
async function validateWalletConnection(walletInfo: WalletInfo, config: NetworkConfig): Promise<WalletValidationResult>
// Get account information
async function getAccountInfo(walletInfo: WalletInfo): Promise<AccountInfo>Contract storage management:
class UnifiedStorageManager {
// Load contract data
async loadContract(contractName: string): Promise<ContractStorage | null>
// Save contract data
async saveContract(contract: ContractStorage): Promise<void>
// List all contracts
async listAllContracts(): Promise<ContractSummary[]>
// Get contracts by deployment phase
async getContractsByPhase(phase: DeploymentPhase): Promise<ContractStorage[]>
// Clean storage
async cleanContracts(options: CleanOptions): Promise<void>
}WASM file management:
// Load WASM file
async function loadWasmFile(wasmFile: string, options?: WasmLoadOptions): Promise<WasmLoadResult>
// Load multiple WASM files
async function loadMultipleWasmFiles(wasmFiles: string[], options?: WasmLoadOptions): Promise<Map<string, WasmLoadResult>>
// Get WASM file info without loading
async function getWasmFileInfo(wasmFile: string, customPath?: string): Promise<WasmFileInfo>interface DeploymentOptions {
storageConfig: StorageConfig; // Storage configuration
skipExisting?: boolean; // Skip already deployed contracts
forceRedeploy?: boolean; // Force redeploy existing contracts
uploadOnly?: boolean; // Only upload, don't instantiate
instantiateOnly?: boolean; // Only instantiate uploaded contracts
dryRun?: boolean; // Simulate deployment without executing
continueOnError?: boolean; // Continue on individual contract failures
gasPrice?: string; // Custom gas price
gasLimit?: number; // Custom gas limit
}interface StorageConfig {
network: string; // Network name (e.g., "thorchain")
baseDir?: string; // Storage directory (default: "./deployed")
}interface ContractConfig {
name: string; // Contract name
wasmFile: string; // WASM file name
wasmPath?: string; // Custom WASM file path
label?: string; // Contract label
admin?: string; // Admin address
initMsg: any; // Instantiation message
gasLimit?: number; // Custom gas limit
}Contracts progress through deployment phases:
- UPLOADED: Contract code uploaded to blockchain, has
codeId - INSTANTIATED: Contract instantiated, has
address
Each phase is tracked in individual storage files:
// deployed/my_contract.json
{
"contractName": "MyContract",
"wasmFile": "my_contract.wasm",
"network": "thorchain",
"phase": "INSTANTIATED",
"codeId": 123,
"address": "thor1abc...",
"uploadTxHash": "0x123...",
"instantiateTxHash": "0x456...",
"uploadedAt": "2024-01-01T00:00:00.000Z",
"instantiatedAt": "2024-01-01T00:01:00.000Z",
"metadata": {
"label": "My Contract v1.0",
"admin": "thor1def...",
"initMsg": { "admin": "thor1def..." }
}
}# Simple deployment
npm run deploy:simple -- <config-file>
# Waterfall deployment with dependencies
npm run deploy:waterfall -- --config=<waterfall-config>
# Strategy-based deployment
npm run deploy:strategy -- --strategy=<strategy-name>
# Clean deployments
npm run clean -- --all
npm run clean -- --phase=uploaded
npm run clean -- --contract=<contract-name># Build TypeScript
npm run build
# Generate schemas
npm run generate-schema
# Fund wallet for testing
npm run fund-wallet
# System tests
npm run test-system# Testnet deployments
npm run deploy:testnet:simple -- <config>
npm run deploy:testnet:waterfall -- --config=<config>
# Mainnet deployments
npm run deploy:mainnet:simple -- <config>
npm run deploy:mainnet:waterfall -- --config=<config>import { CoreDeploymentEngine, createWallet } from "./src/core/index.js";
class CustomDeploymentEngine extends CoreDeploymentEngine {
async deployWithRetry(config: ContractConfig, options: DeploymentOptions, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await this.deployContract(config, options);
if (!result.error) return result;
console.log(`Attempt ${attempt} failed: ${result.error}`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
}
}import { UnifiedStorageManager } from "./src/core/index.js";
class DatabaseStorageManager extends UnifiedStorageManager {
async saveContract(contract: ContractStorage): Promise<void> {
// Custom database storage logic
await database.contracts.save(contract);
// Also save to file system for backup
await super.saveContract(contract);
}
}// Custom dependency resolver
async function customDependencyResolver(initMsg: any, dependencies: string[]): Promise<any> {
const resolved = { ...initMsg };
for (const dep of dependencies) {
const address = await getContractAddress(storageConfig, dep);
if (address) {
resolved[`${dep}_address`] = address;
}
}
return resolved;
}-
WASM File Not Found
# Check artifacts path ls -la ./artifacts/ # Verify environment variable echo $ARTIFACTS_PATH
-
Wallet Connection Issues
// Validate wallet before deployment const validation = await validateWalletConnection(walletInfo, networkConfig); if (!validation.valid) { throw new Error(validation.error); }
-
Storage Permission Issues
# Check deployed directory permissions mkdir -p ./deployed chmod 755 ./deployed -
Gas Estimation Failures
// Use manual gas limits const result = await engine.deployContract(config, { ...options, gasLimit: 3000000 });
Enable detailed logging:
const result = await engine.deployContract(config, {
...options,
dryRun: true // Simulate without executing
});- Mnemonic Storage: Never commit mnemonics to version control
- Environment Files: Use environment-specific files (
.env.dev,.env.prod) - Admin Keys: Rotate admin keys regularly
- Gas Limits: Set reasonable gas limits to prevent excessive fees
- Network Validation: Always validate network configuration
- WASM Caching: WASM files are cached automatically during deployment
- Concurrent Deployments: Use parallel strategies for independent contracts
- Storage Optimization: Individual storage files reduce I/O overhead
- Connection Pooling: Wallet connections are reused across deployments
- Fork the repository
- Create a feature branch:
git checkout -b feature-name - Make your changes and add tests
- Ensure all tests pass:
npm test - Submit a pull request
- Use TypeScript strict mode
- Follow existing naming conventions
- Add JSDoc comments for public APIs
- Include unit tests for new features
This project is licensed under the MIT License - see the LICENSE file for details.
- CosmJS team for excellent CosmWasm tooling
- THORChain team for network infrastructure
- All contributors who helped improve this framework
If you're upgrading from an older version:
The core refactoring maintains backward compatibility, but you can now use the improved core modules:
// Old way (still works)
import { DeploymentManager } from "./src/deployment-manager.js";
// New way (recommended)
import { DeploymentEngine, createWallet, getStorageManager } from "./src/core/index.js";Legacy storage files are automatically compatible. No migration required.
All existing configuration files continue to work unchanged.