K3 Wasm Rust SDK

This document is for developers new to the framework who would like to make applications using our Rust SDK.

Getting started

The recommended way to start is by forking one of the many Rust examples we have on our GitHub (the simplest one is https://github.com/k3-labs/k3-template-hello-world-rust) and then deploy that on our platform through your GitHub login. If you have a Rust project already or want to learn how to set up a K3 project yourself here are the steps:

  1. Add K3 dependencies:

    k3-wasm-sdk = { version = "0.1.15" }
    k3-wasm-macros = { version = "0.1.10" }
  2. (optional) switch to nightly and add the nightly feature for the k3-wasm-macros dependency

    Switching to nightly gives you access to file-based routing for the macros (i.e. you can have a get function exported in /api/users/mod.rs and it would work as an HTTP GET handler for /api/users. Without file routing you have to specify the HTTP route you want to catch in the macro (this will be shown later)

  3. Add the init macro call to your main.rs file:

    // your other code here...
    
    k3_wasm_macros::init!();
  4. Annotate any handlers you want to export with the http_handler decorator:

    #[k3_wasm_macros::http_handler]
    pub fn get(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
    	// your code here...
    }

    Or without file-based routing:

    #[k3_wasm_macros::http_handler("/api/users")]
    pub fn get(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
    	// your code here...
    }

Writing handlers

Handlers are similar to serverless functions if you’ve ever implemented those in Node.js or Go. Their only parameter is an HTTP request object and it expects you to return an HTTP response object (we re-export the types from the popular Rust http crate for both). For example:

#[k3_wasm_macros::http_handler]
pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let target = req.uri().to_string();
	let name = target.split('/').last().unwrap();
	if name == "" {
		Response::builder()
			.status(400)
			.body("USAGE: /[name]".as_bytes().to_vec())
			.unwrap()
	} else {
		Response::builder()
			.status(200)
			.body(format!("Hello {}!", name).as_bytes().to_vec())
			.unwrap()
	}
}

You can have any logic you like there but you must remember that this will be compiled to WebAssembly which is a very sandboxed environment and doesn’t have direct access to the file-system or the internet for example.

To access the outside world you can use functions from our SDK and for dependencies you will have to check in their documentation if they support the WebAssembly environment (libraries that work in no_std will usually directly support WebAssembly as well).

Using the SDK

The K3 execution environment gives you many tools to access from the outside world (some of which you might have to enable in the dashboard for your team). We’ll go through all of them with a reference for each export and then examples on how to use them.

HTTP

This is currently the most minimal SDK module and will likely have more functions added soon.

fn get(url: &str) -> Option<Vec<u8>>

Makes an HTTP GET request to the provided URL and returns the response body as a buffer.

pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let res = k3_wasm_sdk::http::get("<https://example.com>").unwrap();
	Response::builder()
		.status(200)
		.header("Content-Type", "text/html")
		.body(res)
		.unwrap()
}

fn get_with_auth(url: &str, auth: &str) -> Option<Vec<u8>> Makes an HTTP GET request with AUTHORIZATION header to the provided URL and returns the response body as a buffer.

pub fn get_with_auth(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let res = k3_wasm_sdk::http::get_with_auth(
	"<https://example.com>",
	"EXAMPLE_AUTH_TOKEN"
	).unwrap();

	Response::builder()
		.status(200)
		.header("Content-Type", "text/html")
		.body(res)
		.unwrap()
}

IPFS

The IPFS module allows you to store and read files from IPFS.

fn upload(contents: Vec<u8>) -> String

Uploads some data to IPFS and returns a content ID that can be used to fetch it later.

pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let cid = k3_wasm_sdk::ipfs::upload("Hello IPFS!".as_bytes().to_vec());
	Response::builder()
		.status(200)
		.body(cid.as_bytes().to_vec())
		.unwrap()
}

fn read(cid: &str) -> Vec<u8>

Reads a file from IPFS given its content ID.

pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let cid = &req.uri().query().unwrap()["cid=".len()..];
	let buf = k3_wasm_sdk::ipfs::read(cid);
	Response::builder()
		.status(200)
		.body(buf)
		.unwrap()
}

Key Value Store

The kv module gives you access to a fast, in-memory, string-based key value store (that unreliably commits to IPFS). The module only exports the Db struct, the methods in which give you access to the functionality.

fn Db::open_default() -> Db

Opens the default store for this module.

pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let db = k3_wasm_sdk::kv::open_default();
	// use db here...
}

fn Db::get(&self, key: &str) -> Option<String>

Gets and returns a value from this store.

pub fn get(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let db = k3_wasm_sdk::kv::open_default();
	let current_value = db.get("value").unwrap_or("<UNDEFINED>".to_string());
	Response::builder()
		.status(200)
		.body(current_value.as_bytes().to_vec())
		.unwrap()
}

fn Db::set(&mut self, key: &str, value: String)

Inserts or modifies a value (depending on whether it exists or not) in this store.

pub fn post(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let new_value = String::from_utf8(req.into_body()).unwrap();
	let mut db = k3_wasm_sdk::kv::open_default();
	db.set("value", new_value);
	Response::builder()
		.status(200)
		.body("Updated value!".as_bytes().to_vec())
		.unwrap()
}

fn Db::delete(&mut self, key: &str)

Deletes the value for a given key in this store.

pub fn delete(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let mut db = k3_wasm_sdk::kv::open_default();
	db.delete("value");
	Response::builder()
		.status(200)
		.body("Deleted value!".as_bytes().to_vec())
		.unwrap()
}

Smart contracts

The sc module let’s you call Ethereum smart contracts and query data from them. Functions in this module expect you to provide the smart contracts address and also its ABI as a JSON string.

fn call(address: &str, abi: &str, fn_name: &str, params: impl Tokenize) -> String

Calls a function in the smart contract with the provided parameters and returns the transaction hash as a hex string. Panics if params are in an invalid format for the function.

pub fn post(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let address = std::env::var("SC_ADDRESS").unwrap();
	let new_value = String::from_utf8(req.into_body()).unwrap();
	let tx_hash = k3_wasm_sdk::sc::call(
		&address,
		include_str!("../abi.json"),
		"writeData",
		new_value,
	);
	Response::builder()
		.status(200)
		.body(tx_hash.as_bytes().to_vec())
		.unwrap()
}

fn query<R: Detokenize>(address: &str, abi: &str, fn_name: &str, params: impl Tokenize) -> R

Queries a function in the smart contract and returns the data. Panics if params or expected response type are invalid for the function.

pub fn get(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let address = std::env::var("SC_ADDRESS").unwrap();
	let data: (String, U256) = k3_wasm_sdk::sc::query(&address, include_str!("../abi.json"), "readData", ());
	Response::builder()
		.status(200)
		.body(data.0.as_bytes().to_vec())
		.unwrap()
}

Space & Time

The sxt module gives you access to your team’s Space & Time databases letting you run SQL on existing tables and make new ones.

fn execute_sql(sql: &str) -> String

Executes some provided SQL on your databases. Returns a JSON string with either info about how many rows were updated or the queried data. To learn more about the JSON format check out Space & Time’s HTTP API reference.

pub fn get(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	let data = execute_sql("SELECT * FROM users WHERE name = 'John'");
	Response::builder()
		.status(200)
		.header("Content-Type", "application/json")
		.body(data.as_bytes().to_vec())
		.unwrap()
}

fn execute_create_table(name: &str, types: &str) -> String

Creates a table in your selected database. This function exists because CREATE table instructions are special in Space & Time and require a biscuit generated using a unique public/private key pair. This function will handle that logic for you, but you can also manually do that using their API as reference and directly call execute_sql instead.

pub fn create(_req: Request<Vec<u8>>) -> Response<Vec<u8>> {
	execute_create_table(
		"public.users",
		"(name VARCHAR PRIMARY KEY, displayname VARCHAR NOT NULL, passwordhash VARCHAR NOT NULL)",
	);
	Response::builder()
		.status(200)
		.header("Content-Type", "application/json")
		.body("Created table!".as_bytes().to_vec())
		.unwrap()
}

Last updated