Skip to main content

Building a Lite Stateful VIDA

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:

FeatureLite VIDAProduction VIDA
StorageHashMap (memory)Merkle Trees
ValidationSingle instanceMulti-instance consensus
RecoveryRestart from scratchCrash recovery + rollback
APIsNoneHTTP REST endpoints
Learning FocusBasicAdvanced
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. Import the PWR SDK.
  2. Select an ID for Your VIDA.
  3. Initializing PWR with an RPC Endpoint.
  4. Create and Fund a Wallet.

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
}
]

Setup Hashmap and Transfer Function

The Hash Map will be used to store all balances.

import PWRJS from '@pwrjs/core';

const VIDA_ID = YOUR_VIDA_ID;
const START_BLOCK = 350000;
const RPC_ENDPOINT = "https://pwrrpc.pwrlabs.io/";

const userTokenBalances = new Map();

function getBalance(address) {
address = address.startsWith("0x") ? address : "0x" + address;
return userTokenBalances.get(address.toLowerCase()) || 0n;
}

function setBalance(address, balance) {
address = address.startsWith("0x") ? address : "0x" + address;
userTokenBalances.set(address.toLowerCase(), balance);
}

function transferTokens(from, to, amount) {
if (!from || !to || amount <= 0n) {
console.error(`Invalid transfer parameters: from=${from}, to=${to}, amount=${amount}`);
return false;
}

// Normalize addresses for consistency
from = from.startsWith("0x") ? from : "0x" + from;
to = to.startsWith("0x") ? to : "0x" + to;

const fromBalance = getBalance(from);
if (fromBalance < amount) {
console.error(`Insufficient balance for transfer: ${from.toLowerCase()} has ${fromBalance}, trying to transfer ${amount}`);
return false;
}

// Perform the transfer
setBalance(from, fromBalance - amount);
const toBalance = getBalance(to);
setBalance(to, toBalance + amount);

console.log(`New balances - ${from.toLowerCase()}: ${getBalance(from)}, ${to.toLowerCase()}: ${getBalance(to)}`);

return true;
}

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/.

const START_BLOCK = 350000;

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.

function setupInitialBalances() {
setBalance("0xc767ea1d613eefe0ce1610b18cb047881bafb829", 1_000_000_000_000n);
setBalance("0x3b4412f57828d1ceb0dbf0d460f7eb1f21fed8b4", 1_000_000_000_000n);
}

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.

function processTransaction(transaction) {
try {
const from = (transaction.sender).startsWith("0x") ? transaction.sender : "0x" + transaction.sender;
const data = transaction.data;

// Parse transaction data as JSON
let jsonData;
try {
// Convert hex data to string and parse JSON
const dataBytes = Buffer.from(data, 'hex');
const dataString = dataBytes.toString('utf8');
jsonData = JSON.parse(dataString);
} catch (parseError) {
console.error(`Failed to parse transaction data: ${parseError.message}`);
return;
}

const action = jsonData.action || "";

if (action === "send-tokens-v1") {
const amount = BigInt(jsonData.amount || 0);
const receiver = jsonData.receiver.startsWith("0x") ? jsonData.receiver : "0x" + jsonData.receiver;

console.log(`Transfer request: ${amount} tokens from ${from.toLowerCase()} to ${receiver.toLowerCase()}`);

if (transferTokens(from, receiver, amount)) {
console.log(`✅ Transaction processed successfully`);
} else {
console.error(`❌ Failed to process transaction`);
}
} else {
console.log(`Unknown action: ${action}`);
}

} catch (error) {
console.error(`Error processing transaction: ${error.message}`);
}
}

async function main() {
console.log("🚀 Starting PWR Chain Lite Stateful VIDA - Token Transfer System");

try {
// Initialize PWR Chain connection
const pwrjs = new PWRJS(RPC_ENDPOINT);

setupInitialBalances();

const subscription = pwrjs.subscribeToVidaTransactions(
BigInt(VIDA_ID),
BigInt(START_BLOCK),
processTransaction
);

console.log("\n⏳ Application running... Press Ctrl+C to stop");

process.on('SIGINT', () => {
console.log('\n🛑 Shutting down gracefully...');
subscription.stop();
console.log('✅ Subscription stopped');
process.exit(0);
});
} catch (error) {
console.error(`❌ Application error: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
}

Send Transactions

async function sendTransfer(wallet, receiver, amount) {
try {
const normalizedReceiver = receiver.toLowerCase();

const transferData = {
action: "send-tokens-v1",
receiver: normalizedReceiver,
amount: amount.toString()
};

// Convert to bytes
const dataString = JSON.stringify(transferData);
const data = new TextEncoder().encode(dataString);

// Send transaction to PWR Chain
const response = await wallet.sendPayableVidaData(BigInt(123), data, 0n);

if (response.success) {
console.log(`✅ Transaction sent successfully!`);
console.log(`Transaction hash: ${response.hash}`);
return true;
} else {
console.error(`❌ Failed to send transaction: ${response.message}`);
return false;
}
} catch (error) {
console.error(`Error sending transfer: ${error.message}`);
return false;
}
}

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.