Fluence Network

The Fluence Labs Developer Hub

Welcome to the Fluence Labs developer hub. You'll find comprehensive guides and documentation to help you start working with Fluence Labs as quickly as possible, as well as support if you get stuck. Let's jump right in!

Should you have any questions, feel free to join our Discord or Telegram!

Get Started    API Reference

Fluid – decentralized twitter-like feed built on Fluence

What?

This workshop contains step-by-step code samples that guide you through building decentralized feed application.

It would be simple backend powered by a SQLite ported to WebAssembly, and it is intended to be ran in a decentralized fashion on Fluence platform. All the development, experimenting and debugging will happen locally though!

You can choose either Rust or AssemblyScript programming languages to go through this workshop.

Prerequisites

You need to install the a few tools for the workshop:

Docker (required)

To run your application locally

NPM

If you would use AssemblyScript

macOS: brew install npm
Ubuntu: apt-get install npm
Download: here

Rust

If you would use Rust

./install.sh in repository root

jq and base64

Required to parse output from your application

macOS: brew install jq base64
Ubuntu: apt-get install jq base64

Docker on macOS

macOS users, kindly note that brew install docker is not enough, you will also need a Docker daemon. To install: download, open, copy Docker.app to Applications and run Docker.app from the Applications folder.

Where's the code?!

The code can be found in fluencelabs/fluid repo on GitHub. Please refer to the directory corresponding to your language of choice:

Please clone the repository, and go to step0-framework directory to start with workshop!

Step 0 – Framework

Let's take a look at the most basic Fluence application, and try to run it.

It consists of the following files:

-- src/lib.rs           // Defines application entrypoint and compilation units
-- Cargo.toml           // Defines how to compile app, what dependencies to use, etc
-- run.sh               // Script that builds application and runs it
-- assembly/index.ts    // Describes API that Fluence will use to call the app
-- assembly/main.ts     // Contains app's entrypoint
-- package.json         // Descibes dependencies used by application

Let's run it!

# In step0-framework directory
$ ./run.sh
Building...
    Finished release [optimized] target(s) in 0.53s




Running...
docker run -d --name frun --rm -v "$(pwd)/wasm:/code" -p 30000:30000 fluencelabs/frun:latest

Sending request: username
curl -s 'http://localhost:30000/apps/1/tx' --data $'sessionId/0\n'username --compressed

Hello, username!

Stopping...
# In step0-framework directory
$ ./run.sh
Building...
audited 69 packages in 1.023s
found 0 vulnerabilities
> fluid@1.0.0 flbuild /Users/folex/Development/fluencelabs/fluid/backend-assemblyscript/step0-framework
> asc assembly/index.ts -b "build/$npm_package_name.wasm" --validate --optimize --use abort='' --runtime stub

Running...
docker run -d --name frun --rm -v "$(pwd)/wasm:/code" -p 30000:30000 fluencelabs/frun:latest

Sending request: username
curl -s 'http://localhost:30000/apps/1/tx' --data $'sessionId/0\n'username --compressed

Hello, username!

Stopping...

You can see application is built, and then running inside docker container on port 30000. After it's successfully started, the request is sent via curl.

It is also possible to send request via javascript, but let's keep that for later.

Step 1 – JSON API

Working code for this step could be found in step1-json-api directory

So, what were we building? Definitely not another "hello world" application!

Right, it was a decentralized feed application, where people can share their thoughts, jokes and be nice to each other.

And to share their niceness people need... API! A JSON one would do, I think.

The API could be something like this:

// First we need to be able to post messages to our app

// Request could look something like this
{
    "action": "Post",
    "message": "I'm nice, you're nice, it's nice!",
    "username": "random_joe"
}
// And the successfull response could tell us total number of posts so far
{
    "count": 256
}

// Okay, now we need to fetch posts, right?
{
    "action": "Fetch",
    "username": "random_joe" // <-- Optional field to filter by author
}
// Response
{
    "posts": [
        {
            "message": "I'm nice, you're nice, it's nice!",
            "username": "random_joe"
        }
    ]
}

To implement that API, let's write some code!

// Create the following files
-- src/api.rs       // Where we would put our API
-- src/model.rs     // Where we would describe what we want from database
-- src/errors.rs    // To describe our errors

// Add JSON library to dependencies in Cargo.toml
serde = { version = "=1.0.88", features = ["derive"] }
serde_json = { version = "=1.0.38", features = ["raw_value"] }
// Create the following files
-- assembly/request.ts     // Where we would put requests API
-- assembly/response.ts    // Where we would put responses API


// Add JSON library to dependencies in package.json
"devDependencies": {
    "assemblyscript-json": "github:fluencelabs/assemblyscript-json#update-as1",
}

Now, let's define API parsing in code. A small hint:

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;

#[derive(Deserialize)]
#[serde(tag = "action")]
pub enum Request {
    ... // remember, Fetch and Post?
}

#[derive(Serialize, Debug)]
#[serde(untagged)]
pub enum Response {
    ... // Fetch, Post, and... what if everything goes wrong?
}

// JSON => Request
pub fn parse(s: String) -> AppResult<Request> {}
// Response => JSON
pub fn serialize(response: &Response) -> String {}
// This is an utility file to make error handling easier, 
// and save some typing

use std::fmt;

#[derive(Debug)]
pub struct Error(String);

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

pub fn err_msg(s: &str) -> Box<Error> {
    Error(s.to_string()).into()
}

pub type AppResult<T> = ::std::result::Result<T, Box<Error>>;
// Import json decoder
import {JSONDecoder, JSONHandler} from "../node_modules/assemblyscript-json/assembly/decoder";

// Define requests API
export enum Action { ... }
export abstract class Request { public action: Action = ...; }
export class PostRequest extends Request { ... }
export class FetchRequest extends Request { ... }

// JSON => Request
export function decode(input: string): Request {
    let jsonHandler = new RequestJSONEventsHandler();
    let decoder = new JSONDecoder<RequestJSONEventsHandler>(jsonHandler);

    let bytes = string2Bytes(input);
    decoder.deserialize(bytes);
  
    // Handle each action, instantiate corresponding Request
}

// Utilities
function string2Bytes(str: string): Uint8Array {
    return Uint8Array.wrap(String.UTF8.encode(str));
}

// Decoder for Request
class RequestJSONEventsHandler extends JSONHandler {
    // Field name and value
    setString(name: string, value: string): void { ... }
}
// Define Response API and how to serialize each response
export abstract class Response { serialize(): string { ... } }

export class PostResponse extends Response {
    serialize(): string {
        let encoder = new JSONEncoder();
        encoder.pushObject(null);
        encoder.setString("action", "Post");
        ...
    }
}

Working code for this step could be found in step1-json-api directory

Next step is to implement a little bit of business logic – we want to handle requests, store and read posts from database, and return back to the user.

For this step, just make a prototype of database – you'll learn how to implement it later. This will allow to implement our API and business logic in full.

// TODO: Create empty methods for the following tasks
//
// Create scheme
// Add post
// Get all posts
// Get posts filtered by username
// Get posts count – to return it on Post request
//
// You can simply log actions to see it works, and return empty results
use log;

pub fn create_scheme() -> AppResult<()> { Ok(log::info!("create scheme")) }
pub fn add_post(message: String, username: String) -> AppResult<()> { ... }
...
// TODO: Create empty methods for the following tasks
//
// Create scheme
// Add post
// Get all posts
// Get posts filtered by username
// Get posts count – to return it on Post request
//
// You can simply log actions to see it works, and return empty results
import {log} from "../node_modules/assemblyscript-sdk/assembly/logger";

export function createScheme(): void { log("create scheme"); }
export function addMessage(message: string, username: string): void { ... }
...

Add missing functions, and...

Run it!

After you've described model prototype, and finished with implementing API, you can run your example with ./run.sh in your starting directory or in step1-json-api directory.

Step 2 – Using SQLite

For this step let's put our business logic away for a while, so we can get real close with multi-module SQLite setup.

What I mean by multi-module setup is just several .wasm modules (even written in different languages) working together as a single program. This is kind of similar to how dynamic libraries work.

It may sound complex, but it is far easier in practice, so let's learn by doing!

Where to go?

I recommend you to go to step2-database-only directory where you don't have JSON parsing logic in your way, so you can start experimenting with SQLite right away.

But if you'd like to stay where you are, you can bring database into you scope like this:

// Copy the following files from step2-database-only:
-- ffi.rs        // Describes communication betwee fluid and sqlite wasm modules
-- database.rs   // Implements query function: copy data between modules, etc
-- run.sh        // Downloads sqlite.wasm module and runs the app

// Add modules to you lib.rs:
pub mod database;
pub mod ffi;

// And use it in your invocation_handler like this:
// Create table for messages storage
database::query("CREATE TABLE messages(message text, username text)".to_string())
    .expect("error on CREATE TABLE");
// Copy the following files from step2-database-only directory
-- run.sh // Downloads sqlite.wasm module and runs the app

// Add dependency to package.json:
"devDependencies": {
	"db-connector": "github:fluencelabs/db-connector"
}

// Import in main.ts:
import {query} from "../node_modules/db-connector/assembly/sqlite"

// And use it like this:
// Create table for messages storage
query(`CREATE TABLE messages(message text, username text)`);

Feel free to do all nasty things with it, you can even take input from invocation handler, and send it directly to SQLite as a query. This way, you'd have HTTP SQL REPL! So many big letters, gotta be cool :)

Now that we have some queries going on in our application, let's run it like always – through run.sh

Remember to copy run.sh from step2-database-only directory

Great! Now, let's finish our application by tying database with business logic!

Step 3 – Finishing the app

Working code for this step could be found in step3-finished-app directory

So goal of this step is to implement methods in the database model file. By now, you should have something like this:

use log;
use crate::errors::AppResult;

pub fn create_scheme() -> AppResult<()> {
    Ok(log::info!("creating scheme"))
}

pub fn add_post(message: String, username: String) -> AppResult<()> {
    Ok(log::info!("add post {} {}", message, username))
}

pub fn get_all_posts() -> AppResult<String> {
    log::info!("get all posts");
    Ok("[]".to_string())
}

pub fn get_posts_by_username(username: String) -> AppResult<String> {
    log::info!("get all posts by username {}", username);
    Ok("[]".to_string())
}

pub fn get_posts_count() -> AppResult<i32> {
    log::info!("get posts count");
    Ok(0)
}
import {log} from "../node_modules/assemblyscript-sdk/assembly/logger";

export function createScheme(): void {
    log("create scheme");
}
export function addMessage(message: string, username: string): void {
    log("add message");
}
export function getMessages(username: string | null): string {
    log("get messages");
    return "[]";
}
export function getPostsCount(): u32 {
    log("get posts count");
    return 0;
}

Use the knowledge from the previous step – connect with SQLite in your code, and execute real queries in these methods. A little hint:

// To be able to use i32::from_str ;)
use std::str::FromStr;

use crate::api::AppResult;
use crate::database;
use crate::errors::err_msg;

pub fn create_scheme() -> AppResult<()> {
    // Executing query
    database::query("CREATE TABLE messages(message text, username text)".to_string())
        // Add useful information to the error message
        .map_err(|e| err_msg(&format!("Error creating table messages: {}", e)))
        // Cast AppResult<String> to AppResult<()>, to mark we don't need result
        .map(drop)
}
import {query} from "../node_modules/db-connector/assembly/sqlite"

export function createScheme(): void {
    let request = `CREATE TABLE messages(message text, username text)`;
    query(request);
}

Run it as usual via run.sh, and now we have a full-blown backend for a feed application! Hooray!

Uhm... what? Oh, there should be a frontend? People don't like terminal-only backends? That's a bummer.

Well, let's wire up a frontend!

Human-friendly frontend

TODO

Fluid – decentralized twitter-like feed built on Fluence


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.