🧪Simulation and Gas optimization
This guide demonstrates advanced techniques for utilizing gas simulation to achieve better fee estimations and guarantees, as well as implementing efficient transaction batching patterns using the cheqd SDK. These techniques are based on the SDK's internal implementation and provide production-ready optimization strategies.
Table of Contents
Gas Simulation Overview
Gas simulation allows you to accurately estimate transaction costs before execution, providing several key benefits:
Core Benefits
Cost Prediction
Accurate fee estimation before transaction execution
User experience, cost planning
Failure Prevention
Early detection of insufficient gas scenarios
Transaction reliability
Optimal Gas Setting
Avoid over/under-provisioning gas
Cost optimization
Batch Optimization
Efficient grouping of multiple transactions
Throughput maximization
Simulation vs Real Execution
Network State
Current state snapshot
State at execution time
Gas Consumption
Estimated usage
Actual usage
Side Effects
None (read-only)
Permanent state changes
Cost
Free
Consumes actual fees
Advanced Gas Estimation
Basic Gas Simulation
The SDK provides a simulate()
method for accurate gas estimation:
import { CheqdSigningStargateClient } from '@cheqd/sdk';
// Initialize signer with endpoint for simulation
const signer = await CheqdSigningStargateClient.connectWithSigner(
rpcEndpoint,
wallet,
{
registry: createDefaultCheqdRegistry(),
endpoint: rpcEndpoint // Required for simulation
}
);
// Simulate single transaction
async function simulateTransaction(
signerAddress: string,
messages: EncodeObject[],
memo?: string
): Promise<number> {
try {
const gasUsed = await signer.simulate(signerAddress, messages, memo);
console.log(`Estimated gas usage: ${gasUsed}`);
return gasUsed;
} catch (error) {
console.error('Simulation failed:', error);
throw error;
}
}
Enhanced Gas Estimation with Safety Margins
interface GasEstimationOptions {
safetyMargin: number; // Multiplier for gas estimation (e.g., 1.3 = 30% buffer)
maxGasLimit: number; // Maximum allowed gas per transaction
fallbackGas: number; // Default gas if simulation fails
}
class AdvancedGasEstimator {
private signer: CheqdSigningStargateClient;
private options: GasEstimationOptions;
constructor(signer: CheqdSigningStargateClient, options?: Partial<GasEstimationOptions>) {
this.signer = signer;
this.options = {
safetyMargin: 1.3,
maxGasLimit: 30_000_000,
fallbackGas: 200_000,
...options
};
}
async estimateGasWithSafety(
signerAddress: string,
messages: EncodeObject[],
memo?: string
): Promise<{ estimatedGas: number; recommendedGas: number; safeGas: number }> {
try {
// Perform simulation
const estimatedGas = await this.signer.simulate(signerAddress, messages, memo);
// Apply safety margin
const recommendedGas = Math.ceil(estimatedGas * this.options.safetyMargin);
// Ensure within limits
const safeGas = Math.min(recommendedGas, this.options.maxGasLimit);
return {
estimatedGas,
recommendedGas,
safeGas
};
} catch (error) {
console.warn('Gas simulation failed, using fallback:', error);
return {
estimatedGas: this.options.fallbackGas,
recommendedGas: this.options.fallbackGas,
safeGas: this.options.fallbackGas
};
}
}
async calculateOptimalFee(
signerAddress: string,
messages: EncodeObject[],
gasPrice: GasPrice,
memo?: string
): Promise<DidStdFee> {
const { safeGas } = await this.estimateGasWithSafety(signerAddress, messages, memo);
return {
amount: [coin(
Math.ceil(safeGas * parseFloat(gasPrice.amount.toString())).toString(),
gasPrice.denom
)],
gas: safeGas.toString(),
payer: signerAddress
};
}
}
Bulk Gas Estimation for Multiple Transactions
interface BulkEstimationResult {
totalGas: number;
individualGas: number[];
averageGas: number;
maxGas: number;
minGas: number;
}
async function estimateBulkGas(
signer: CheqdSigningStargateClient,
signerAddress: string,
messageGroups: EncodeObject[][],
memo?: string
): Promise<BulkEstimationResult> {
// Simulate all transactions in parallel
const gasEstimates = await Promise.all(
messageGroups.map(messages =>
signer.simulate(signerAddress, messages, memo).catch(error => {
console.warn(`Simulation failed for batch:`, error);
return 200_000; // Fallback gas
})
)
);
const totalGas = gasEstimates.reduce((sum, gas) => sum + gas, 0);
return {
totalGas,
individualGas: gasEstimates,
averageGas: Math.ceil(totalGas / gasEstimates.length),
maxGas: Math.max(...gasEstimates),
minGas: Math.min(...gasEstimates)
};
}
Transaction Batching Strategies
The SDK provides an intelligent batchMessages()
method that optimally groups transactions based on gas limits:
Core Batching Implementation
interface MessageBatch {
batches: EncodeObject[][]; // Groups of messages
gas: number[]; // Gas usage per batch
}
// The SDK's batchMessages method automatically:
// 1. Simulates each individual message
// 2. Groups messages to stay within gas limits
// 3. Returns optimized batches with gas estimates
async function createOptimizedBatches(
signer: CheqdSigningStargateClient,
signerAddress: string,
messages: EncodeObject[],
maxGasLimit: number = 30_000_000,
memo?: string
): Promise<MessageBatch> {
return await signer.batchMessages(messages, signerAddress, memo, maxGasLimit);
}
Advanced Batching Strategies
1. Priority-Based Batching
interface PriorityMessage {
message: EncodeObject;
priority: 'high' | 'medium' | 'low';
estimatedGas?: number;
}
class PriorityBatcher {
private signer: CheqdSigningStargateClient;
private maxGasLimit: number;
constructor(signer: CheqdSigningStargateClient, maxGasLimit: number = 30_000_000) {
this.signer = signer;
this.maxGasLimit = maxGasLimit;
}
async createPriorityBatches(
signerAddress: string,
priorityMessages: PriorityMessage[],
memo?: string
): Promise<{
highPriorityBatches: MessageBatch;
mediumPriorityBatches: MessageBatch;
lowPriorityBatches: MessageBatch;
}> {
// Group by priority
const grouped = {
high: priorityMessages.filter(pm => pm.priority === 'high').map(pm => pm.message),
medium: priorityMessages.filter(pm => pm.priority === 'medium').map(pm => pm.message),
low: priorityMessages.filter(pm => pm.priority === 'low').map(pm => pm.message)
};
// Batch each priority group
const [highPriorityBatches, mediumPriorityBatches, lowPriorityBatches] = await Promise.all([
this.signer.batchMessages(grouped.high, signerAddress, memo, this.maxGasLimit),
this.signer.batchMessages(grouped.medium, signerAddress, memo, this.maxGasLimit),
this.signer.batchMessages(grouped.low, signerAddress, memo, this.maxGasLimit)
]);
return {
highPriorityBatches,
mediumPriorityBatches,
lowPriorityBatches
};
}
}
2. Smart Batching with Gas Optimization
interface BatchingMetrics {
totalMessages: number;
totalBatches: number;
averageMessagesPerBatch: number;
gasUtilization: number;
estimatedCostSaving: number;
}
class SmartBatcher {
private signer: CheqdSigningStargateClient;
constructor(signer: CheqdSigningStargateClient) {
this.signer = signer;
}
async createSmartBatches(
signerAddress: string,
messages: EncodeObject[],
options: {
maxGasLimit?: number;
targetUtilization?: number; // Target gas utilization percentage
memo?: string;
} = {}
): Promise<{ batches: MessageBatch; metrics: BatchingMetrics }> {
const {
maxGasLimit = 30_000_000,
targetUtilization = 0.85, // Use 85% of max gas limit
memo
} = options;
// Adjust max gas based on target utilization
const adjustedMaxGas = Math.floor(maxGasLimit * targetUtilization);
// Create batches
const batches = await this.signer.batchMessages(
messages,
signerAddress,
memo,
adjustedMaxGas
);
// Calculate metrics
const totalGas = batches.gas.reduce((sum, gas) => sum + gas, 0);
const averageGasPerBatch = totalGas / batches.batches.length;
const gasUtilization = averageGasPerBatch / adjustedMaxGas;
// Estimate cost savings (vs individual transactions)
const individualTxCost = messages.length * 50_000; // Base transaction cost
const batchedTxCost = batches.batches.length * 50_000;
const estimatedCostSaving = ((individualTxCost - batchedTxCost) / individualTxCost) * 100;
const metrics: BatchingMetrics = {
totalMessages: messages.length,
totalBatches: batches.batches.length,
averageMessagesPerBatch: messages.length / batches.batches.length,
gasUtilization,
estimatedCostSaving
};
return { batches, metrics };
}
}
3. Transaction Type-Aware Batching
interface TypedMessage {
message: EncodeObject;
type: 'did' | 'resource' | 'cosmos' | 'governance';
complexity: 'simple' | 'complex';
}
class TypeAwareBatcher {
private signer: CheqdSigningStargateClient;
constructor(signer: CheqdSigningStargateClient) {
this.signer = signer;
}
private categorizeMessage(message: EncodeObject): TypedMessage {
const { typeUrl } = message;
let type: TypedMessage['type'];
let complexity: TypedMessage['complexity'] = 'simple';
if (typeUrl.includes('did')) {
type = 'did';
complexity = 'complex'; // DID operations are typically more complex
} else if (typeUrl.includes('resource')) {
type = 'resource';
complexity = 'complex'; // Resource operations can be large
} else if (typeUrl.includes('gov')) {
type = 'governance';
complexity = 'simple';
} else {
type = 'cosmos';
complexity = 'simple';
}
return { message, type, complexity };
}
async createTypedBatches(
signerAddress: string,
messages: EncodeObject[],
memo?: string
): Promise<{
didBatches: MessageBatch;
resourceBatches: MessageBatch;
cosmosBatches: MessageBatch;
governanceBatches: MessageBatch;
}> {
// Categorize messages
const typedMessages = messages.map(msg => this.categorizeMessage(msg));
// Group by type
const grouped = {
did: typedMessages.filter(tm => tm.type === 'did').map(tm => tm.message),
resource: typedMessages.filter(tm => tm.type === 'resource').map(tm => tm.message),
cosmos: typedMessages.filter(tm => tm.type === 'cosmos').map(tm => tm.message),
governance: typedMessages.filter(tm => tm.type === 'governance').map(tm => tm.message)
};
// Different gas limits based on complexity
const gasLimits = {
did: 20_000_000, // Lower limit for complex DID operations
resource: 15_000_000, // Even lower for resource operations
cosmos: 30_000_000, // Full limit for simple operations
governance: 25_000_000 // Moderate limit for governance
};
// Batch each type with appropriate gas limits
const [didBatches, resourceBatches, cosmosBatches, governanceBatches] = await Promise.all([
grouped.did.length > 0 ? this.signer.batchMessages(grouped.did, signerAddress, memo, gasLimits.did) : { batches: [], gas: [] },
grouped.resource.length > 0 ? this.signer.batchMessages(grouped.resource, signerAddress, memo, gasLimits.resource) : { batches: [], gas: [] },
grouped.cosmos.length > 0 ? this.signer.batchMessages(grouped.cosmos, signerAddress, memo, gasLimits.cosmos) : { batches: [], gas: [] },
grouped.governance.length > 0 ? this.signer.batchMessages(grouped.governance, signerAddress, memo, gasLimits.governance) : { batches: [], gas: [] }
]);
return {
didBatches,
resourceBatches,
cosmosBatches,
governanceBatches
};
}
}
Fee Optimization Patterns
Dynamic Fee Calculation with Simulation
interface OptimizedFeeStrategy {
strategy: 'conservative' | 'balanced' | 'aggressive';
gasMultiplier: number;
maxGasPrice?: GasPrice;
fallbackFee?: DidStdFee;
}
class FeeOptimizer {
private signer: CheqdSigningStargateClient;
private feemarketModule?: FeemarketModule;
constructor(signer: CheqdSigningStargateClient, feemarketModule?: FeemarketModule) {
this.signer = signer;
this.feemarketModule = feemarketModule;
}
async optimizeFee(
signerAddress: string,
messages: EncodeObject[],
strategy: OptimizedFeeStrategy,
memo?: string
): Promise<DidStdFee> {
try {
// 1. Simulate to get gas estimate
const estimatedGas = await this.signer.simulate(signerAddress, messages, memo);
// 2. Apply strategy-based multiplier
const adjustedGas = Math.ceil(estimatedGas * strategy.gasMultiplier);
// 3. Get current gas price (dynamic or fallback)
let gasPrice: GasPrice;
if (this.feemarketModule) {
try {
gasPrice = await this.feemarketModule.generateGasPrice('ncheq');
} catch (error) {
console.warn('Failed to get dynamic gas price, using fallback');
gasPrice = strategy.maxGasPrice || GasPrice.fromString('25000ncheq');
}
} else {
gasPrice = strategy.maxGasPrice || GasPrice.fromString('25000ncheq');
}
// 4. Calculate fee
const feeAmount = Math.ceil(adjustedGas * parseFloat(gasPrice.amount.toString()));
return {
amount: [coin(feeAmount.toString(), gasPrice.denom)],
gas: adjustedGas.toString(),
payer: signerAddress
};
} catch (error) {
console.error('Fee optimization failed:', error);
return strategy.fallbackFee || {
amount: [coin('500000', 'ncheq')],
gas: '200000',
payer: signerAddress
};
}
}
// Predefined strategies
static readonly strategies = {
conservative: {
strategy: 'conservative' as const,
gasMultiplier: 1.5, // 50% buffer
},
balanced: {
strategy: 'balanced' as const,
gasMultiplier: 1.3, // 30% buffer
},
aggressive: {
strategy: 'aggressive' as const,
gasMultiplier: 1.1, // 10% buffer
}
};
}
Batch Fee Optimization
interface BatchFeeResult {
totalFee: DidStdFee;
individualFees: DidStdFee[];
savings: {
absolute: number;
percentage: number;
};
}
async function optimizeBatchFees(
signer: CheqdSigningStargateClient,
signerAddress: string,
batches: MessageBatch,
gasPrice: GasPrice,
strategy: OptimizedFeeStrategy
): Promise<BatchFeeResult> {
const individualFees: DidStdFee[] = [];
let totalGasUsed = 0;
let totalFeeAmount = 0;
// Calculate fees for each batch
for (let i = 0; i < batches.batches.length; i++) {
const batchGas = batches.gas[i];
const adjustedGas = Math.ceil(batchGas * strategy.gasMultiplier);
const feeAmount = Math.ceil(adjustedGas * parseFloat(gasPrice.amount.toString()));
const batchFee: DidStdFee = {
amount: [coin(feeAmount.toString(), gasPrice.denom)],
gas: adjustedGas.toString(),
payer: signerAddress
};
individualFees.push(batchFee);
totalGasUsed += adjustedGas;
totalFeeAmount += feeAmount;
}
// Calculate total fee
const totalFee: DidStdFee = {
amount: [coin(totalFeeAmount.toString(), gasPrice.denom)],
gas: totalGasUsed.toString(),
payer: signerAddress
};
// Calculate savings vs individual transactions
const messagesCount = batches.batches.reduce((sum, batch) => sum + batch.length, 0);
const individualTxFee = 50000; // Base transaction fee
const potentialIndividualCost = messagesCount * individualTxFee;
const actualBatchCost = batches.batches.length * individualTxFee;
const absoluteSavings = potentialIndividualCost - actualBatchCost;
const percentageSavings = (absoluteSavings / potentialIndividualCost) * 100;
return {
totalFee,
individualFees,
savings: {
absolute: absoluteSavings,
percentage: percentageSavings
}
};
}
Production Implementation
Complete Implementation Example
import {
CheqdSigningStargateClient,
createCheqdSDK,
FeemarketModule,
createDefaultCheqdRegistry
} from '@cheqd/sdk';
class ProductionTransactionManager {
private signer: CheqdSigningStargateClient;
private gasEstimator: AdvancedGasEstimator;
private smartBatcher: SmartBatcher;
private feeOptimizer: FeeOptimizer;
constructor(
signer: CheqdSigningStargateClient,
feemarketModule?: FeemarketModule
) {
this.signer = signer;
this.gasEstimator = new AdvancedGasEstimator(signer);
this.smartBatcher = new SmartBatcher(signer);
this.feeOptimizer = new FeeOptimizer(signer, feemarketModule);
}
async executeOptimizedBatch(
signerAddress: string,
messages: EncodeObject[],
options: {
strategy?: OptimizedFeeStrategy;
maxGasLimit?: number;
memo?: string;
dryRun?: boolean;
} = {}
): Promise<{
results?: DeliverTxResponse[];
batches: MessageBatch;
fees: BatchFeeResult;
metrics: BatchingMetrics;
gasEstimates: BulkEstimationResult;
}> {
const {
strategy = FeeOptimizer.strategies.balanced,
maxGasLimit = 30_000_000,
memo,
dryRun = false
} = options;
console.log(`Processing ${messages.length} messages with ${strategy.strategy} strategy`);
// 1. Create optimized batches
const { batches, metrics } = await this.smartBatcher.createSmartBatches(
signerAddress,
messages,
{ maxGasLimit, memo }
);
console.log(`Created ${batches.batches.length} optimized batches`);
// 2. Get bulk gas estimates
const gasEstimates = await estimateBulkGas(
this.signer,
signerAddress,
batches.batches,
memo
);
// 3. Get current gas price and optimize fees
const gasPrice = await this.getGasPrice();
const fees = await optimizeBatchFees(
this.signer,
signerAddress,
batches,
gasPrice,
strategy
);
console.log(`Total estimated cost: ${fees.totalFee.amount[0].amount} ${fees.totalFee.amount[0].denom}`);
console.log(`Estimated savings: ${fees.savings.percentage.toFixed(2)}%`);
// 4. Execute transactions (unless dry run)
let results: DeliverTxResponse[] | undefined;
if (!dryRun) {
results = [];
for (let i = 0; i < batches.batches.length; i++) {
const batch = batches.batches[i];
const batchFee = fees.individualFees[i];
try {
console.log(`Executing batch ${i + 1}/${batches.batches.length} with ${batch.length} messages`);
const result = await this.signer.signAndBroadcast(
signerAddress,
batch,
batchFee,
memo
);
results.push(result);
console.log(`Batch ${i + 1} executed successfully: ${result.transactionHash}`);
} catch (error) {
console.error(`Batch ${i + 1} failed:`, error);
throw new Error(`Batch execution failed at batch ${i + 1}: ${error}`);
}
}
}
return {
results,
batches,
fees,
metrics,
gasEstimates
};
}
private async getGasPrice(): Promise<GasPrice> {
if (this.feeOptimizer['feemarketModule']) {
try {
return await this.feeOptimizer['feemarketModule'].generateGasPrice('ncheq');
} catch (error) {
console.warn('Dynamic gas price unavailable, using fallback');
}
}
return GasPrice.fromString('25000ncheq');
}
// Health check method
async validateSetup(): Promise<{
simulationWorking: boolean;
batchingWorking: boolean;
feeCalculationWorking: boolean;
gasPrice: GasPrice;
}> {
const testMessage: EncodeObject = {
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: {
fromAddress: 'cheqd1test',
toAddress: 'cheqd1test2',
amount: [coin('1', 'ncheq')]
}
};
let simulationWorking = false;
let batchingWorking = false;
let feeCalculationWorking = false;
let gasPrice: GasPrice;
try {
// Test simulation (will fail but should not throw for wrong reason)
await this.signer.simulate('cheqd1test', [testMessage]);
simulationWorking = true;
} catch (error) {
simulationWorking = error.message.includes('account') || error.message.includes('not found');
}
try {
// Test batching
await this.signer.batchMessages([testMessage], 'cheqd1test');
batchingWorking = true;
} catch (error) {
batchingWorking = error.message.includes('account') || error.message.includes('not found');
}
try {
gasPrice = await this.getGasPrice();
feeCalculationWorking = true;
} catch (error) {
gasPrice = GasPrice.fromString('25000ncheq');
feeCalculationWorking = false;
}
return {
simulationWorking,
batchingWorking,
feeCalculationWorking,
gasPrice
};
}
}
// Example usage
async function main() {
const signer = await CheqdSigningStargateClient.connectWithSigner(
'https://rpc.cheqd.net',
wallet,
{
registry: createDefaultCheqdRegistry(),
endpoint: 'https://rpc.cheqd.net'
}
);
const sdk = await createCheqdSDK({
modules: [FeemarketModule],
network: CheqdNetwork.Mainnet,
wallet: wallet,
rpcEndpoint: 'https://rpc.cheqd.net'
});
const txManager = new ProductionTransactionManager(signer, sdk.modules.feemarket);
// Validate setup
const health = await txManager.validateSetup();
console.log('Setup validation:', health);
// Execute optimized batch
const result = await txManager.executeOptimizedBatch(
signerAddress,
messages,
{
strategy: FeeOptimizer.strategies.balanced,
dryRun: false // Set to true for testing
}
);
console.log('Execution completed:', {
batchCount: result.batches.batches.length,
totalMessages: result.metrics.totalMessages,
gasUtilization: result.metrics.gasUtilization,
costSavings: result.fees.savings.percentage
});
}
Error Handling and Resilience
Robust Error Handling
interface SimulationError {
type: 'network' | 'account' | 'gas' | 'validation' | 'unknown';
message: string;
recoverable: boolean;
}
class ResilientSimulator {
private signer: CheqdSigningStargateClient;
private retryConfig: {
maxRetries: number;
baseDelay: number;
maxDelay: number;
};
constructor(
signer: CheqdSigningStargateClient,
retryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 5000 }
) {
this.signer = signer;
this.retryConfig = retryConfig;
}
private classifyError(error: any): SimulationError {
const message = error.message || error.toString();
if (message.includes('account') && message.includes('not found')) {
return { type: 'account', message, recoverable: false };
} else if (message.includes('network') || message.includes('connection')) {
return { type: 'network', message, recoverable: true };
} else if (message.includes('gas') || message.includes('limit')) {
return { type: 'gas', message, recoverable: false };
} else if (message.includes('invalid') || message.includes('malformed')) {
return { type: 'validation', message, recoverable: false };
} else {
return { type: 'unknown', message, recoverable: true };
}
}
async simulateWithRetry(
signerAddress: string,
messages: EncodeObject[],
memo?: string
): Promise<{ gasUsed: number; attempts: number; errors: SimulationError[] }> {
const errors: SimulationError[] = [];
let lastError: SimulationError | null = null;
for (let attempt = 1; attempt <= this.retryConfig.maxRetries; attempt++) {
try {
const gasUsed = await this.signer.simulate(signerAddress, messages, memo);
return { gasUsed, attempts: attempt, errors };
} catch (error) {
const classifiedError = this.classifyError(error);
errors.push(classifiedError);
lastError = classifiedError;
// Don't retry if error is not recoverable
if (!classifiedError.recoverable) {
break;
}
// Don't retry on last attempt
if (attempt === this.retryConfig.maxRetries) {
break;
}
// Exponential backoff
const delay = Math.min(
this.retryConfig.baseDelay * Math.pow(2, attempt - 1),
this.retryConfig.maxDelay
);
console.warn(`Simulation attempt ${attempt} failed, retrying in ${delay}ms:`, classifiedError.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error(`Simulation failed after ${this.retryConfig.maxRetries} attempts. Last error: ${lastError?.message}`);
}
async batchSimulateWithFallback(
signerAddress: string,
messageGroups: EncodeObject[][],
memo?: string,
fallbackGas: number = 200_000
): Promise<{
gasEstimates: number[];
successfulSimulations: number;
failedSimulations: number;
totalFallbacks: number;
}> {
const gasEstimates: number[] = [];
let successfulSimulations = 0;
let failedSimulations = 0;
let totalFallbacks = 0;
for (const messages of messageGroups) {
try {
const { gasUsed } = await this.simulateWithRetry(signerAddress, messages, memo);
gasEstimates.push(gasUsed);
successfulSimulations++;
} catch (error) {
console.warn(`Simulation failed for batch, using fallback gas:`, error);
gasEstimates.push(fallbackGas);
failedSimulations++;
totalFallbacks++;
}
}
return {
gasEstimates,
successfulSimulations,
failedSimulations,
totalFallbacks
};
}
}
Circuit Breaker Pattern
interface CircuitBreakerState {
failures: number;
lastFailure?: Date;
state: 'closed' | 'open' | 'half-open';
}
class SimulationCircuitBreaker {
private state: CircuitBreakerState = {
failures: 0,
state: 'closed'
};
private readonly threshold: number;
private readonly resetTimeout: number;
private readonly fallbackGas: number;
constructor(
threshold: number = 5,
resetTimeout: number = 60000, // 1 minute
fallbackGas: number = 200_000
) {
this.threshold = threshold;
this.resetTimeout = resetTimeout;
this.fallbackGas = fallbackGas;
}
async executeWithCircuitBreaker<T>(
operation: () => Promise<T>,
fallback: () => T
): Promise<T> {
// Check if circuit should reset
if (this.state.state === 'open' && this.shouldReset()) {
this.state.state = 'half-open';
}
// If circuit is open, use fallback immediately
if (this.state.state === 'open') {
console.warn('Circuit breaker is open, using fallback');
return fallback();
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
if (this.state.state === 'open') {
console.warn('Circuit breaker opened, using fallback');
return fallback();
}
throw error;
}
}
private shouldReset(): boolean {
return this.state.lastFailure &&
Date.now() - this.state.lastFailure.getTime() > this.resetTimeout;
}
private onSuccess(): void {
this.state.failures = 0;
this.state.state = 'closed';
}
private onFailure(): void {
this.state.failures++;
this.state.lastFailure = new Date();
if (this.state.failures >= this.threshold) {
this.state.state = 'open';
console.warn(`Circuit breaker opened after ${this.state.failures} failures`);
}
}
getState(): CircuitBreakerState {
return { ...this.state };
}
}
Built-in Transaction Retry Policy
The cheqd SDK includes a sophisticated retry mechanism in the broadcastTx
method that automatically handles transient network failures and ensures reliable transaction delivery. Understanding this built-in functionality helps you design more robust applications.
Retry Policy Overview
The CheqdSigningStargateClient.broadcastTx()
method implements an enhanced retry policy with the following features:
Max Retries
Maximum number of broadcast attempts
3
Timeout
Maximum wait time for transaction inclusion
60,000 ms (60s)
Poll Interval
Frequency of checking for transaction inclusion
3,000 ms (3s)
Backoff Delay
Brief delay between retry attempts
1,000 ms (1s)
How the Retry Policy Works
// Built into the SDK - no additional code needed
const result = await signer.broadcastTx(
txBytes,
60_000, // timeoutMs
3_000, // pollIntervalMs
3 // maxRetries
);
Retry Flow:
Initial Broadcast: Attempts to broadcast transaction using
broadcastTxSync
Transaction Tracking: Records transaction hash for monitoring
Polling Phase: Continuously checks for transaction inclusion in blocks
Failure Handling: On broadcast failure, implements exponential backoff
Retry Logic: Retries up to
maxRetries
times with same signed transactionDouble-Spend Prevention: Reuses identical signed transaction bytes across retries
Retry Scenarios
The retry policy handles several common failure scenarios:
// Example scenarios handled automatically:
// 1. Network congestion - retry broadcast
try {
const result = await signer.signAndBroadcast(address, messages, 'auto');
// SDK automatically retries on temporary network issues
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Transaction hash:', error.txHash); // Available even on timeout
}
}
// 2. Node synchronization issues - poll longer
// The SDK continues polling until timeout or success
// 3. Temporary RPC failures - retry with backoff
// Built-in 1-second backoff between retry attempts
Error Handling and Transaction Tracking
The retry policy provides enhanced error information:
try {
const result = await signer.broadcastTx(txBytes, 60000, 3000, 3);
console.log('Transaction successful:', result.transactionHash);
} catch (error) {
if (error.name === 'TimeoutError') {
// Even on timeout, transaction hash is available
console.log('Transaction may still be processed:', error.txHash);
// You can continue monitoring manually if needed
const finalResult = await signer.getTx(error.txHash);
} else if (error.name === 'BroadcastTxError') {
// Transaction was rejected during CheckTx
console.error('Transaction rejected:', error.log);
}
}
Integration with Gas Simulation
The retry policy works seamlessly with gas simulation and fee calculation:
// Complete example with gas simulation and retry policy
async function executeTransactionWithRetries(
signer: CheqdSigningStargateClient,
address: string,
messages: EncodeObject[]
): Promise<DeliverTxResponse> {
// 1. Simulate for accurate gas estimation
const gasEstimate = await signer.simulate(address, messages, '');
const gasWithMargin = Math.round(gasEstimate * 1.3);
// 2. Calculate fees based on simulation
const fee = calculateDidFee(gasWithMargin, signer.gasPrice);
// 3. Sign transaction
const txRaw = await signer.sign(address, messages, fee, '');
const txBytes = TxRaw.encode(txRaw).finish();
// 4. Broadcast with built-in retry policy
return signer.broadcastTx(
txBytes,
90_000, // Extended timeout for complex transactions
2_000, // More frequent polling
5 // More retries for critical operations
);
}
Production Configuration
For production deployments, you can tune the retry parameters based on your requirements:
// Conservative configuration for critical transactions
const conservativeResult = await signer.broadcastTx(
txBytes,
120_000, // 2-minute timeout
2_000, // Poll every 2 seconds
5 // 5 retry attempts
);
// Fast configuration for less critical operations
const fastResult = await signer.broadcastTx(
txBytes,
30_000, // 30-second timeout
5_000, // Poll every 5 seconds
2 // 2 retry attempts
);
Monitoring Retry Behavior
You can implement monitoring to track retry patterns:
class TransactionMonitor {
private retryStats = {
attempts: [] as number[],
timeouts: 0,
successes: 0
};
async monitoredBroadcast(
signer: CheqdSigningStargateClient,
txBytes: Uint8Array
): Promise<DeliverTxResponse> {
const startTime = Date.now();
try {
const result = await signer.broadcastTx(txBytes, 60000, 3000, 3);
this.retryStats.successes++;
const duration = Date.now() - startTime;
console.log(`Transaction succeeded in ${duration}ms`);
return result;
} catch (error) {
if (error.name === 'TimeoutError') {
this.retryStats.timeouts++;
console.warn('Transaction timeout, but may still be processed');
}
throw error;
}
}
getStats() {
return { ...this.retryStats };
}
}
Key Benefits
The built-in retry policy provides several advantages:
Reliability: Automatic handling of transient network issues
Transaction Safety: Prevents double-spending through transaction reuse
Visibility: Maintains transaction hash tracking even on failures
Configurability: Tunable parameters for different use cases
Integration: Works seamlessly with gas simulation and fee calculation
This retry mechanism significantly improves the reliability of transaction broadcasting without requiring additional application-level retry logic.
Best Practices Summary
Gas Simulation Best Practices
Always Use Safety Margins: Apply 20-50% buffer to simulated gas estimates
Handle Simulation Failures: Implement fallback gas values for production systems
Batch Simulations: Simulate multiple transactions in parallel for efficiency
Monitor Gas Usage: Track actual vs estimated gas consumption
Network-Specific Limits: Respect network gas limits and consensus parameters
Batching Best Practices
Optimal Batch Size: Target 80-90% of max gas limit for efficiency
Transaction Prioritization: Group high-priority transactions together
Type-Aware Batching: Consider transaction complexity when batching
Error Isolation: Design batches to minimize failure propagation
Progress Monitoring: Implement progress tracking for large batch operations
Production Deployment
Comprehensive Testing: Test all scenarios in development environments
Monitoring and Alerting: Track gas usage, failure rates, and cost optimization
Graceful Degradation: Implement fallback strategies for all failure modes
Performance Metrics: Monitor transaction throughput and cost effectiveness
Regular Optimization: Continuously tune parameters based on network conditions
This comprehensive approach to gas simulation and transaction batching will provide optimal performance, cost efficiency, and reliability for production cheqd SDK implementations.
Last updated
Was this helpful?