Building a Lite Stateful VIDA

A learning-focused implementation of stateful applications

A Lite Stateful VIDA is a learning-focused version of a stateful VIDA that demonstrates core concepts without production complexity.

Key Characteristics:

Stateful = Remembers data between transactions

// Each transaction builds on previous state
User A: 1000 tokens → Transfer 100 to User B → User A: 900 tokens
User B: 500 tokens → Receives 100 from User A → User B: 600 tokens

Lite = Simplified for learning

  • In-memory storage (HashMap) instead of databases

  • Single instance instead of distributed validation

  • Simple logging instead of production monitoring

  • Basic error handling instead of complex recovery

What Makes It "Stateful"?

Unlike stateless VIDAs that process each transaction independently, stateful VIDAs:

  1. Remember Previous Transactions: Each new transaction can depend on what happened before

  2. Maintain Application State: User balances, game scores, inventory levels persist

  3. Process Sequentially: Transactions must be handled in blockchain order

  4. Provide Consistency: All instances of the VIDA reach the same state

Lite vs Production Comparison:

Feature
Lite VIDA
Production VIDA

Storage

HashMap (memory)

Merkle Trees

Validation

Single instance

Multi-instance consensus

Recovery

Restart from scratch

Crash recovery + rollback

APIs

None

HTTP REST endpoints

Learning Focus

⭐⭐⭐⭐⭐

⭐⭐

Production Ready


Prerequisites to Building a Lite Stateful VIDA


Building a Lite Stateful VIDA

In this tutorial we will build a token transfer system.

1. Select an ID for Your VIDA

2. Import the PWR SDK

3. Initializing PWR with an RPC Endpoint

4. Create and Fund a Wallet

5. Define Transaction Data Structure

While PWR Chain stores all transaction data as raw byte arrays, VIDAs can encode this data into structured formats like JSON. Defining a schema for your transactions ensures consistency, simplifies development, and enables collaboration across teams.

Why Define a Schema?

  • Consistency: Ensures all transactions follow a predictable format.

  • Documentation: Serves as a reference for developers interacting with your VIDA.

  • Validation: Helps catch malformed data early.

Example:

[
    {
        "action": "send-tokens-v1",
        "receiver": "0xC767EA1D613EEFE0CE1610B18CB047881BAFB829",
        "amount": 1000000
    }
]

6. Setup Hashmap and Transfer Function

The Hash Map will be used to store all balances.

    private static Map<String /*Address*/, Long /*Balance*/> userTokenBalances = new HashMap<>();
    
    private static boolean transferTokens(String from, String to, long amount) {
        if (from == null || to == null || amount <= 0) {
            System.err.println("Invalid transfer parameters: from=" + from + ", to=" + to + ", amount=" + amount);
            return false;
        }
        
        // Normalize addresses to lowercase for consistency
        from = from.toLowerCase();
        to = to.toLowerCase();

        Long fromBalance = userTokenBalances.get(from);
        if (fromBalance == null || fromBalance < amount) {
            System.err.println("Insufficient balance for transfer: " + from + " has " + fromBalance + ", trying to transfer " + amount);
            return false;
        }

        userTokenBalances.put(from, fromBalance - amount);
        userTokenBalances.put(to, userTokenBalances.getOrDefault(to, 0L) + amount);
        System.out.println("Transfer successful: " + amount + " tokens from " + from + " to " + to);

        return true;
    }

7. Define a Starting Block

Stateful VIDAs must define a starting block because they need to build up their state by processing every relevant transaction in order. Without knowing where to start, they can't guarantee their state is correct.

Best Practice: Set your starting block to the latest PWR Chain block at the time of your VIDA's development or launch, since previous blocks won't contain any transactions for your VIDA (it didn't exist yet).

You can find the current latest block at: https://explorer.pwrlabs.io/

private static final long START_BLOCK = 22338;

8. Set Initial Balances

Since we're creating a token VIDA, some addresses must have tokens when the VIDA launches. Without initial balances, no one would have tokens to transfer, making the system unusable.

userTokenBalances.put("0xc767ea1d613eefe0ce1610b18cb047881bafb829".toLowerCase(), 1_000_000_000_000L); //Replace the address with your address or any desired address
userTokenBalances.put("0x3b4412f57828d1ceb0dbf0d460f7eb1f21fed8b4".toLowerCase(), 1_000_000_000_000L);

9. Read Data from PWR Chain & Handle it

Stateful VIDAs need to read data from PWR Chain to update their state. This is done by subscribing to the VIDA's transactions and handling them accordingly.

private static void readAndHandleData(PWRJ pwrj, long vidaId, long startingBlock) throws IOException {
    pwrj.subscribeToVidaTransactions(pwrj, vidaId, startingBlock, null, (transaction) -> {
        String from = transaction.getSender();
        String to = transaction.getReceiver();
        byte[] data = transaction.getData();
        
        // Normalize addresses to ensure they start with "0x" because the RPC might return them without it
        if(!from.startsWith("0x")) from = "0x" + from;
        if(!to.startsWith("0x")) to = "0x" + to;

        JSONObject jsonData = new JSONObject(new String(data));
        String action = jsonData.optString("action", "");
        
        if(action.equalsIgnoreCase("transfer")) {
            long amount = jsonData.optLong("amount", 0);
            
            if (transferTokens(from, to, amount)) {
                System.out.println("Transaction processed: " + from + " transferred " + amount + " tokens to " + to);
            } else {
                System.err.println("Failed to process transaction: " + from + " -> " + to + " for amount: " + amount);
            }
        }
    });
}

10. Send Transactions

private static boolean transferTokens(PWRFalconWallet wallet, String receiver, long amount) throws IOException {
    String senderAddress = wallet.getAddress().toLowerCase();
    long senderBalance = getBalance(senderAddress);

    if (senderBalance < amount) {
        System.err.println("Insufficient balance for transfer: " + senderAddress + " has " + senderBalance + ", trying to transfer " + amount);
        return false;
    }

    // Normalize receiver address to ensure it starts with "0x"
    if(!receiver.startsWith("0x")) {
        receiver = "0x" + receiver; // Ensure the address starts with "0x"
    }

    JSONObject transferData = new JSONObject();
    transferData.put("action", "send-tokens-v1");
    transferData.put("receiver", receiver);
    transferData.put("amount", amount);

    byte[] data = transferData.toString().getBytes();

    Response response = wallet.submitPayableVidaData(VIDA_ID, data, 0, pwrj.getFeePerByte());
    if (!response.isSuccess()) {
        System.err.println("Failed to transfer tokens: " + response.getError());
        return false;
    }

    return true;
}

Final Notes & Best Practices

When building a Lite Stateful VIDA, your primary goal is to maintain the benefits of a stateful design—verifiable consistency, auditability, and resilience—while minimizing complexity and overhead. By storing only essential state, validating transactions from a known checkpoint, and leveraging PWR Chain’s immutable ledger, you can achieve strong guarantees without excessive resource usage.

Last updated

Was this helpful?