Introduction to Execution Machine (EXM) - Permanent Serverless Functions

Nader Dabit - Oct 26 '22 - - Dev Community

Introduction to Execution Machine (EXM)

The code for this tutorial is located here

EXM is a language-agnostic serverless environment powered by Arweave, and enables developers to create permanent, serverless functions on the blockchain.

EXM eliminates the need to understand smart contracts, transactions, or blocks to leverage blockchain technology.

EXM / Arweave provide something that most traditional data stores do not - permanence. Once the data / transactions are stored, they can be retrieved and consumed forever (or as long as the network exists).

Some interesting use cases might be registries, games, CMSs, permanent data protocols, general data processing, and possibly even building a permanent NoSQL database.

Under the hood, EXM utilizes SmartWeave

Lazy Evaluation

The Smartweave (and therefore EXM) execution layer is based on what's known as "Lazy Evaluation".

In most blockchains, the computation and current state is executed by the blockchain nodes. With lazy evaluation, the burden of execution is shifted to either the client or a middle layer (like EXM) that implements a processor and conveyor that essentially re-evaluates and caches the latest state of the contract so that the client does not need to process all previous transactions in order to calculate the current state.

CLI vs SDK

You can interact with EXV either via their CLI or their SDK. In this tutorial we'll be interacting with the platform using the JavaScript SDK.

Hello World tutorial

Let's explore how to build, deploy, and test an EXM app.

In this example we'll create a basic CMS that will allow a user to create, update, and delete posts. This example will enable us to implement a basic CRUD app which is the basis for most real-world applications.

Prerequisites

To be successful with this tutorial you must have:

  1. An EXM API key. You can get one here
  2. Node.js installed on your machine. I recommend installing Node.js either via NVM or FNM
  3. Mocha installed globally (if you want to run tests)

Getting started

To get started, we'll create a new Node.js project in an empty directory:

npm init --y
Enter fullscreen mode Exit fullscreen mode

Next, we'll install the dependencies we'll need for our app:

npm install @execution-machine/sdk uuid chai
Enter fullscreen mode Exit fullscreen mode

Finally, update package.json to add this configuration to enable ES Modules:

"type": "module",
Enter fullscreen mode Exit fullscreen mode

Basic setup

For a basic EXM app you need these basic components:

  1. Initial state for the program
  2. A handler function to manage state updates. This function is the entry point for your EXM function. For a write operation to be considered valid as well as able to change the state of the function, it needs to return an object with state property in it. (more on this later)
  3. A script to deploy the program
  4. Scripts to read and update the state of the program

Configuring EXM

To interact with the platform we'll need some basic configuration that we can set up and reuse.

First, make sure you've created an EXM API key here.

Next, create and set an environment variable either in your local config file, or from your terminal:

export EXM_PK=<your-api-key>
Enter fullscreen mode Exit fullscreen mode

Now, create a new file named exm.js and add the following code:

/* exm.js */
import { Exm } from '@execution-machine/sdk'

const APIKEY = process.env.EXM_PK
export const exmInstance = new Exm({ token: APIKEY })
Enter fullscreen mode Exit fullscreen mode

Now we'll be able to interact with EXM by importing exmInstance.

Initial State

As mentioned previously, the app we'll be creating will allow users to create, edit, and delete posts. A post will be an object with the following fields:

type Post {
  id: string
  title: string
  content: string
  author: string
}
Enter fullscreen mode Exit fullscreen mode

To hold the posts, we can create some data structure to hold a list of posts. I will be using an object but you could also use an array.

To set up this initial state, create a new file named state.js and add the following code:

/* state.js */
export const state = {
  "posts": {}
}
Enter fullscreen mode Exit fullscreen mode

Handler function

Now that we have the initial state defined we'll create the handler function.

Create a new file named handler.js and add the following code:

/* handler.js */
export async function handle(state, action) {
  const { input } = action
  if (input.type === 'createPost' || input.type === 'updatePost') {
    state.posts[input.post.id] = input.post
  }
  if (input.type === 'deletePost') {
    delete state.posts[input.postId]
  }
  return { state }
}
Enter fullscreen mode Exit fullscreen mode

This function will accept three different types of actions:

createPost - Creates a new post
updatePost - Updates an existing post with new data
deletePost - Deletes a post

If the input type does not match any of these, then the state is just returned without any update.

Deploy script

Now that we have the state and the handler function defined, we can create the deploy script.

This script will deploy our function to EXM and make it available for us to start using.

Create a new file named deploy.js and add the following code:

/* deploy.js */
import { ContractType } from '@execution-machine/sdk'
import fs from 'fs'
import { exmInstance } from './exm.js'
import { state } from './state.js'

const contractSource = fs.readFileSync('handler.js')
const data = await exmInstance.functions.deploy(contractSource, state, ContractType.JS)

console.log({ data })

/* after the contract is deployed, write the function id to a local file */
fs.writeFileSync('./functionId.js', `export const functionId = "${data.id}"`)
Enter fullscreen mode Exit fullscreen mode

Creating a new post

Next let's look at how to make a state update.

The first thing we'll want to try to do is to create a new post. Create a new file named createPost.js and add the following code:

/* createPost.js */
import { exmInstance } from './exm.js'
import { functionId } from './functionId.js'
import { v4 as uuid } from 'uuid'

const id = uuid()

/* the inputs array defines the data that will be available in the handler function as action.input */
const inputs = [{
  type: 'createPost',
  post: {
    id,
    title: "Hello world",
    content: "My first post",
    author: "Nader Dabit"
  }
}]

const data = await exmInstance.functions.write(functionId, inputs)
console.log({ data })
Enter fullscreen mode Exit fullscreen mode

As commented in the code above, the most important thing to recognize is the structure of the array of inputs, and how they correlate to the arguments received in the handler function.

In createPost.js, the type will determine the type of state update that is made in the handler, and the post is the data used in the state update.

Reading the current state

Now that we have a way to deploy the function and create a post, let's look at how to read the state.

To do so, create a file named read.js and add the following code:

/* read.js */
import { exmInstance } from './exm.js'
import { functionId } from './functionId.js'

const data = await exmInstance.functions.read(functionId)
console.log("data: ", JSON.stringify(data))
Enter fullscreen mode Exit fullscreen mode

You can also read the state of any function ID by calling or visiting https://api.exm.dev/read/<function-id>

Trying it out

Now that everything is set up, let's give it a spin!

From your terminal, run the deploy script:

node deploy.js
Enter fullscreen mode Exit fullscreen mode

Once the function has been deployed, you should see a new file written to the project named functionId.js.

Next, read the current state of the program:

node read.js
Enter fullscreen mode Exit fullscreen mode

The return value of the state should be an empty posts object.

Now, create a new post:

node createPost.js
Enter fullscreen mode Exit fullscreen mode

Then read the state of the app again:

node read.js
Enter fullscreen mode Exit fullscreen mode

The return value should now include the new post that was just created.

Reminder that you can also read the state of any function ID by calling or visiting https://api.exm.dev/read/<function-id>

Updating and deleting posts

Let's now create the scripts for updating and deleting posts.

For deleting a post, create a file named deletePost.js and add the following code:

/* deletePost.js */
import { exmInstance } from './exm.js'
import { functionId } from './functionId.js'

const inputs = [{
  type: 'deletePost',
  postId: process.argv[2]
}];

await exmInstance.functions.write(functionId, inputs)
Enter fullscreen mode Exit fullscreen mode

This script takes a post ID as input from the user running the script, and uses it to call the deletePost input, passing in the postId as an argument.

To update a post, create a new file named updatePost.js and add the following code:

/* updatePost.js */
import { exmInstance } from './exm.js'
import { functionId } from './functionId.js'

const inputs = [{
  type: 'updatePost',
  post: {
    id: process.argv[2],
    title: "Hello world V2",
    content: "My updated post!",
    author: "Nader Dabit"
  }
}]

const data = await exmInstance.functions.write(functionId, inputs)

console.log({ data })
Enter fullscreen mode Exit fullscreen mode

Now, you should be able to update and delete posts by running these two scripts, passing in the post ID:

node deletePost.js <post-id>
Enter fullscreen mode Exit fullscreen mode

Testing

EXM also comes built in with testing functions in their SDK to facilitate testing in a way that can be readable & efficient.

Let's create a test script that will check that all of our action types work as we expect.

Create a new file named test.js and add the following code:

import fs from 'fs'
import { TestFunction, createWrite, FunctionType } from "@execution-machine/sdk"
import { state } from './state.js'
import { v4 as uuid } from 'uuid'
import { expect } from 'chai'

const id = uuid()
const functionSource = fs.readFileSync('./handler.js')

const createPost = {
  type: 'createPost',
  post: {
    id,
    title: "Hello world",
    content: "My first post",
    author: "Nader Dabit"
  }
}

const updatePost = {
  type: 'updatePost',
  post: {
    id,
    title: "Hello world V2",
    content: "My updated post!",
    author: "Nader Dabit"
  }
}

const deletePost = {
  type: 'deletePost',
  postId: id
}

describe("Testing EXM", function () {
  it("Should create a post", async function () {
    const testAttempt = await TestFunction({
      functionSource,
      functionType: FunctionType.JAVASCRIPT,
      functionInitState: state,
      writes: [createWrite(createPost)]
    })
    const value = testAttempt.state.posts[id]

    expect(value.id).to.equal(id)
    expect(value.title).to.equal("Hello world")
  })

  it("Should update a post", async function () {
    const testAttempt = await TestFunction({
      functionSource,
      functionType: FunctionType.JAVASCRIPT,
      functionInitState: state,
      writes: [createWrite(createPost), createWrite(updatePost)]
    })
    const value = testAttempt.state.posts[id]
    expect(value.title).to.equal("Hello world V2")
  })

  it("Should delete a post", async function () {
    const testAttempt = await TestFunction({
      functionSource,
      functionType: FunctionType.JAVASCRIPT,
      functionInitState: state,
      writes: [createWrite(createPost), createWrite(deletePost)]
    })
    const value = testAttempt.state.posts
    const length = Object.keys(value).length
    expect(length).to.equal(0)
  })
})
Enter fullscreen mode Exit fullscreen mode

To test, run the following command:

mocha test.js
Enter fullscreen mode Exit fullscreen mode

Next steps

To learn more about EXM, check out the EXM Discord.

To learn more about the Arweave Developer Ecosystem, check out learn.arweave.dev.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .