Advanced Features
Explore advanced features and best practices of MACI SDK.
Transaction Monitoring
Monitor Transaction Status
async function monitorTransaction(
client: MaciClient,
txHash: string
): Promise<void> {
let confirmed = false;
let attempts = 0;
const maxAttempts = 30;
while (!confirmed && attempts < maxAttempts) {
try {
const tx = await client.indexer.getTransactionByHash(txHash);
if (tx.success) {
console.log('Transaction confirmed');
console.log(`Block height: ${tx.height}`);
console.log(`Gas used: ${tx.gasUsed}`);
confirmed = true;
} else {
console.log('Transaction failed');
break;
}
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 2000));
attempts++;
}
}
}Transaction Retry Mechanism
async function retryTransaction<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}
// Usage
const round = await retryTransaction(() =>
client.createAMaciRound({
operator: operator.address,
/* other parameters */
})
);Batch Operations
Batch Query Rounds
async function batchGetRounds(
client: MaciClient,
contractAddresses: string[]
): Promise<Round[]> {
const promises = contractAddresses.map(addr =>
client.getRoundInfo({ contractAddress: addr }).catch(error => {
console.error(`Failed to get ${addr}:`, error.message);
return null;
})
);
const results = await Promise.all(promises);
return results.filter(r => r !== null) as Round[];
}Batch Voting Processing
async function batchVote(
client: MaciClient,
wallet: any,
address: string,
rounds: Array<{
contractAddress: string;
options: { idx: number; vc: number }[];
}>
): Promise<void> {
for (const round of rounds) {
try {
await completeVotingProcess(
client,
wallet,
address,
round.contractAddress,
round.options
);
} catch (error) {
console.error(`Vote failed for ${round.contractAddress}:`, error.message);
}
}
}Custom Network Configuration
Connect to Custom Network
import { MaciClient } from '@dorafactory/maci-sdk';
const customClient = new MaciClient({
network: 'testnet',
rpcEndpoint: 'https://custom-rpc.example.com',
chainId: 'custom-chain-1',
gasPrice: '0.025udora',
registryAddress: 'dora1customregistry...'
});Dynamic Network Switching
class MultiNetworkClient {
private clients: Map<string, MaciClient> = new Map();
getClient(network: 'mainnet' | 'testnet' | 'local'): MaciClient {
if (!this.clients.has(network)) {
this.clients.set(network, new MaciClient({ network }));
}
return this.clients.get(network)!;
}
async getRoundOnAnyNetwork(contractAddress: string) {
for (const network of ['mainnet', 'testnet', 'local'] as const) {
try {
const client = this.getClient(network);
const round = await client.getRoundInfo({ contractAddress });
return { network, round };
} catch (error) {
continue;
}
}
throw new Error('Round not found on any network');
}
}Gas Optimization
Estimate Gas
async function estimateGas(
client: MaciClient,
operation: 'signup' | 'vote' | 'createRound'
): Promise<number> {
const gasEstimates = {
signup: 200000,
vote: 150000,
createRound: 500000
};
return gasEstimates[operation];
}Gas Price Optimization
async function optimizedTransaction<T>(
client: MaciClient,
txFn: () => Promise<T>,
useGasStation: boolean = true
): Promise<T> {
if (useGasStation) {
return await txFn();
} else {
// Pay gas yourself, can dynamically adjust price based on network conditions
return await txFn();
}
}Error Handling Best Practices
Unified Error Handling
class MaciError extends Error {
constructor(
message: string,
public code: string,
public details?: any
) {
super(message);
this.name = 'MaciError';
}
}
async function handleMaciOperation<T>(
operation: () => Promise<T>
): Promise<T> {
try {
return await operation();
} catch (error: any) {
if (error.message.includes('insufficient funds')) {
throw new MaciError('Insufficient balance', 'INSUFFICIENT_FUNDS', error);
} else if (error.message.includes('not in voting period')) {
throw new MaciError('Not in voting period', 'NOT_IN_VOTING_PERIOD', error);
} else if (error.message.includes('already signed up')) {
throw new MaciError('Already signed up', 'ALREADY_SIGNED_UP', error);
} else {
throw new MaciError('Operation failed', 'UNKNOWN_ERROR', error);
}
}
}
// Usage
try {
await handleMaciOperation(() =>
client.maci.signup({ /* parameters */ })
);
} catch (error) {
if (error instanceof MaciError) {
console.error(`Error [${error.code}]: ${error.message}`);
}
}Event Listening
Monitor Round Status Changes
class RoundWatcher {
private intervals: Map<string, NodeJS.Timeout> = new Map();
watch(
client: MaciClient,
contractAddress: string,
callback: (status: string) => void,
interval: number = 10000
): void {
let lastStatus: string | null = null;
const checkStatus = async () => {
try {
const round = await client.getRoundInfo({ contractAddress });
if (round.status !== lastStatus) {
lastStatus = round.status;
callback(round.status);
}
} catch (error) {
console.error('Status check failed:', error);
}
};
checkStatus();
const timer = setInterval(checkStatus, interval);
this.intervals.set(contractAddress, timer);
}
unwatch(contractAddress: string): void {
const timer = this.intervals.get(contractAddress);
if (timer) {
clearInterval(timer);
this.intervals.delete(contractAddress);
}
}
unwatchAll(): void {
for (const timer of this.intervals.values()) {
clearInterval(timer);
}
this.intervals.clear();
}
}
// Usage
const watcher = new RoundWatcher();
watcher.watch(client, 'dora1contract...', (status) => {
console.log(`Round status changed: ${status}`);
if (status === 'Tallied') {
watcher.unwatch('dora1contract...');
}
});Utility Functions
Time Conversion
function timestampToDate(timestamp: number): Date {
return new Date(timestamp * 1000);
}
function dateToTimestamp(date: Date): number {
return Math.floor(date.getTime() / 1000);
}
function formatVotingTime(votingTime: VotingTime): string {
const start = timestampToDate(votingTime.startTime);
const end = timestampToDate(votingTime.endTime);
return `${start.toLocaleString()} - ${end.toLocaleString()}`;
}Voting Weight Calculation
function calculateMaxVotes(voiceCredits: number, isQV: boolean): number {
if (isQV) {
return Math.floor(Math.sqrt(voiceCredits));
} else {
return voiceCredits;
}
}
function calculateVoteCost(
votes: { idx: number; vc: number }[],
isQV: boolean
): number {
return votes.reduce((sum, vote) => {
return sum + (isQV ? vote.vc * vote.vc : vote.vc);
}, 0);
}
function validateVotes(
votes: { idx: number; vc: number }[],
voiceCredits: number,
isQV: boolean
): { valid: boolean; error?: string } {
const cost = calculateVoteCost(votes, isQV);
if (cost > voiceCredits) {
return {
valid: false,
error: `Cost (${cost}) exceeds available credits (${voiceCredits})`
};
}
return { valid: true };
}Performance Optimization
Request Deduplication
class RequestDeduplicator {
private pending: Map<string, Promise<any>> = new Map();
async deduplicate<T>(
key: string,
fn: () => Promise<T>
): Promise<T> {
if (this.pending.has(key)) {
return this.pending.get(key);
}
const promise = fn().finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
}
}
const dedup = new RequestDeduplicator();
// Even with concurrent calls, only one request is sent
const [round1, round2, round3] = await Promise.all([
dedup.deduplicate('round:dora1...', () => client.getRoundById('dora1...')),
dedup.deduplicate('round:dora1...', () => client.getRoundById('dora1...')),
dedup.deduplicate('round:dora1...', () => client.getRoundById('dora1...'))
]);Related Documentation
- Complete Examples - View practical application code
- SDK Overview - Return to SDK documentation homepage
- Quick Start - Get started with MACI quickly
Last updated on