This section explains step by step how to write a new smart contract on
Casper. Start with main.rs
from the previous section.
The Casper VM executes a smart contract by calling the
call
function specified in the contract. If the function is
missing, the smart contract is not valid. The simplest possible example is
an empty call
function.
#[no_mangle]
pub extern "C" fn call() {}
The #[no_mangle]
attribute prevents the compiler from
changing (mangling) the function name when converting to the binary format
of Wasm. Without it, the VM exits with the error message:
Module doesn't have export call
.
It's possible to pass arguments to smart contracts. To leverage this feature, use runtime::get_named_arg.
use casperlabs_contract::contract_api::runtime;
#[no_mangle]
pub extern "C" fn call() {
let value: String = runtime::get_named_arg("value");
}
Saving and reading values to and from the blockchain is a manual process
in Casper. It requires more code to be written, but also provides a lot of
flexibility. The storage system works similarly to a file system in an
operating system. Let's say we have a string
"Hello Casper!"
that needs to be saved. To do this,
use the text editor, create a new file, paste the string in and save it
under a name in some directory. The pattern is similar on the Casper
blockchain. First you have to save your value to the memory using
storage::new_uref. This returns a reference to the memory object that holds the
"Hello Casper!"
value. You could use this reference
to update the value to something else. It's like a file. Secondly you
have to save the reference under a human-readable string using
runtime::put_key. It's like giving a name to the file. The following function
implements this scenario:
const KEY: &str = "special_value";
fn store(value: String) {
// Store `value` under a new unforgeable reference.
let value_ref = storage::new_uref(value);
// Wrap the unforgeable reference in a `Key`.
let value_key: Key = value_ref.into();
// Store this key under the name "special_value" in context-local storage.
runtime::put_key(KEY, value_key);
}
After this function is executed, the context (Account or Smart Contract)
will have the content of the value
stored under
KEY
in its named keys space. The named keys space is a
key-value storage that every context has. It's like a home directory.
The code below is the simple contract generated by
cargo-casper (found in
contract/src/main.rs
of a project created by the tool). It
reads an argument and stores it in the memory under a key named
"special_value"
.
#![cfg_attr(
not(target_arch = "wasm32"),
crate_type = "target arch should be wasm32"
)]
#![no_main]
use casperlabs_contract::{
contract_api::{runtime, storage},
};
use casperlabs_types::{Key, URef};
const KEY: &str = "special_value";
const ARG_MESSAGE: &str = "message";
fn store(value: String) {
// Store `value` under a new unforgeable reference.
let value_ref: URef = storage::new_uref(value);
// Wrap the unforgeable reference in a value of type `Key`.
let value_key: Key = value_ref.into();
// Store this key under the name "special_value" in context-local storage.
runtime::put_key(KEY, value_key);
}
// All session code must have a `call` entrypoint.
#[no_mangle]
pub extern "C" fn call() {
// Get the optional first argument supplied to the argument.
let value: String = runtime::get_named_arg(ARG_MESSAGE);
store(value);
}
CasperLabs maintains the casper-contract to allow developers to create smart contracts using AssemblyScript. The package source is hosted in the main Casper Network repository.
For each smart contract, it is necessary to create a project directory and initialize it.
The npm init
process prompts for various details about the
project. Answer as you see fit, but you may safely default everything
except name
, which needs to be specified. In this guide, we
will refer to the contract name as your-contract-name
.
mkdir project
cd project
npm init
Then install AssemblyScript and this package in the project directory.
npm install --save-dev assemblyscript@0.9.1
npm install --save casper-contract
The Assemblyscript contract API documentation can be found at https://www.npmjs.com/package/casper-contract.
Add script entries for AssemblyScript to your project's
package.json
. Note that your contract name is used for the
name of the WASM file. Replace your-contract-name with the name
of your contract.
{
"name": "your-contract-name",
...
"scripts": {
"asbuild:optimized": "asc assembly/index.ts -b dist/your-contract-name.wasm --validate --optimize --use abort=",
"asbuild": "npm run asbuild:optimized",
...
},
...
}
In the project root, create an index.js
file with the
following contents. Replace your-contract-name with the name of
your contract.
const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/dist/your-contract-name.wasm"));
const imports = {
env: {
abort(_msg, _file, line, column) {
console.error("abort called at index.ts:" + line + ":" + column);
},
},
};
Object.defineProperty(module, "exports", {
get: () => new WebAssembly.Instance(compiled, imports).exports,
});
Next, create a directory called assembly
, and in that
directory, create a file called tsconfig.json
in the
following way:
{
"extends": "../node_modules/assemblyscript/std/assembly.json",
"include": ["./**/*.ts"]
}
In the assembly
directory, also create an
index.ts
file, where the code for the contract needs to go.
You can use the following sample snippet, which demonstrates a simple smart contract that immediately returns an error and writes a message to a block when executed on the Casper Network.
//@ts-nocheck
import { Error, ErrorCode } from "casper-contract/error";
// simplest possible feedback loop
export function call(): void {
Error.fromErrorCode(ErrorCode.None).revert(); // ErrorCode: 1
}
Deploy is done by executing the previously created
execute_request
instance.
builder.exec(execute_request).expect_success().commit();
This will query the post-deploy value and assert for its change.
let result_of_query = builder
.query(None, Key::Account(account_addr), &[KEY.to_string()])
.expect("should be stored value.")
.as_cl_value()
.expect("should be cl value.")
.clone()
.into_t::<String>()
.expect("should be string.");
If you prefer a more complicated first contract, you can look at example contracts on the Casper Ecosystem GitHub repository for inspiration.