ParallelChain F Smart Contract SDK (pchain-sdk) provide Rust structs, functions, types, and macros that aid with the development of smart contracts executable in WebAssembly (WASM) engines implementing the ParallelChain F Smart Contract Virtual Machine (VM) specification.
pchain-sdk enable developers to write Smart Contracts in a idiomatic and highly-readable style. The SDK's macros transparently generate lower-level 'boilerplate' code for you, so that you can focus on writing the business logic of your application.
In particular, pchain-sdk is inspired by the Object-Oriented Programming (OOP) style. In the pchain-sdk model, a contract is simply a Rust struct that is associated with a set of functions and methods that are callable from the outside world (entrypoints).A quick glossary of the most important terms used in this README:
ParallelChain smart contract execution engine recognizes certain functions defined in a contract as entrypoint. The recognized functions with designated function name are called Entrypoints:
|Function name | Entrypoint | SDK Support | | :--- | :--- | :---| |init| An entrance that will be entered once during the contract deployment.| Optional | |actions| Starting point of execution when contract is being called in EtoC transaction.| Required| |views| An entrance that can be entered in EtoC transaction or request from REST API | Optional |
A very basic type of entrypoint is a function with name "contract". The macro #[contract_init]
transforms the "contract" function to an extern "C" function
so that it is callable to fullnode executor when the contract deployed as wasm file.
Before transforming, ```rust
pub fn actions(tx: Transaction) { ...
```
After transforming, ```rust
pub extern "C" fn actions() { ... ```
Contract can perform set and get of data to its own world-state just like an object can access and modify its own fields in common programming languages.
The concept is applied here to provide a structure that is familiar to contract developers. The macro #[contract]
applied to a struct will create getter and setter methods for the fields inside.
```rust
struct MyContract{ data: i32 } ```
In the body of smart contract, it can access the data and update it without explicity calling smart_contract::Transaction::get
and smart_contract::Transaction::set
, take care of name of the key and arguments parsing.
In this model,
- Key is an index in u8 integer format (hence, the maximum number of fields is 256). The above example, the key will be [0x0]. The order of intex is as same as the order of fields defined in the struct.
- Value are borsh-serializable and borsh-deserializable
- Value is primitive types (i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, usize, String, bool, Vec\ Nested struct is supported by deriving macro trait named ```rust struct MyField {
data1: u64,
data2: i32
} struct MyContract {
my_field :MyField
}
`` SDK provides convenience to access data in Contract Storage by creating getter and setting methods of fields defined in the contract struct.
If field name is For example,
```rust struct MyContract {
my_field :String
} // Example
...
MyContract::setmyfield("store to storage".tostring());
let storeddata: String = MyContract::getmyfield();
asserteq!("store to storage".tostring(), stored_data);
``` note: Getter and setter methods are not generated for Contract Field. Getting and setting this field will always load/save all data from/to Contract Storage. Contract can be invoked to carry out different operations just like accessing methods of an object or class in common programming languages.
The concept is applied here to allow developers specify functions in an impl as entrypoint methods.
The macro Example:
```rust impl MyContract { }
``` The generated skeleton consists of a Example
```rust
match ctx.getmethodname() {
"get_data" => {
// code block to execute the function
...
}
...
_=>{ unimplemented!() } ``` note:
- avoid defining method name with prefix The arguement inputted to the method is in format of CallData is parsed from the transaction arguement. The parsing logics follows the procedure below:
- first 4 bytes are version bytes which match CALLDATAVERSION
- if it is less than 4 bytes, it assumes version = 0 and CallData is returned with "empty" data
- panic if version does not match with CALLDATAVERSION
- the rest of the bytes are borsh-serialized from the structure CallData The return value from a contract call is in format of Contract MetaData is descriptive information about the contract's entrypoint methods. It is represented as a trait of the contract. Example:
Smart contract developers can share or even publish this information to the public so that others can interact with the contract in a proper way. To enable this feature in contract, add keyword "meta" as attribute to the contract macro. ```rust impl MyContract {
...
}
``` Under the hood, SDK generates a static slice variable The Init entrypoint is optional in the contract. It is enabled if the contract defines a constructor in contract Impl. ```rust impl MyContract { ``` Tne init entrypoint methods are recognized in the same way of actions entrypoint methods except that
- macro View entrypoint is optional in the contract. It is enabled by applying macro ```rust impl MyContract { }
``` Tne view entrypoint methods are recognized in the same way of actions entrypoint methods except that
- macro Nested struct
ContractField
. The nested struct also follows key-value conditions as same as Contract struct.[derive(ContractField)]
[contract]
In the above example, the key for storing in world-state will be [0x0, 0x0] for
data1and [0x0, 0x1] for
data2` while the value stored in world-state will be borse-serialized u64 data.Getter and Setter
data
, then associate methods get_data
and set_data
could be called to obtain data stored in the Contract Storage.[contract]
Entrypoint Methods as functions in an Impl
#[contract]
applied to an impl with macro action
will generate a skeleton code that executes a method according to the function name in input arguments from caller.[contract]
#[actoon]
fn get_data(&self) -> i32 {
...
}
#[action]
fn set_data(&mut self, d1 :i32) {
...
}
match
selector to find a matching str
to the function name, and then execute it.
}get_
or set_
followed by field name because it causes conflicts with getter and setting methods that are auto-generated by SDK.
- if the method takes the immutable receiver &self
as first argument, data is loaded from storage before execution. It is expensive operation.
- if the method takes the mutable receiver &mut self
as first argument, data is loaded from storage before execution and saved back to storage after execution. It is expsnsive operation.Input arguments from caller
Vec<u8>
. It is composited of leading 4 bytes as format version number and the raw bytes representing a borsh-serialized struct.
[format version (4 bytes)][raw content (bytes)]
- Format version is u32 integer and converted to 4 bytes in little endian format. It determines the way to decode raw content. Current version = 0.
- Raw content refers to the struct CallData:
rust
pub struct CallData{
method_name :String,
arguments :Vec<u8>,
}
- entrypoint is the name of the function of impl.
- arguments Vec\rust
fn entrypoint_1(data :i32, name :String) { ...
Then, Vector of Vec\Return Value to Contract Call
Vec<u8>
. It is borsh-seralizable so that it is up to developer to design the response data structure.rust
pub struct Callback {
return_val :Vec<u8>
}
Contract MetaData
rust
pub trait HelloContract {
fn hello();
fn hello_from(name:String) -> u32;
}
[contract(meta)]
__contract_metadata__
terminated by character '\0'. Its data resides in the memory section of the wasm code, which can be recognized by ParallelChain mainnet nodes.Init Entrypoint
[contract]
/// Init entrypoint method
#[init]
fn init_my_contract() {
...
init
is applied on the method
- must be associate method (no recevier, i.e. self, as method argument)
- there should be only one init
entrypoint methodView Entrypoint
view
on a method inside contract impl.[contract]
#[view]
pub fn view_my_data() -> i32 {
...
}
view
is applied on the method
- must be associate method (no recevier, i.e. self, as method argument)