Skip to main content

Using Conduit Nodes

Conduit nodes play a crucial role in enabling decentralized applications (dApps) and virtual machines (VMs) on the PWR Chain to interact with the base layer and with each other. These nodes act as intermediaries, translating transaction requests from dApps and VMs into actual transactions on the PWR Chain.

In this guide, we'll walk you through the process of setting up your application to communicate with conduit nodes and send transaction requests.

Prerequisites

Before starting, make sure you have the following:

  • A development environment set up for your preferred programming language.
  • PWR SDK installed and configured in your project.
  • You've finished reading about Conduits Nodes.
  • You have finished reading the SDK guides.

Step 1: Set Up the Project

Create a new project in your preferred programming language, and add the necessary dependencies, including the PWR SDK, to your project's configuration file or build tool.

mkdir conduits && cd conduits
npm init --yes
npm install @pwrjs/core dotenv readline express

NOTE: Rust developers will face some issues with file formatting compared to other languages, you can check out this project on Github.

Step 2: ENV setup to load the wallet

Create a .env file in your project folder and add your wallet's PRIVATE_KEY in the file.

PRIVATE_KEY="ADD_YOUR_PRIVATE_KEY_HERE"

Step 3: Send Messages

To send a message, we'll use the method provided by the PWR SDK to send a transaction with the message data.

Create a send_message file in your project and add the following code:

const { PWRWallet } = require("@pwrjs/core");
require('dotenv').config();

// Setting up your wallet in the SDK
const privateKey = process.env.PRIVATE_KEY;
const wallet = new PWRWallet(privateKey);

async function sendMessage() {
const obj = { message: "please send me pwr" };
const data = Buffer.from(JSON.stringify(obj), 'utf8'); // Serialize to JSON bytes
const vmId = 123;

// Sending the VM data transaction
const res = await wallet.sendVMDataTxn(vmId, data);
console.log(res.transactionHash);
}
sendMessage();

Step 4: Transactions Awaiting Approval

The Transactions class manages the list of transactions awaiting approval:

Once the data is fetched from PWR Chain the transactions associated with sending 100 pwr to the user will be stored in the transactionsAwaitingApproval array.

Create a transaction file in your project and add the following code:

class Transactions {
static transactionsAwaitingApproval = [];

static add(txn) {
this.transactionsAwaitingApproval.push(txn);
}

static remove(txn) {
this.transactionsAwaitingApproval = this.transactionsAwaitingApproval.filter(
tx => JSON.stringify(tx) !== JSON.stringify(txn)
);
}

static getPendingTransactions() {
return [...this.transactionsAwaitingApproval];
}
}
module.exports = { Transactions };

Step 5: Fetch Messages

To fetch messages from the PWR Chain, we'll use the method provided by the PWR SDK to retrieve transactions within a range of blocks.

Create a sync_messages file in your project and add the following code:

const { PWRJS, PWRWallet, TransactionBuilder } = require("@pwrjs/core");
const { Transactions } = require("./transaction.js");
require('dotenv').config();

// Setting up your wallet in the SDK
const privateKey = process.env.PRIVATE_KEY;
const wallet = new PWRWallet(privateKey);
// Setting up the rpc api
const rpc = new PWRJS("https://pwrrpc.pwrlabs.io/");

async function sync() {
let startingBlock = 876040; // Adjust starting block as needed
const vmId = 123;

// Defining an asynchronous loop function that fetches and processes new transactions
const loop = async () => {
// Fetching the latest block number from the blockchain via the RPC API
const latestBlock = await rpc.getLatestBlockNumber();
// Defining the effective block range for the next batch of transactions, limiting to 1000 blocks at a time
let effectiveLatestBlock = latestBlock > startingBlock + 1000 ? startingBlock + 1000 : latestBlock;

// Checking if there are new blocks to process
if (effectiveLatestBlock > startingBlock) {
// Fetching VM data transactions between the starting block and the effective latest block for a given VM ID
const txns = await rpc.getVMDataTransactions(startingBlock, effectiveLatestBlock, vmId);
// Looping through the transactions fetched from the blockchain
for (let txn of txns) {
const sender = txn.sender;
const dataHex = txn.data;
let nonce = await wallet.getNonce();
// Converting the hex data to a buffer and then to a UTF-8 string
const data = Buffer.from(dataHex.substring(2), 'hex');
const object = JSON.parse(data.toString('utf8'));

// Iterating over each key in the object to check for specific conditions
Object.keys(object).forEach(async (key) => {
if (key.toLowerCase() === "message" && object[key].toLowerCase() === "please send me pwr") {
// Building a transfer transaction to send PWR tokens
const transferTxn = TransactionBuilder.getTransferPwrTransaction(
rpc.getChainId(), nonce, 100, sender
);
// Adding the transaction to the Transactions class
Transactions.add(transferTxn)
// Logging the message and the sender to the console
console.log(`\nMessage from ${sender}: ${object[key]}`);
}
});
}
// Updating the starting block number for the next loop iteration
startingBlock = effectiveLatestBlock + 1;
}
setTimeout(loop, 1000); // Wait 1 second before the next loop
}
loop();
}
module.exports = { sync };

Step 6: Create Your Conduits Transactions API

Conduit nodes run instances of the virtual machine on their servers and make repetitive API calls to check if the application has any new transactions it wants submitted to the PWR Chain.

Create a app file in your project and add the following code:

const express = require("express");
const { Transactions } = require("./transaction.js");
const { sync } = require("./sync_messages.js");

// Initialize the Express application by creating an app object
const app = express();

// Add sync to fetch messages and add it to the pending txs
sync();

// Define an HTTP GET route at '/pendingVmTransactions'
// When accessed, this route will return the list of pending transactions
app.get('/pendingVmTransactions', (req, res) => {
// Set the response header to ensure the response is sent as JSON data
res.header("Content-Type", "application/json");
// Retrieve the list of pending transactions using the getPendingTransactions method
const pendingTransactions = Transactions.getPendingTransactions();
// Map through each transaction in the pendingTransactions array
const array = pendingTransactions.map(txn => {
// Convert each transaction (assumed to be a Buffer or Uint8Array) to a hexadecimal string
const hexString = '0x' + Array.from(txn, byte => byte.toString(16).padStart(2, '0')).join(''); // Convert Buffer to hex string
// Remove the transaction from the pending transactions list after processing
Transactions.remove(txn);
// Return the hexadecimal representation of the transaction
return hexString;
});
// Send the resulting array of hex strings as a JSON response
res.json(array);
})
// Set the port number for the server to listen on
const port = 8000;
// Start the Express server and listen for connections on the specified port
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});

Conduit nodes will call the /pendingVmTransactions endpoint and retrieve the application's transactions as hex strings in a JSON array.

Example API request::

curl -X GET http://localhost:8000/pendingVmTransactions/ -H "Content-Type: application/json"

Example API response::

[
"0x1234567890abcdef...",
"0x0987654321fedcba..."
]

In the code above, we remove the transactions once they have been retrieved by the conduit node, but depending on your application's needs and error handling, you might want to implement a different approach to transaction handling, such as marking transactions as processed or implementing a retry mechanism.

Step 7: Set Conduit Nodes

To set the conduit nodes for your application, use the Set Conduits method provided by the PWR SDK.

import { PWRWallet } from "@pwrjs/core";
import dotenv from 'dotenv';
dotenv.config();

// Setting up your wallet in the SDK
const privateKey = process.env.PRIVATE_KEY;
const wallet = new PWRWallet(privateKey);

async function conduits() {
const conduits = [
Buffer.from("conduit_node_address", "hex"),
];
const vmId = "your_vm_id";

const res = await wallet.setConduits(vmId, conduits);
console.log(res.transactionHash);
}
conduits();

Replace your_vm_id with the claimed VM ID from Step 1 and provide the addresses of the conduit nodes you have agreed with.

When selecting conduit nodes, consider the following factors:

  • Reliability: Choose conduit nodes that have a proven track record of uptime and reliability. They should be able to consistently process transactions and maintain synchronization with the PWR Chain.
  • Security: Ensure that the conduit nodes follow best practices for security, such as using secure communication channels, implementing access controls, and regularly updating their software and infrastructure.
  • Incentives: Agree on a fair decentralized incentive model with the conduit nodes to motivate them to run your application and process transactions efficiently. This can include rewards, revenue sharing, or other benefits.

After the transaction is confirmed, the specified conduit nodes will be set for your application. These conduit nodes will now be responsible for translating your application's transaction requests into actual transactions on the PWR Chain.

Once you have established conduit nodes, you cannot remove or modify them without their consent. Therefore, it is advisable to integrate the rules for adding and approving conduit nodes directly into your application. By doing so, the conduit nodes will automatically enforce these rules.

Step 8: Distribute Your Application To The Conduit Nodes

Once all the above steps are accomplished, you can share your application with the conduit nodes, and they will become its validators and message relayers. It's important to share the same application with all conduits to ensure a unified set of rules across them all.

When distributing your application to conduit nodes, consider the following best practices:

Versioning: Use a clear versioning scheme for your application releases. This helps conduit nodes understand which version they are running and makes it easier to manage updates and bug fixes.

Update Mechanism: Establish a clear mechanism for updating your application when new versions are released. This can include automated update scripts or notifications to conduit nodes about new releases.