Docs

suix_queryTransactionBlocks - Query Tran...

Query and filter Sui transactions using suix_queryTransactionBlocks RPC method. Search by sender, recipient, transaction kind, time range with pagination support using Dwellir's high-performance Sui infrastructure.

Queries and filters transaction blocks on the Sui blockchain with support for various search criteria including sender, recipient, transaction type, and time-based filtering with pagination support.

Overview

The suix_queryTransactionBlocks method is a powerful transaction search and filtering tool that enables applications to find specific transactions based on multiple criteria. Unlike simple transaction lookups by digest, this method allows complex queries to build transaction histories, monitor specific addresses, track particular transaction types, and perform analytical queries. It's essential for wallets, explorers, analytics tools, and monitoring systems.

Code Examples

Common Use Cases

1. Wallet Transaction History

JavaScript
async function buildWalletHistory(address, limit = 100) {
  const transactions = await getAllTransactionHistory(address, limit);
  
  const history = transactions.map(txn => {
    const timestamp = parseInt(txn.timestampMs || '0');
    const status = txn.effects?.status?.status || 'unknown';
    
    // Determine transaction direction and counterpart
    let direction = 'unknown';
    let counterpart = null;
    let amount = 0;
    let coinType = '0x2::sui::SUI';
    
    if (txn.balanceChanges) {
      const userChange = txn.balanceChanges.find(
        change => change.owner?.AddressOwner === address
      );
      
      if (userChange) {
        amount = Math.abs(parseInt(userChange.amount));
        coinType = userChange.coinType;
        direction = parseInt(userChange.amount) >= 0 ? 'received' : 'sent';
        
        // Find counterpart
        const counterpartChange = txn.balanceChanges.find(
          change => change.owner?.AddressOwner !== address && 
                   change.coinType === coinType
        );
        
        if (counterpartChange) {
          counterpart = counterpartChange.owner?.AddressOwner;
        }
      }
    }
    
    return {
      digest: txn.digest,
      timestamp: new Date(timestamp),
      status: status,
      direction: direction,
      counterpart: counterpart,
      amount: amount,
      coinType: coinType,
      gasUsed: txn.effects?.gasUsed,
      type: txn.transaction?.data?.transaction?.kind || 'Unknown'
    };
  });
  
  return history.sort((a, b) => b.timestamp - a.timestamp);
}

2. DeFi Protocol Analytics

JavaScript
async function analyzeDeFiProtocol(packageId, module, functions = []) {
  const protocolTransactions = [];
  
  // Query transactions for each function
  for (const functionName of functions) {
    const txns = await getMoveCallTransactions(packageId, module, functionName);
    
    txns.forEach(txn => {
      txn._functionCalled = functionName;
      protocolTransactions.push(txn);
    });
  }
  
  // Sort by timestamp
  protocolTransactions.sort((a, b) => 
    parseInt(b.timestampMs || '0') - parseInt(a.timestampMs || '0')
  );
  
  // Analyze usage patterns
  const analysis = {
    totalTransactions: protocolTransactions.length,
    uniqueUsers: new Set(),
    functionUsage: {},
    dailyVolume: {},
    gasMetrics: {
      total: 0,
      average: 0,
      min: Infinity,
      max: 0
    },
    errorRate: 0
  };
  
  let failedCount = 0;
  
  protocolTransactions.forEach(txn => {
    // User tracking
    const sender = txn.transaction?.data?.sender;
    if (sender) analysis.uniqueUsers.add(sender);
    
    // Function usage
    const func = txn._functionCalled;
    analysis.functionUsage[func] = (analysis.functionUsage[func] || 0) + 1;
    
    // Daily volume
    const timestamp = parseInt(txn.timestampMs || '0');
    if (timestamp > 0) {
      const day = new Date(timestamp).toISOString().split('T')[0];
      analysis.dailyVolume[day] = (analysis.dailyVolume[day] || 0) + 1;
    }
    
    // Gas analysis
    const gasUsed = txn.effects?.gasUsed;
    if (gasUsed) {
      const totalGas = parseInt(gasUsed.computationCost) + 
                      parseInt(gasUsed.storageCost) - 
                      parseInt(gasUsed.storageRebate);
      
      analysis.gasMetrics.total += totalGas;
      analysis.gasMetrics.min = Math.min(analysis.gasMetrics.min, totalGas);
      analysis.gasMetrics.max = Math.max(analysis.gasMetrics.max, totalGas);
    }
    
    // Error tracking
    if (txn.effects?.status?.status !== 'success') {
      failedCount++;
    }
  });
  
  analysis.uniqueUsers = analysis.uniqueUsers.size;
  analysis.gasMetrics.average = analysis.gasMetrics.total / protocolTransactions.length;
  analysis.errorRate = failedCount / protocolTransactions.length;
  
  return { transactions: protocolTransactions, analysis };
}

3. Address Relationship Mapping

JavaScript
async function mapAddressRelationships(centralAddress, depth = 2) {
  const relationships = {
    nodes: new Set([centralAddress]),
    edges: [],
    transactions: new Map()
  };
  
  const queue = [{ address: centralAddress, level: 0 }];
  const processed = new Set();
  
  while (queue.length > 0 && queue[0].level < depth) {
    const { address, level } = queue.shift();
    
    if (processed.has(address)) continue;
    processed.add(address);
    
    console.log(`Processing level ${level}: ${address}`);
    
    // Get transactions from this address
    const sentTransactions = await getCompleteTransactionHistory(address, 5);
    
    // Get transactions to this address
    const receivedResult = await client.queryTransactionBlocks({
      filter: { ToAddress: address },
      options: { showBalanceChanges: true },
      limit: 100,
      order: 'descending'
    });
    
    // Process sent transactions
    sentTransactions.forEach(txn => {
      txn.balanceChanges?.forEach(change => {
        const recipient = change.owner?.AddressOwner;
        
        if (recipient && recipient !== address && !relationships.nodes.has(recipient)) {
          relationships.nodes.add(recipient);
          relationships.edges.push({
            from: address,
            to: recipient,
            amount: Math.abs(parseInt(change.amount)),
            coinType: change.coinType,
            txDigest: txn.digest,
            timestamp: parseInt(txn.timestampMs || '0')
          });
          
          if (level + 1 < depth) {
            queue.push({ address: recipient, level: level + 1 });
          }
        }
      });
    });
    
    // Process received transactions
    receivedResult.data.forEach(txn => {
      const sender = txn.transaction?.data?.sender;
      
      if (sender && sender !== address && !relationships.nodes.has(sender)) {
        relationships.nodes.add(sender);
        
        const relevantChange = txn.balanceChanges?.find(
          change => change.owner?.AddressOwner === address
        );
        
        if (relevantChange) {
          relationships.edges.push({
            from: sender,
            to: address,
            amount: Math.abs(parseInt(relevantChange.amount)),
            coinType: relevantChange.coinType,
            txDigest: txn.digest,
            timestamp: parseInt(txn.timestampMs || '0')
          });
        }
        
        if (level + 1 < depth) {
          queue.push({ address: sender, level: level + 1 });
        }
      }
    });
  }
  
  return {
    nodes: Array.from(relationships.nodes),
    edges: relationships.edges,
    centralAddress,
    depth,
    totalNodes: relationships.nodes.size,
    totalEdges: relationships.edges.length
  };
}

4. Transaction Pattern Detection

JavaScript
class TransactionPatternDetector {
  constructor(client) {
    this.client = client;
    this.patterns = {
      MEV: [],
      arbitrage: [],
      liquidations: [],
      flashLoans: [],
      suspiciousActivity: []
    };
  }
  
  async detectPatterns(address, lookbackDays = 7) {
    const transactions = await getCompleteTransactionHistory(address, 50);
    
    // Filter recent transactions
    const cutoff = Date.now() - (lookbackDays * 24 * 60 * 60 * 1000);
    const recentTxns = transactions.filter(txn => 
      parseInt(txn.timestampMs || '0') > cutoff
    );
    
    await Promise.all([
      this.detectArbitragePatterns(recentTxns),
      this.detectMEVPatterns(recentTxns),
      this.detectSuspiciousActivity(recentTxns),
      this.detectFlashLoanPatterns(recentTxns)
    ]);
    
    return this.patterns;
  }
  
  async detectArbitragePatterns(transactions) {
    // Look for sequences of trades that result in profit
    const trades = transactions.filter(txn => 
      txn.events?.some(event => event.type.includes('Swap') || event.type.includes('Trade'))
    );
    
    for (let i = 0; i < trades.length - 1; i++) {
      const trade1 = trades[i];
      const trade2 = trades[i + 1];
      
      const timeDiff = parseInt(trade1.timestampMs) - parseInt(trade2.timestampMs);
      
      // Trades within 5 minutes might be arbitrage
      if (Math.abs(timeDiff) < 5 * 60 * 1000) {
        // Analyze balance changes to detect profit
        const profit = this.calculateProfitBetweenTrades(trade1, trade2);
        
        if (profit > 0) {
          this.patterns.arbitrage.push({
            trades: [trade1.digest, trade2.digest],
            profit: profit,
            timeSpan: Math.abs(timeDiff),
            confidence: this.calculateArbitrageConfidence(trade1, trade2)
          });
        }
      }
    }
  }
  
  async detectMEVPatterns(transactions) {
    // Look for transactions that appear to front-run or back-run others
    for (let i = 0; i < transactions.length; i++) {
      const txn = transactions[i];
      
      // Check if this transaction has unusually high gas price
      const gasPrice = parseInt(txn.transaction?.data?.gasData?.price || '0');
      
      if (gasPrice > 1500) { // Higher than typical 750
        // Check for related transactions in the same block/checkpoint
        const sameCheckpoint = transactions.filter(t => 
          t.checkpoint === txn.checkpoint && t.digest !== txn.digest
        );
        
        if (sameCheckpoint.length > 0) {
          this.patterns.MEV.push({
            frontRunTx: txn.digest,
            relatedTxs: sameCheckpoint.map(t => t.digest),
            gasPrice: gasPrice,
            checkpoint: txn.checkpoint,
            confidence: this.calculateMEVConfidence(txn, sameCheckpoint)
          });
        }
      }
    }
  }
  
  async detectSuspiciousActivity(transactions) {
    // Look for potential wash trading or circular transactions
    const addressCounts = new Map();
    const timeWindows = new Map();
    
    transactions.forEach(txn => {
      txn.balanceChanges?.forEach(change => {
        const addr = change.owner?.AddressOwner;
        if (addr) {
          addressCounts.set(addr, (addressCounts.get(addr) || 0) + 1);
        }
      });
      
      // Group by hour
      const hour = Math.floor(parseInt(txn.timestampMs || '0') / (60 * 60 * 1000));
      if (!timeWindows.has(hour)) {
        timeWindows.set(hour, []);
      }
      timeWindows.get(hour).push(txn);
    });
    
    // Check for addresses with excessive interactions
    for (const [address, count] of addressCounts) {
      if (count > 10) { // Threshold for suspicious activity
        this.patterns.suspiciousActivity.push({
          type: 'excessive_interactions',
          address: address,
          interactionCount: count,
          confidence: Math.min(count / 20, 1)
        });
      }
    }
    
    // Check for high-frequency trading in short windows
    for (const [hour, hourTxns] of timeWindows) {
      if (hourTxns.length > 20) {
        this.patterns.suspiciousActivity.push({
          type: 'high_frequency_trading',
          hour: new Date(hour * 60 * 60 * 1000).toISOString(),
          transactionCount: hourTxns.length,
          confidence: Math.min(hourTxns.length / 50, 1)
        });
      }
    }
  }
  
  calculateProfitBetweenTrades(trade1, trade2) {
    // Simplified profit calculation - would need more sophisticated analysis
    const balance1 = this.sumBalanceChanges(trade1.balanceChanges || []);
    const balance2 = this.sumBalanceChanges(trade2.balanceChanges || []);
    
    return balance1 + balance2; // Simplified
  }
  
  sumBalanceChanges(changes) {
    return changes.reduce((sum, change) => {
      if (change.coinType === '0x2::sui::SUI') {
        return sum + parseInt(change.amount || '0');
      }
      return sum;
    }, 0);
  }
  
  calculateArbitrageConfidence(trade1, trade2) {
    // Simple confidence calculation based on various factors
    let confidence = 0.5;
    
    // Close timing increases confidence
    const timeDiff = Math.abs(parseInt(trade1.timestampMs) - parseInt(trade2.timestampMs));
    if (timeDiff < 60000) confidence += 0.3; // Within 1 minute
    else if (timeDiff < 300000) confidence += 0.2; // Within 5 minutes
    
    // Multiple balance changes suggest complex trading
    const totalChanges = (trade1.balanceChanges?.length || 0) + (trade2.balanceChanges?.length || 0);
    if (totalChanges > 4) confidence += 0.2;
    
    return Math.min(confidence, 1);
  }
  
  calculateMEVConfidence(txn, relatedTxns) {
    let confidence = 0.3; // Base confidence for high gas price
    
    // More related transactions increase confidence
    confidence += Math.min(relatedTxns.length * 0.1, 0.5);
    
    // Success of high-gas transaction increases confidence
    if (txn.effects?.status?.status === 'success') {
      confidence += 0.2;
    }
    
    return Math.min(confidence, 1);
  }
}

Advanced Query Techniques

1. Time-Range Queries

JavaScript
async function queryTransactionsByTimeRange(address, startTime, endTime) {
  const allTransactions = await getCompleteTransactionHistory(address, 100);
  
  const filteredTransactions = allTransactions.filter(txn => {
    const timestamp = parseInt(txn.timestampMs || '0');
    return timestamp >= startTime && timestamp <= endTime;
  });
  
  // Group by day
  const dailyGroups = {};
  filteredTransactions.forEach(txn => {
    const day = new Date(parseInt(txn.timestampMs)).toISOString().split('T')[0];
    if (!dailyGroups[day]) {
      dailyGroups[day] = [];
    }
    dailyGroups[day].push(txn);
  });
  
  return {
    totalTransactions: filteredTransactions.length,
    timeRange: {
      start: new Date(startTime).toISOString(),
      end: new Date(endTime).toISOString()
    },
    dailyBreakdown: Object.entries(dailyGroups).map(([day, txns]) => ({
      date: day,
      count: txns.length,
      successful: txns.filter(t => t.effects?.status?.status === 'success').length
    }))
  };
}

2. Multi-Filter Queries

JavaScript
async function complexTransactionQuery(filters) {
  // Execute multiple queries in parallel
  const queryPromises = filters.map(async filter => {
    const result = await client.queryTransactionBlocks({
      filter: filter,
      options: {
        showEffects: true,
        showBalanceChanges: true
      },
      limit: 100,
      order: 'descending'
    });
    
    return {
      filter: filter,
      transactions: result.data,
      count: result.data.length
    };
  });
  
  const results = await Promise.all(queryPromises);
  
  // Find intersections and unions
  const allTransactions = new Map();
  const filterResults = new Map();
  
  results.forEach(result => {
    filterResults.set(JSON.stringify(result.filter), result);
    
    result.transactions.forEach(txn => {
      if (!allTransactions.has(txn.digest)) {
        allTransactions.set(txn.digest, {
          transaction: txn,
          matchingFilters: []
        });
      }
      
      allTransactions.get(txn.digest).matchingFilters.push(result.filter);
    });
  });
  
  return {
    individualResults: results,
    combinedResults: Array.from(allTransactions.values()),
    intersections: Array.from(allTransactions.values()).filter(
      item => item.matchingFilters.length > 1
    ),
    totalUniqueTransactions: allTransactions.size
  };
}

Performance Considerations

1. Efficient Pagination

JavaScript
class EfficientTransactionPaginator {
  constructor(client) {
    this.client = client;
    this.pageCache = new Map();
    this.maxCacheSize = 100;
  }
  
  async getPage(filter, cursor = null, limit = 50) {
    const cacheKey = `${JSON.stringify(filter)}_${cursor || 'start'}_${limit}`;
    
    if (this.pageCache.has(cacheKey)) {
      return this.pageCache.get(cacheKey);
    }
    
    const result = await this.client.queryTransactionBlocks({
      filter: filter,
      cursor: cursor,
      limit: limit,
      options: { showEffects: true },
      order: 'descending'
    });
    
    // Cache management
    if (this.pageCache.size >= this.maxCacheSize) {
      const firstKey = this.pageCache.keys().next().value;
      this.pageCache.delete(firstKey);
    }
    
    this.pageCache.set(cacheKey, result);
    return result;
  }
  
  async *iterateAllPages(filter, batchSize = 50) {
    let cursor = null;
    let hasMore = true;
    
    while (hasMore) {
      const page = await this.getPage(filter, cursor, batchSize);
      
      yield page.data;
      
      cursor = page.nextCursor;
      hasMore = page.hasNextPage;
      
      // Small delay to prevent overwhelming the RPC
      if (hasMore) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
    }
  }
}

2. Query Optimization

JavaScript
async function optimizeQueryPerformance(address, analysisDepth = 'shallow') {
  const queries = {
    shallow: {
      limit: 20,
      options: { showEffects: true }
    },
    medium: {
      limit: 100,
      options: { showEffects: true, showBalanceChanges: true }
    },
    deep: {
      limit: 500,
      options: {
        showInput: true,
        showEffects: true,
        showEvents: true,
        showBalanceChanges: true
      }
    }
  };
  
  const config = queries[analysisDepth] || queries.shallow;
  
  const startTime = Date.now();
  
  const result = await client.queryTransactionBlocks({
    filter: { FromAddress: address },
    ...config,
    order: 'descending'
  });
  
  const endTime = Date.now();
  
  return {
    transactions: result.data,
    performance: {
      queryTime: endTime - startTime,
      transactionCount: result.data.length,
      avgTimePerTransaction: (endTime - startTime) / result.data.length,
      analysisDepth: analysisDepth
    }
  };
}

Best Practices

1. Query Optimization

  • Use specific filters to reduce result sets and improve performance
  • Request only needed data through selective options configuration
  • Implement pagination properly for large datasets
  • Cache frequent queries to reduce API load

2. Performance Management

  • Batch related queries when possible
  • Use descending order for most recent transactions first
  • Implement rate limiting to avoid hitting API limits
  • Monitor query response times and adjust accordingly

3. Data Processing

  • Stream process large datasets instead of loading everything into memory
  • Use incremental updates for real-time monitoring
  • Implement proper error handling for network timeouts
  • Validate response data before processing

4. Analysis Strategies

  • Combine multiple filters for complex queries
  • Use time-based filtering to focus on relevant periods
  • Track patterns across transactions for behavioral analysis
  • Implement anomaly detection for security monitoring

5. User Experience

  • Show loading indicators for long-running queries
  • Implement progressive loading for better responsiveness
  • Provide meaningful error messages for failed queries
  • Cache results to improve repeat access performance

Need help? Contact our support team or check the Sui documentation.