DEV Community

Cover image for Pay for light (Part 2)
Peter Okwara
Peter Okwara

Posted on • Edited on

Pay for light (Part 2)

This tutorial continues from Part 1 which can be found here make sure you go through it before going through this.

Step 7: Api Setup

The code for the api can be found here. All is needed is to download the folder and run npm run install within the folder. In the following sections we will walk through the major files within the api.

Step 8: TextHelper

The TextHelper class holds helper functions to help store information on the Tangle.

/**
 * Helper functions for use with text.
 */
export class TextHelper {
}
Enter fullscreen mode Exit fullscreen mode

The encodeNonASCII function encodes non ASCII characters to escaped characters.

/**
     * Encode Non ASCII characters to escaped characters.
     * @param value The value to encode.
     * @returns The encoded value.
     */
    public static encodeNonASCII(value: string): string | undefined {
        return typeof (value) === "string" ?
            value.replace(/[\u007F-\uFFFF]/g, chr => `\\u${(`0000${chr.charCodeAt(0).toString(16)}`).substr(-4)}`)
            : undefined;
    }
Enter fullscreen mode Exit fullscreen mode

The decodeNonASCII function decodes escaped characters to non ASCII characters.

/**
     * Decode escaped Non ASCII characters.
     * @param value The value to decode.
     * @returns The decoded value.
     */
    public static decodeNonASCII(value: string): string | undefined {
        return typeof (value) === "string" ?
            value.replace(/\\u([\d\w]{4})/gi, (match, grp) => String.fromCharCode(parseInt(grp, 16))) :
            undefined;
    }
Enter fullscreen mode Exit fullscreen mode

Step 9: TrytesHelper

The necessary functions were imported to the TrytesHelper class. This will help convert objects to trytes and trytes to objects. In case we need to generate a hash, we also create a function to generate a given hash.

import { asciiToTrytes, TRYTE_ALPHABET, trytesToAscii } from "@iota/converter";
import * as crypto from "crypto";
import { TextHelper } from "./textHelper";
Enter fullscreen mode Exit fullscreen mode

The create class helps with all the trytes operations we will deal with.

/**
 * Helper functions for use with trytes.
 */
export class TrytesHelper {

}
Enter fullscreen mode Exit fullscreen mode

The toTrytes function converts an object to trytes.

/**
     * Convert an object to Trytes.
     * @param obj The obj to encode.
     * @returns The encoded trytes value.
     */
    public static toTrytes(obj: unknown): string {
        const json = JSON.stringify(obj);
        const encoded = TextHelper.encodeNonASCII(json);
        return encoded ? asciiToTrytes(encoded) : "";
    }

Enter fullscreen mode Exit fullscreen mode

The fromTrytes function converts trytes to a given object.

/**
     * Convert an object from Trytes.
     * @param trytes The trytes to decode.
     * @returns The decoded object.
     */
    public static fromTrytes<T>(trytes: string): T {
        if (typeof (trytes) !== "string") {
            throw new Error("fromTrytes can only convert strings");
        }

        // Trim trailing 9s
        const trimmed = trytes.replace(/\9+$/, "");

        if (trimmed.length === 0) {
            throw new Error("fromTrytes trytes does not contain any data");
        }

        const ascii = trytesToAscii(trimmed);
        const json = TextHelper.decodeNonASCII(ascii);
        return json ? JSON.parse(json) : undefined;
    }
Enter fullscreen mode Exit fullscreen mode

The generateHash function generates a random hash for the length provided.

  /**
     * Generate a random hash.
     * @param length The length of the hash.
     * @returns The hash.
     */
    public static generateHash(length: number = 81): string {
        let hash = "";

        while (hash.length < length) {
            const byte = crypto.randomBytes(1);
            if (byte[0] < 243) {
                hash += TRYTE_ALPHABET.charAt(byte[0] % 27);
            }
        }

        return hash;
    }
Enter fullscreen mode Exit fullscreen mode

Step 10: MQTT

The necessary files were imported using the code below. One is the Mqtt library. The other is the configuration files.

import mqtt from "mqtt";
import config from "../data/config.local.json";
Enter fullscreen mode Exit fullscreen mode

The MqttHelper class handles all the Mqtt functions.

/**
 * Class to handle Mqtt functions
 */
export class MqttHelper {
}
Enter fullscreen mode Exit fullscreen mode

Inside the class, variables were set which represent the configuration files for the Mqtt connection to Adafruit IO, which acts like our broker in the cloud. This was done using the code below.

/**
     * The mqtt client
     */
    protected _mqttClient: mqtt.MqttClient;

    /**
     * The mqtt host
     */
    protected readonly _host: string;

    /**
     * The username for the mqtt host/broker
     */
    protected readonly _username: string;

    /**
     * The password for the mqtt host/broker
     */
    protected readonly _password: string;

    /**
     * The feed/topic to subscribe to
     */
    protected readonly _feed: string;

    /**
     * The port to use when connecting to the mqtt host/broker
     */
    protected readonly _port: number;


Enter fullscreen mode Exit fullscreen mode

In our constructor, we set up our variables to the values we stored in our configuration files.

constructor() {
        this._mqttClient = null;
        this._host = `mqtts://${config.MQTT_CONFIG.ADAFRUIT_IO_URL}`;
        this._username = config.MQTT_CONFIG.ADAFRUIT_USERNAME;
        this._password = config.MQTT_CONFIG.ADAFRUIT_IO_KEY;
        this._feed = `${config.MQTT_CONFIG.ADAFRUIT_USERNAME}/f/${config.MQTT_CONFIG.ADAFRUIT_IO_FEEDNAME}`;
        this._port = config.MQTT_CONFIG.ADAFRUIT_IO_PORT;
    }
Enter fullscreen mode Exit fullscreen mode

We then created a function that will help us connect to our broker. In this case Adafruit IO .

 /**
   * Function to connect to a mqtt host/broker and subscribe to events coming from it
   */
public connect(): void {

}
Enter fullscreen mode Exit fullscreen mode

Connect to the cloud broker with username, password and port to use.

// Connect mqtt with credentials (in case of needed, otherwise we can omit 2nd param)
        this._mqttClient = mqtt.connect(
            this._host, { username: this._username, password: this._password, port: this._port });
Enter fullscreen mode Exit fullscreen mode

In case of error we want to log the error and break off the connection.

 // Mqtt error calback
        this._mqttClient.on("error", err => {
            console.log(err);
            this._mqttClient.end();
        });
Enter fullscreen mode Exit fullscreen mode

In case we successfully connect, we print out a message that our computer has successfully connected.

// Connection callback
        this._mqttClient.on("connect", () => {
            console.log(`mqtt client connected`);
        });
Enter fullscreen mode Exit fullscreen mode

We specify the feed we want to connect to and subscribe to it.

// mqtt subscriptions
        this._mqttClient.subscribe(this._feed);
Enter fullscreen mode Exit fullscreen mode

In case we get a message, we will just show the message we received.

  // When a message arrives, console.log it
        this._mqttClient.on("message", message => {
            console.log(message.toString());
        });
Enter fullscreen mode Exit fullscreen mode

On close, we will just close the Mqtt client.

this._mqttClient.on("close", () => {
            console.log(`mqtt client disconnected`);
        });
Enter fullscreen mode Exit fullscreen mode

We created another function for publishing messages to Mqtt.

 /**
     * Function  to send messages to the mqtt client/broker
     * @param message The message to be sent
     */
    public sendMessage(message: string | Buffer): void {
        this._mqttClient.publish(this._feed, message);
    }
Enter fullscreen mode Exit fullscreen mode

Step 11: MAM

We import the necessary libraries.

import { composeAPI } from "@iota/core";
import { createChannel, createMessage, mamAttach } from "@iota/mam.js";
import fs from "fs";
import config from "../data/config.local.json";
import { INodeConfiguration } from "../models/configuration/INodeConfiguration";
import { TrytesHelper } from "./trytesHelper";
Enter fullscreen mode Exit fullscreen mode

A class called MamHelper was created which will helps us with our connection to the Mam channel.

export class MamHelper {
}
Enter fullscreen mode Exit fullscreen mode

In it we import the configuration settings for the node we want to use.

/**
  * Node configuratin settins
  */
  private readonly _nodeConfig: INodeConfiguration;

  constructor() {
       this._nodeConfig = config.node;
   }
Enter fullscreen mode Exit fullscreen mode

We created a function to store messages on the mam channel.

/**
 * Function to store information on the mam channel
 * @param asciiMessage The message to be stored on the mam channel
 */
public async create(asciiMessage: object): Promise<void> {
}
Enter fullscreen mode Exit fullscreen mode

We then created a try-catch to catch errors if the whole process of storing the messages in the mam channel fails.

try {

} catch (error) {
            throw new Error(`Could not store the message on the mam channel ${error} `);
}
Enter fullscreen mode Exit fullscreen mode

We then try to load the channel state and parse it.

 let channelState;

            // Try and load the channel state from json file
            try {
                const currentState = fs.readFileSync("./channelState.json");
                if (currentState) {
                    channelState = JSON.parse(currentState.toString());
                }
            } catch (e) { }
Enter fullscreen mode Exit fullscreen mode

If we are unable to load the channel state, we create a new one.

// If we couldn't load the details then create a new channel.
if (!channelState) {

    // set up details for the channel
    const mode = "public";

    // create a new mam channel
    channelState = createChannel(TrytesHelper.generateHash(81), 2, mode);
}
Enter fullscreen mode Exit fullscreen mode

We create a Mam message using the channel state

// Create a MAM message using the channel state.
const mamMessage = createMessage(channelState, TrytesHelper.toTrytes(asciiMessage));
Enter fullscreen mode Exit fullscreen mode

Then display mam channel information

// Display the details for the MAM message.
console.log("Seed:", channelState.seed);
console.log("Address:", mamMessage.address);
console.log("Root:", mamMessage.root);
console.log("NextRoot:", channelState.nextRoot);
Enter fullscreen mode Exit fullscreen mode

Setup node configuration for the provider.

 const iota = composeAPI({
          provider: this._nodeConfig.provider
 });
Enter fullscreen mode Exit fullscreen mode

Then attach the message to the mam channel.

// Attach the message.
console.log("Attaching to tangle, please wait...");

await mamAttach(iota, mamMessage, this._nodeConfig.depth, this._nodeConfig.mwm);
Enter fullscreen mode Exit fullscreen mode

We provide a link where we can see all the information from the Mam channel.

console.log(`You can view the mam channel here https://utils.iota.org/mam/${mamMessage.root}/devnet`);

Enter fullscreen mode Exit fullscreen mode

We then store the state of the Mam channel json file

 // Store the channel state.
 try {
         fs.writeFileSync("./channelState.json", JSON.stringify(channelState, undefined, "\t"));
 } catch (e) {
         console.error(e);
 }
Enter fullscreen mode Exit fullscreen mode

Step 12: Iota-Pay

We import the necessary libraries and files for the project.

import bodyParser from "body-parser";
import express from "express";
import paymentModule from "iota-payment";
import scheduler from "node-schedule";
import { MamHelper } from "./utils/mamHelper";
import { MqttHelper } from "./utils/mqttHelper";
Enter fullscreen mode Exit fullscreen mode

We then set up an express server.

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
Enter fullscreen mode Exit fullscreen mode

We import the MqttHelper function. We then connect and first of all turn the relay off by sending an "OFF" message.

const mqttClient = new MqttHelper();
mqttClient.connect();
mqttClient.sendMessage("OFF");
Enter fullscreen mode Exit fullscreen mode

We then set up our options for the payment module. This involves enabling the api option, the dashboard option and the web socket option.

const options = {
    api: true,
    dashboard: true,
    websockets: true
};
Enter fullscreen mode Exit fullscreen mode

Set up an express server and run it. This provides us with two routes. The /iotapay route and the /iotapay/api route.

const server = paymentModule.createServer(app, options);

// Start server with iota-payment dashboard on '/iotapay' and api on '/iotapay/api'
server.listen(5000, () => {
    console.log(`Server started on http://localhost:5000 `);
    console.info(`Please visit http://localhost:5000/iotapay in your browser`);
});
Enter fullscreen mode Exit fullscreen mode

Set up a time variable that will return the current date and time.

const time = () => {
    return new Date();
};
Enter fullscreen mode Exit fullscreen mode

We then create a payment handler. The payment handler will be called once a payment is successful.

//Create an event handler which is called, when a payment was successful
const onPaymentSuccess = async payment => {
}
Enter fullscreen mode Exit fullscreen mode

We store all the payment information into variable. This also includes the payment event itself.

const transaction = {
        paid: payment.paid,
        type: payment.type,
        value: payment.value,
        address: payment.address,
        index: payment.index,
        id: payment.id,
        txInfo: {
            timestamp: payment.txInfo.timestamp,
            hash: payment.txInfo.hash,
            value: payment.txInfo.value,
            message: payment.txInfo.message
        },
        event: [`User has paid ${payment.txInfo.value} tokens for lights`]
    };
Enter fullscreen mode Exit fullscreen mode

Set up the mam channel and create a message.

const mamHelper = new MamHelper();

await mamHelper.create(transaction);
Enter fullscreen mode Exit fullscreen mode

We then wait for 3 seconds before adding the time we turn the lights on and push the whole transaction to the mam channel.

setTimeout(async () => {
        // send the event that the lights are now on
        transaction.event.push(`Lights are on ${time()}`);
        await mamHelper.create(transaction);

        // tslint:disable-next-line: align
    }, 3000);
Enter fullscreen mode Exit fullscreen mode

Let's make the amount of time (in minutes) the lights will be on be the amount of IOTA tokens multiplied by 60000.

 const seconds: number = payment.txInfo.value * 60000;
Enter fullscreen mode Exit fullscreen mode

Let's set up the start time and the end time, the lights will be on.

 const startTime = new Date(Date.now());
 const endTime = new Date(startTime.getTime() + seconds);
Enter fullscreen mode Exit fullscreen mode

To keep the lights on, we send a Mqtt message every 10 seconds for the amount of time we have set.

await scheduler.scheduleJob({ start: startTime, end: endTime, rule: `*/10 * * * * *` }, () => {
        mqttClient.sendMessage("ON");
        console.log("😊");
   });
Enter fullscreen mode Exit fullscreen mode

Once the time is over, we set the end time for which which the off message will be sent.

 const newEndTime = new Date(endTime.getTime() + 30000);
Enter fullscreen mode Exit fullscreen mode

We send an OFF mesage for another 10 seconds for a period of 30 seconds to ensure the relay is off.

await scheduler.scheduleJob({ start: endTime, end: newEndTime, rule: `*/10 * * * * *` }, () => {
        mqttClient.sendMessage("OFF");
        console.log("😐");
    });
Enter fullscreen mode Exit fullscreen mode

We then immediately send that the lights are off to the mam channel.

await scheduler.scheduleJob({ start: endTime, end: newEndTime, rule: `*/30 * * * * *` }, async () => {

        // send the event that the lights are now off
        transaction.event.push(`Lights are off ${time()}`);
        await mamHelper.create(transaction);
    });
Enter fullscreen mode Exit fullscreen mode

At the end of this, we now call the function onPaymentSuccess that we just created.

paymentModule.onEvent("paymentSuccess", onPaymentSuccess);
Enter fullscreen mode Exit fullscreen mode

Step 13: Configuration

There are two configurations we need to set up. One is for the MQTT and the IOTA Node, to store transaction information using MAM. The other one is a seed for generating an address for payment and connecting to the IOTA Nodes only to do payments.

Same with the configuration on step 4, we would only need to change the adafruit io username and adafruit io key. This configuration is found here. The file should be saved as config.local.json.

{
    "MQTT_CONFIG": {
        "ADAFRUIT_IO_URL": "io.adafruit.com",
        "ADAFRUIT_USERNAME": "",
        "ADAFRUIT_IO_KEY": "",
        "ADAFRUIT_IO_FEEDNAME": "pay-for-light.bulb-relay",
        "ADAFRUIT_IO_PORT": 8883
    },
    "node": {
        "provider": "https://nodes.devnet.iota.org:443",
        "depth": 3,
        "mwm": 9
    }
}
Enter fullscreen mode Exit fullscreen mode

Create another file called a .env file in the api folder. Add the following configuration.

seed=
iotaNodes=["https://nodes.comnet.thetangle.org:443"]
Enter fullscreen mode Exit fullscreen mode

Generate a seed by heading over to https://docs.iota.org/docs/getting-started/0.1/tutorials/create-a-seed and running one of the commands to generate a seed. After that you can paste the seed as it is in the seed= parameter.

Step 14: Testing

We can now run the api server by running the command below.

cd api && npm run start

This boots up the api server as shown below.

Alt Text

Note: Running npm run start-dev puts its in a loop. Don't use it.

Once the api server has boot up, you can now go to the storefront to get your IOTA address. This is highlighted in red in the above image as.

http://localhost:5000/iotapay/#/

Once you go to the url on your machine or server, the page looks like this.

Alt Text

Clicking on Show QR Code displays the QR code. The QR code can be scanned to get the IOTA address to make payments to.

Alt Text

Clicking on Copy address copies the IOTA address to the clipboard. Once we have the IOTA address, we can now make payments to the given address.

Go to the IOTA Comnet *Fauchet*site here. The site provides free tokens for the community testent. The tokens will be used to simulate a payment process.

Alt Text

Enter the address to send tokens on the IOTA Comnet address to and specify the amount.

Alt Text

For now we will send 3 IOTA tokens. Once we enter the amount, we can now hit send. One can also add an optional message to accompany the payment.

Alt Text

Once the payment is successfull, a message will pop up, Transaction sent: with a link to the transaction on the tangle. This also shows up on the console as Payment Successful! Address XXXXX

Alt Text

Going to the link shown above (You can view the mam channel here), we can see the in the mam explorer, the payment information and the events. This is shown in the photos below.

Alt Text

Alt Text

Alt Text

The event are:

  • User has paid 3 tokens for lights
  • Lights are on
  • Lights are off

The lights will then turn on and then turn off after 3 minutes.

Limitations

Limitations of the project are:

  • Security has not been implemented in the MAM channel for this project. At the moment, anyone can access the information. Security can be enhanced by using private mode or restricted mode.
  • There is no mechanism for handling multiple payments at the same time. One can only do one payment at a time for it to work correctly. Probably having a deposit account and checking the amount would help. Or queue the payments
  • Maximum amount of time the lights can be turned on is 59 minutes. This limitation is easy to fix, it just involves modifying the *npm *module, node schedule to allow for more date and time.
  • One cannot get a specific transaction. It shows the compete list of transactions from which one has to manually search for a specific transaction.

References

Some of the references for this code is found here:

Top comments (0)