Plugin Framework
Getting Started - Write your first plugin

Plugin Getting Started Guide

Supported Version : 2.0.0-beta3 and later

A beginner-friendly guide to writing your first yaci-store plugin

This guide will walk you through creating simple plugins using MVEL and SpEL expression languages. By the end of this tutorial, you'll understand how to filter and process blockchain data using the yaci-store plugin system.

Prerequisites

  • Yaci-store docker distribution installed
  • Basic understanding of blockchain concepts (transactions, UTXOs, metadata)
  • Familiarity with simple programming concepts

Setup

1. Enable Plugin Support

First, unzip your yaci-store docker distribution and navigate to the directory.

Edit the config/env file to enable plugin support by uncommenting the following line:

JDK_JAVA_OPTIONS=${JDK_JAVA_OPTIONS} -Dloader.path=plugins,plugins/lib,plugins/ext-jars

2. Understanding the Plugin Directory Structure

The plugin system uses the following directory structure:

plugins/
├── scripts/     # Your plugin script files go here
├── ext-jars/    # External JAR files (for advanced use cases)
└── lib/         # Additional libraries

3. Plugin Configuration

Inside the config folder, you'll find application-plugins.yml. This file configures all your plugins.

To enable plugin support, set store.plugins.enabled to true:

store:
  plugins:
    enabled: true

Understanding Plugin Types

Yaci-store supports several types of plugins:

  • Filter Plugins: Control what data gets stored in the database
  • Pre-Action Plugins: Modify data before it's saved
  • Post-Action Plugins: Perform actions after data is saved
  • Event Handler Plugins: React to blockchain events

Plugin Languages

For this guide, we'll focus on two simple expression languages:

  • MVEL: A powerful expression language with Java-like syntax
  • SpEL: Spring Expression Language, great for simple filtering

The Yaci Store plugin system also supports writing plugins in Python and JavaScript. However, Python and JavaScript support is currently in preview. We will cover them in a separate document later.

Your First Plugin: Filtering NFT Metadata

Let's create a plugin that only stores NFT metadata (label 721) in the database.

Method 1: Using Expressions (Simplest)

The easiest way to create a filter is using simple expressions:

MVEL Expression

store:
  plugins:
    enabled: true
    filters:
      metadata.save:
        - name: "NFT Metadata Filter"
          lang: mvel
          expression: label == "721"

SpEL Expression

store:
  plugins:
    enabled: true
    filters:
      metadata.save:
        - name: "NFT Metadata Filter"
          lang: spel
          expression: label == "721"

Both expressions do the same thing: they filter metadata to only include items with label "721" (NFT metadata standard).

Key Points:

  • The metadata.save extension point is triggered when metadata is saved.
  • In an expression-based filter, direct access to the domain class's fields is available.
  • The expression should evaluate to true for the item to be stored.

Method 2: Using Inline Scripts

For more complex logic, you can write inline scripts:

store:
  plugins:
    enabled: true
    filters:
      metadata.save:
        - name: "Multiple Label Filter"
          lang: mvel
          inline-script: |
            filteredItems = [];
            for (item : items) {
                if (item.label == "721" || item.label == "1967") {
                    filteredItems.add(item);
                }
            }
            return filteredItems;

Key Points:

  • In inline scripts, the input is always called items
  • You must return the filtered list
  • Don't include function signatures, just the body

Method 3: Using External Script Files

For reusable or complex logic, create separate script files:

  1. Create plugins/scripts/nft-filter.mvel:
def filterNFTMetadata(items) {
    filteredItems = [];
    for (item : items) {
        if (item.label == "721") {
            filteredItems.add(item);
        }
    }
    return filteredItems;
}
  1. Reference it in your configuration:
store:
  plugins:
    enabled: true
    filters:
      metadata.save:
        - name: "NFT Metadata Filter"
          lang: mvel
          script:
            file: /app/plugins/scripts/nft-filter.mvel
            function: filterNFTMetadata

Note: For Docker deployments, use /app/plugins/scripts/ as the path prefix.

Testing Your Plugin

Quick Start Configuration

To test quickly without syncing from genesis, you can start from a recent block:

# Add to config/application.properties
store.cardano.sync-start-slot=17558626
store.cardano.sync-start-blockhash=3b907cde82b21fb0511845072139aade67222a47c3dcbf90d1e6931b1ad1e30e

Start Yaci-Store

./yaci-store.sh start

Verify the logs to ensure that yaci-store is running correctly and that plugins are enabled:

tail -f logs/yaci-store.log

or,

./yaci_store.sh logs

Verify Results

Connect to the database and check the results:

./psql.sh
-- Set the schema
SET search_path TO yaci_store;
 
-- Check that only NFT metadata (label 721) is stored
SELECT tx_hash, label FROM transaction_metadata;

You should see only metadata with label "721".

Example 2: Filtering High-Value UTXOs

Let's create a plugin that only stores UTXOs worth more than 1000 ADA:

SpEL Expression (Recommended for simple filters)

store:
  plugins:
    enabled: true
    filters:
      utxo.unspent.save:
        - name: "High Value UTXO Filter"
          lang: spel
          expression: lovelaceAmount > 1000000000

MVEL Expression with Multiple Conditions

High value UTXOs without any native assets:

store:
  plugins:
    enabled: true
    filters:
      utxo.unspent.save:
        - name: "High Value and Asset UTXO Filter"
          lang: mvel
          expression: |
            lovelaceAmount > 1000000000 &&
            (amounts != null && amounts.size() == 1)

Example 3: Filtering by Asset Policy

To filter UTXOs that contain specific native assets:

SpEL with Collection Filtering

store:
  plugins:
    enabled: true
    filters:
      utxo.unspent.save:
        - name: "Specific Asset Filter"
          lang: spel
          expression: amounts.?[policyId == 'your_policy_id_here'].size() > 0

MVEL with Custom Logic

store:
  plugins:
    enabled: true
    filters:
      utxo.unspent.save:
        - name: "Multiple Policy Filter"
          lang: mvel
          expression: |
            if (amounts == null) return false;
            foreach (amount : amounts) {
                if (amount.policyId == 'policy1' ||
                    amount.policyId == 'policy2') {
                    return true;
                }
            }
            return false;

Pre-Action Plugins: Modifying Data

Pre-action plugins run after filters but before data is saved. They can modify the data.

Example: Filter utxos with Specific PolicyId and Replace it with a custom label

store:
  plugins:
    enabled: true
    filters:
      utxo.unspent.save:
        - name: "Specific Asset Filter"
          lang: spel
          expression: amounts.?[policyId == 'bfc7197e735d2748024be222ed510a23d8682e9f912769e6d922a276'].size() > 0
    pre-actions:
      utxo.unspent.save:
        - name: "Replace PolicyId in UTXOs"
          lang: mvel
          inline-script: |
            modifiedUtxos = [];
            for (utxo : items) {
              for (amt: utxo.amounts) {
                if (amt.getPolicyId() == "bfc7197e735d2748024be222ed510a23d8682e9f912769e6d922a276") {
                    amt.setPolicyId("MyPolicyId"); // Update policyId
                    modifiedUtxos.add(amt);
                }
              }
            }
            return modifiedUtxos;

Key Points:

  • Pre-action plugins allow you to modify data before it is saved.
  • While most domain objects (items) are immutable, meaning you can update their fields, there are exceptions. For example, TxMetadataLabel cannot be modified as it does not have setter methods. In such cases, you may need to create a new instance with updated values. To check if an object is mutable or not with regard to the storage extension point, refer to the Plugin API Guide.
  • Similar to filter plugins, a pre-action plugin can be written in an external script file or as an inline script.

Post-Action Plugins: Notifications

Post-action plugins run after data is successfully saved. Perfect for notifications.

Example: Log High-Fee Transactions

store:
  plugins:
    enabled: true
    post-actions:
      transaction.save:
        - name: "High Fee Transaction Logger"
          lang: mvel
          inline-script: |
            highFeeTxCount = 0;
            totalOutput = 0;
            for (txn : items) {
                if (txn.fee > 1000000) { // > 1 ADA
                    highFeeTxCount++;
                }
            }
            if (highFeeTxCount > 0) {
                System.out.println("Found " + highFeeTxCount + " high-fee transactions");
            }

Event Handler Plugins: Reacting to Events

Event handlers listen to blockchain events and can perform custom actions.

Example: Block Processing Monitor

store:
  plugins:
    enabled: true
    event-handlers:
      BlockEvent:
        - name: "Block Monitor"
          lang: mvel
          inline-script: |
            block = event.block;
            System.out.println("Processing block " + block.header.headerBody.blockNumber +
                             " with " + block.transactionBodies.size() + " transactions");

Common Extension Points

Here are the most commonly used extension points:

Storage Extension Points

  • metadata.save - Transaction metadata
  • utxo.unspent.save - New unspent UTXOs
  • transaction.save - Transaction data
  • asset.mint.save - Asset minting operations

Event Types

  • BlockEvent - New blocks
  • TransactionEvent - Transaction processing
  • AddressUtxoEvent - Address UTXO changes
  • MintBurnEvent - Asset mint/burn operations

Tips for Success

1. Start Simple

Begin with expression-based filters before moving to scripts.

2. Test Incrementally

Test each plugin individually before combining multiple plugins.

3. Use Appropriate Field Names

Make sure field names in your expressions match the Java model exactly:

  • lovelaceAmount (not lovelace_amount)
  • policyId (not policy_id)

4. Handle Null Values

Always check for null values in your expressions:

// Good
amounts != null && amounts.size() > 0

// Bad (can cause errors)
amounts.size() > 0

5. Multiple Filters = AND Logic

If you have multiple filters for the same extension point, they all must return true (AND logic).

Common Mistakes to Avoid

  1. Forgetting semicolons in MVEL: Always end statements with ;
  2. Wrong field names: Use exact Java field names
  3. Not handling null values: Check for null before accessing properties
  4. Wrong file paths: Use /app/plugins/scripts/ for Docker deployments

Non-Docker Distribution

If you're using a non-Docker distribution:

  1. Uncomment this line in bin/start.sh:

    JAVA_OPTS="$JAVA_OPTS -Dloader.path=plugins,plugins/lib,plugins/ext-jars"
  2. Use relative paths in your configuration:

    script:
      file: ./plugins/scripts/your-script.mvel

Troubleshooting

Plugin Not Working?

  1. Check that store.plugins.enabled=true
  2. Verify file paths are correct
  3. Check logs for syntax errors
  4. Ensure field names match Java models

Syntax Errors?

  1. Start with simple expressions
  2. Check for missing semicolons
  3. Verify parentheses matching

With these basics, you're ready to start building powerful blockchain data processing plugins!

Next Steps

Once you're comfortable with basic plugins, explore: