Contents:
To create a smart contract with crystalscript, define one of more of the following in your source code file:
constants - global constants
persisted data - contract data on the blockchain
private, public and read-only functions
import statements - accessing other contract's functions and traits
traits - function definitions that are declared, implemented and referenced
At the top level of the contract, constants are equivalent to "define-constant" in Clarity, but declared as follows:
const NAME = EXPRESSION;
For example:
const ERR_INSUFFICIENT_FUNDS = 5;
Constants may also appear in scopes, such as function and if/else bodies. For example:
function xyx() { const factor = u5; }
Scoped constants must appear at the top of the scope before any other statements. However, constants may reference inner functions declared after the const as well as global functions.
Scoped constants are converted to a Clarity "let" function.
Persisted variables hold a single value that may be read and updated by the contract. The value is "global" - not tied the lifetime of the called contract function.
Define a persistent variable using the syntax:
persist NAME as TYPE with initial-value = EXPRESSION;
This statement will compile to the Clarity function "define-data-var".
For example:
persist costFactor as uint with initial-value=5;
To read the variable, simply use it by name. Eg.
function cost(n uint) {
return n * costFactor;
}
To change the variable, simply assign a new value and commit the result with ok():
public function advanceCost(amt uint) {
const oldVal = costFactor;
costFactor = costFactor + amt;
return ok(oldVal);
}
Persistent maps (data maps) are like hash tables, dictionaries, associatve arrays, etc. The hash or key to the data map can be any type, including a map. Data map values can also be any type. Data maps are "global" - not tied the lifetime of the called contract function.
Define a data map using the syntax:
persist NAME as TYPE => TYPE;
This statement will compile to the Clarity function "define-map".
For example:
persist id2user as { id: int } => { name: string[10], balance: uint };
These variables are readable just by using their id, eg:
const username = id2user[{ id: 1 }].name;
And settable:
id2user[{ id: 1 }] = { name:"fred", balance:u500 };
Individual keys in the above example are not changeable. ie.
id2user[{ id: 1 }].name = "fred sr."; // NOT ALLOWED
...but can be accomplished with the merge
function:
id2user[{ id: 1 }] = merge(id2user[{ id: 1 }], { name:"fred sr." });
Data map entries can be removed with delete
:
delete id2user[{ id:1 }];
Use the ?=
assignment operator to update entries only if they don't already exist. This operator compiles to Clarity's "map-insert" function:
id2user[{ id:1 }] ?= { name:"fred", balance:u0 };
Contracts declare fungible token variables to construct new fungible "currencies".
The data associated with fungible tokens is "global" - not tied the lifetime of the called contract function.
To create a fungible token with a limited supply of tokens, use this syntax:
persist NAME as fungible-token with total-supply=AMOUNT;
for fungible tokens with unlimited supply, use:
persist NAME as fungible-token with unlimited-supply;
For example, an 'updown' token with 1 million maximum supply:
persist updown as fungible-token with total-supply=u1000000;
To mint, burn, transfer, etc these tokens, see the system calls that start with "ft-", or use the corresponding properties of the fungible token:
persist updown as fungible-token with total-supply=u1000000;
const alice = 'SP2JPBTPVXN7V5N0SH7ZP95GM1GTFVT8SKVAW3R77;
const bob = 'SP3WT3PT3NA5SWW82DZ8ZFK8RN412AD3KR5Q7Q3K4;
public function mint-and-give() {
updown.mint?(u100, alice);
updown.getSupply();
updown.transfer?(u15, alice, bob);
updown.burn?(u10, alice);
return ok( updown.getBalance(alice) ); // 75
}
Contracts declare non-fungible token variables to construct new non-fungible tokens.
The data associated with non-fungible tokens is "global" - not tied the lifetime of the called contract function.
To create a non-fungible token, use this syntax:
persist NAME as nonfungible-token identified by TYPE;
For example:
persist nifty as nonfungible-token identified by string[50];
The indentifier, or unique id, for non-fungible tokens may use 'int', 'uint', 'buff' and 'string' types.
To mint, burn, transfer, etc these tokens, see the system calls that start with "nft-", or use the corresponding properties of the non-fungible token:
persist nifty as nonfungible-token identified by string[50];
const alice = 'SP2JPBTPVXN7V5N0SH7ZP95GM1GTFVT8SKVAW3R77;
const bob = 'SP3WT3PT3NA5SWW82DZ8ZFK8RN412AD3KR5Q7Q3K4;
public function mint-and-give() {
nifty.mint?("roo", alice);
nifty.mint?("hardy", alice);
nifty.transfer?("roo", alice, bob);
nifty.burn?("hardy", alice);
return ok( nifty.getOwner?("roo") ); // bob
}
Crystalscript functions declared at the top level of the file compile to Clarity "define-public", "define-private", and "define-read-only" functions. Public and read-only functions are callable by other contracts.
A function declaration has the format:
VISIBILITY function NAME (NAME TYPE, ...) { STATEMENTS }
For example:
public readonly function cost (item string[10], basePrice int)
{
if (item == "widget") { return ok(basePrice * 2); }
else { return ok(basePrice); }
}
Visibility may be one of the following:
Visibility | Description |
---|---|
private | Callable only by other functions in the same contract. These functions may return any type. |
public | Callable by other contracts and by functions in the same contract. These functions must return a response type. |
public readonly | Callable by other contract and by functions in the same contract. These functions should return a response type if they will be called by other contracts. |
If not given, a function will be private.
Crystalscript allows function declarations within function bodies. These functions will appear as private functions at the global scope of the compiled Clarity contract. Inner functions may appear anywhere in a scope, as long as they're after after all const
variables. These functions are "hoisted" so that they may be called by const
variable expressions or from within the scope body even though their declaration may come after.
For example:
public function cost(item string[10], basePrice int)
{
const widgetCost = compute(basePrice, 2);
function compute(price int, factor int)
{
return price * factor;
}
if ( item == "widget" ) { return ok(widgetCost); }
return ok(basePrice);
}
More on inner functions can be found here
If the crystalscript compiler is given the --contract-name
argument (or the shortened form -n
), it will create a contract-name.import
file next to the .clar
compiled output file.
For example, running the command:
crystalscript -n mycontract mycontract.crystal
will produce the two files:
mycontract.crystal.clar
mycontract.import
The import file contains declarations that allow other contracts to access it's public functions, read-only functions, trait definitions and trait implementations.
import
has the following two syntaxes:
import CONTRACT-ID from "/path/to/file.import" as ID;
import "/path/to/file.import" as ID;
The first import statement requires a CONTRACT-ID and ID.
CONTRACT-ID is an abolute or relative contract principal. eg. "'ST26FVX16539KKXZKJN098Q08HRX3XBAP541MFS0P.contract-name" or ".contract-name", respectively. And, ID is an identifier used to refer to the contract in the program.
The definitions of the imported file will be assocated with the contract-id specified. At deployment time, the contract must exist and contain those definitions or Stacks will reject the contract.
If the import file path is a relative path:
- ... and the path starts with "./". The file will be read relative to the script that contains the import statement. For example: "./ftcontract.import".
- ... otherwise, the import is loaded from the directory containing the well-known set of imports see well known contracts.
The second import statement syntax lacks a CONTRACT-ID. This syntax requires that an additional file ("file.import.json") exists in the same directory as the import file, which contains a lookup table with the contract id for the stacks network being compile for.
The default stacks network is "dev", but can be changed to something else like "testnet" or "mainnet" with crystalscript argument --net <network>
.
For example, SIP-009 defines trait "nft-trait". This trait exists on mainnet as "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait" and testnet as "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait".
To avoid having to maintain multiple source code files, eg. one for testnet and one for mainnet, simply create an import.json file for it:
{
"contract-id": {
"mainnet": "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait",
"testnet": "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait",
"dev": "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait"
}
}
The compiler will first look for the network name in the "contract-id" map choosing the contract id from that key if it exists. If it doesn't exist, the compiler will look for a key named "default" and use the contract-id associated with it. Otherwise, compilation will fail.
Let's say we have two contracts (files) "price.crystal" and "register.crystal". The "price.crystal" functions get-itemid()
and priceof()
are accessed by "register.crystal" by importing the price.import
file using the syntax import .price from "./price.import" as price
, then calling the functions using price.get-itemid()
and price.priceof()
.
Here is a concrete example:
// --------------------------------------------------------
// price.crystal
// --------------------------------------------------------
// compile with: "crystalscript -n price -t price.crystal"
// saved: price.crystal.clar
// saved: price.import
const noth_itemid = u2001;
public function get-itemid(item string[5]) {
if (item == "noth") {
return ok(noth_itemid);
}
else {
return err(-1);
}
}
public function priceof(itemid uint) {
if (itemid == noth_itemid) {
return ok(u1000);
}
else {
return ok(u500);
}
}
// --------------------------------------------------------
// register.crystal
// --------------------------------------------------------
// compile with: "crystalscript -t --no-newdb register.crystal"
// saved: register.crystal.clar
import .price from "./price.import" as price;
public function get-registration-cost(item string[5]) {
const itemid = price.get-itemid(item);
if (itemid.iserr()) {
return err(itemid.errval);
}
return price.priceof(itemid.okval);
}
// TEST: get-registration-cost(u"noth") => ok: val==1000
crystalscript -n price -t price.crystal
crystalscript -t --no-newdb register.crystal
Arguments:
-n
: names the contract-t
: deploys the contract usingclarity-cli
(which must be installed and available in the path or specified with environment variable CLARITY_CLI)--no-newdb
: tells the compiler not to reset the existing clarity vm database containing the.price
contract deployed in the previous step
Eg:
> crystalscript -n price -t price.crystal
----------------------
compile price.crystal
----------------------
saved: price.crystal.clar
saved: price.import
run tests
creating new clarity vm db 'test_db'
deploy price.crystal.clar as ST26FVX16539KKXZKJN098Q08HRX3XBAP541MFS0P.price
> crystalscript -t --no-newdb register.crystal
-------------------------
compile register.crystal
-------------------------
saved: register.crystal.clar
run tests
deploy register.crystal.clar as ST26FVX16539KKXZKJN098Q08HRX3XBAP541MFS0P.test
test 1: (get-registration-cost u"noth")
success: ok and 'val==1000' is true
1 tests, 0 failures, 1 successes
There are a few well known contracts that are included with crystalscript that can be imported into any script.
Contract | Description |
---|---|
nft-trait | Trait defined by SIP-009 for non-fungible tokens |
sip-010-trait-ft-standard | Trait defined by SIP-010 for fungible tokens |
sip013-semi-fungible-token-trait | Trait defined by SIP-013 for semi-fungible tokens |
sip013-transfer-many-trait | Trait defined by SIP-013 for send many transfers |
These contracts can be imported with the following syntax:
import "CONTRACT" as NAME;
for example:
// import SIP-009 nft-trait contract
import "nft-trait" as nft-trait;
// implement nft-trait from the contract
implement trait nft-trait.nft-trait;
// required function of nft-trait
public function get-last-token-id() {
return ok(u1);
}
//...
For more information see https://github.com/stacksgov/sips.
These contracts all have different contract id's depending on what stacks network you're deploying to (eg. testnet or mainnet). When compiling, use the --net
argument to choose what network to compile for.
In Clarity, a trait is essentially a named group of function definitions. Contracts define traits, implement traits and use traits.
More than one contract can implement the same trait, and Clarity allows functions to accept a contract that implements a specific trait as an argument, so at runtime, the contract can call whatever trait implementation it was passed.
Here are some terms:
- trait definition - a group of function definitions
- trait implementation - an implemetation of all functions described by a trait definition by a contract
- trait type - a crystalscript data type for a trait definition
- trait function - one of the trait's function definitions
- "use a trait" - means a contact has one or more functions that accept an argument having a trait type
Contracts may not implement or use the trait they're defining.
To define a trait, use this syntax:
define trait NAME {
public function NAME(TYPE, ...) => response<OKTYPE, ERRTYPE>,
...
};
To implement a trait, use the following syntax. This example assumes that a trait definition called mytrait
has been defined by contract .contract-with-trait
that has an import file at "./contract-with-trait.import". mytrait
has a single function definition.
// import the contract containing the trait definition
import .contract-with-trait from "./contract-with-trait.import" as contract-with-trait;
// declare that we're implementing a trait defined by the contract
implement trait contract-with-trait.mytrait;
// implement the 'mytrait' trait function
public function NAME(TYPE, ...) {
returns OK()-OR-ERR();
}
The compiler will ensure no trait functions are missed and that the function implementations have the same signatures as the trait definition.
Using a trait means a contact has one or more functions that accept an argument having a trait type.
To use a trait, import the trait definition, then add a trait type argument to a function and call the desired trait function.
// import the contract containing the trait definition
import .contract-with-trait from "./contract-with-trait.import" as contract-with-trait;
// use the trait
public function callme(impl trait<contract-with-trait.mytrait>, str string[5]) {
return impl.NAME(str);
}
Finally, execute a function that uses a trait:
// import the contract that implements the trait "mytrait"
import .mytrait-impl from "./mytrait-impl.import" as mytrait-impl;
// import the contract that uses the trait "mytrait" (as in above, it
// contains a function named 'callme' that has a trait<"mytrait"> argument)
import .uses-trait from "./uses-trait.import" as uses-trait;
// call the function that uses "mytrait" by supplying the implementation
// contract as the argument
function whatever() {
const x = uses-trait.callme(mytrait-impl, "hello");
return ok(x);
}
Crystalscript supports all the same types as Clarity. Strings default to utf-8. To get an ascii string, use "string".ascii()
, which tells the compiler to emit an ascii literal (it won't cause an additional function call).
Type | Description | Literal | Example |
---|---|---|---|
int | Signed whole number | [-]{digits} | const n = -10; |
uint | Unsigned positive whole number | "u"{digits} | const n = u5; |
bool | Boolean true/false | true or false | const b = true; |
principal | relative or absolute contract id | .contract or 'stacks-address.contract | const p = .mycontract; const p2 = 'ST26FVX16539KKXZKJN098Q08HRX3XBAP541MFS0P.mycontract; |
Type | Description | Literal | Example |
---|---|---|---|
string[LEN] | utf-8 string having a maximum length of LEN | text surrounded by double quotes | const str = "string"; |
string-ascii[LEN] | ascii string having a maximum length of LEN | text surrounded by double quotes followed by a call to ascii() | const str = "string".ascii(); |
buff[LEN] | Array of arbitrary bytes having a maximum length of LEN | starts with 0x followed by a series of hexadecimal numbers |
const b = 0x01FF; |
list[LEN] | Array with maximum length LEN having elements all with the same type TYPE. | values between square brackets | const nums = [1,2,3]; const two = nums[1]; |
Type | Description | Literal | Example |
---|---|---|---|
map | This type is called a tuple in Clarity. It's also referred to as an associative array and dictionary. The key/TYPE pairs are specified inside curly brackets. Values don't have to have the same TYPE. | { key:TYPE, ... } | const dict = { a:1, b:{ str:"abc" }}; |
Type | Description | Literal | Example |
---|---|---|---|
optional TYPE | a type that may be 'none' or be TYPE | optional(value) | const op = optional(5); |
Type | Description | Literal | Example |
---|---|---|---|
response<OKTYPE,ERRTYPE> | a response wraps a value that is either designated as "ok" or "err" depending on how the response was contructed. An "ok" response has type OKTYPE, and "err" type ERRTYPE | ok(value) or err(value) | const resp = ok(u10); |
These are advanced types that have no literal construction.
Type | Description |
---|---|
trait<TRAIT-DEF> | trait having a trait definition specified by TRAIT-DEF. See the topic on traits. |
datavar | A persisted variable created with persist NAME as TYPE |
datamap | A persisted map created with persist NAME as TYPE => TYPE |
ft | A fungible token created by persist fungible token |
nft | A non-fungible token created by persist nonfungible-token |
Some types have properties that may be accessed with the dot operator. Below is a table of types and their supported properties along with the corresponding system call for it, if applicable.
Type | Property | System call | Example |
---|---|---|---|
string, | .concat() | concat | "abc".len() |
string-ascii | .indexOf?() | index-of? | |
.replaceAt?() | replace-at? | ||
.slice?() | slice? | ||
.len() | len | ||
.ascii() | |||
.toInt?() | string-to-int? | ||
.toUint?() | string-to-uint? | ||
buff | .concat() | concat | 0x01.concat(0x02) |
.indexOf?() | index-of? | ||
.replaceAt() | replace-at? | ||
.slice?() | slice? | ||
.len() | len | ||
list | .concat() | concat | [1,2].indexOf?(2) |
.indexOf? | index-of? | ||
.replaceAt?() | replace-at? | ||
.slice?() | slice? | ||
.len() | len | ||
.append() | append | ||
int, uint | .toStringAscii() | int-to-ascii | 5.toString() |
.toString() | int-to-utf8 | ||
response | .isok() | is-ok | const v = r.okval |
.iserr() | is-err | ||
.okval | unwrap-panic | ||
.errval | unwrap-err-panic | ||
ft | .getBalance() | ft-get-balance | ft.getSupply() |
.getSupply() | ft-get-supply | ||
.transfer?() | ft-transfer? | ||
.mint?() | ft-mint? | ||
.burn?() | ft-burn? | ||
nft | .getOwner?() | nft-get-owner? | nft.getOwner(u1000) |
.transfer?() | nft-transfer? | ||
.mint?() | nft-mint? | ||
.burn? | nft-burn? |
Function | Description | Example |
---|---|---|
principal() | principal from string literal | principal(".contract") |
optional() | make optional | optional(a) |
int() | convert to int | int(a) |
uint() | convert to uint | uint(a) |
Name | Type |
---|---|
contract-caller | principal |
tx-sender | principal |
block-height | uint |
burn-block-height | uint |
stx-liquid-supply | uint |
is-in-regtest | bool |
Please consult the Clarity docs for details about these.
Public functions in Clarity are required to return a response type. Crystalscript treats responses like an object with 4 properties:
Response property | Description |
---|---|
isok() | this function returns true if the response is an ok response |
iserr() | this function returns true if the response is an err response |
okval | obtains the value of the ok response. Accessing this property when the response is an err response will cause a panic (the call will immediatly exit and fail) |
errval | obtains the value of an err response. Accessing this property when the response is an ok response will cause a panic (the call will immediately exit and fail) |
Examples of using a response object:
public function iseven(n int) {
if ( n < 0 ) { return err(-1); }
return n % 2 == 0 ? ok(true) : ok(false);
}
public function test_even(n int) {
const even_response = iseven(n);
if (even_response.isok()) {
return ok(even_response.okval ? "yes" : "no");
}
return err(even_response.errval);
}
// TEST: test_even(5) => ok: val=="no"
// TEST: test_even(6) => ok: val=="yes"
// TEST: test_even(-1) => err: val==-1
Optionals in Clarity are just like other types except the item's value, in addition to a value of it's designated type, may be 'none' signifying a null or unset value.
Accessing an optional value will cause an automatic unwrap (Clarity's mechanism for obtaining the optional's value). For example, sending optional(2) to 'fn' will return ok(4), but sending 'none' will cause a panic:
public readonly function fn(n optional int) {
return ok(n * 2);
}
// TEST: fn((some 2)) => ok: val==4
// TEST: fn(none) => runtime-failure: /UnwrapFailure/.test(val.error)
This can be avoided by testing that the optional is not 'none':
public readonly function fn(n optional int) {
if (n) { return ok(n * 2); }
return err(-1);
}
// TEST: fn((some 2)) => ok: val==4
// TEST: fn(none) => err: val==-1
Note that if (n)
is a 'truthy' test. Truthy for optionals means that the optional is not 'none' and is equivalent to if (n != none)
. It does not mean that the value of n is truthy (ie. if n were 'false', truthy 'n' is still true).
Functions that expect optionals as arguments, but are called with a concrete type will be made optional. In the example below, it's not necessary to call fn with 'optional(mybuf)', it can just be sent as-is:
function fn(buffer optional buff[100]) {
const newbuf = concat(buffer, 0x00);
return newbuf;
}
public function test() {
const mybuf = 0x0504030201;
return ok(fn(mybuf));
}
// TEST: test() => ok: val=="0x050403020100"
Where possible, 'int' and 'uint' types will be coerced to satisfy the expression or function argument type needed.
For example, function 'fn' below requires a 'uint' argument, but a literal 'int' is given and was coerced to a 'uint'. If the int was negative, a compiler TypeMismatchError will occur.
function fn(x uint) {
return x*2;
}
public function test() {
return ok(fn(5));
}
There is no literal coersion for system calls, except for those implemented as operators.
Crystalscript supports the following operators:
Operator | Name | Description | Example |
---|---|---|---|
+ | add | add two numbers | 1 + 1 |
- | subtract | subtract one number from the other, or change the sign of a number | 2 - 1 |
* | multiply | multiply two numbers | 2 * 2 |
/ | divide | divide one number by another | 4 / 2 |
% | modulo | division remainer | 5 % 2 |
** | power | raise one number to the power of another | 2 ** 4 |
^ | xor | exclusive or | 18 ^ 2 |
>= | gte | greater than or equal | a >= b |
<= | lte | less than or equal | a <= b |
> | gt | greater than | a > b |
< | lt | less than | a < b |
== | eq | equals | a == b |
!= | ne | not equals | a != b |
&& | and | and | a && b |
|| | or | or | a |
! | not | not | ! a |
# | unwrap | unwrap optional | #a |
?: | if-then | if/then/else | a ? b : c |
() | call | function call | concat( [1,2], [3, 4] ) |
= | assign | change a persistent variable or data map entry | data[{ index:1 }] = { amt:10 } |
?= | conditional assign | change a data map entry if it doesn't exist | data[{ index:1 }] ?= { amt:5 } |
delete | delete | delete a data map entry | delete data[{ index:1 }] |
[] | brackets | derefernce something | const amt = data[{ index:1 }]["amt"] |
. | dot | dereferece something | const amt = data[{ index:1 }].amt |
~ | bitwise not | one's compliment | const x = ~n >>10; |
& | bitwise and | bitwise and | const x = u65 & u41; |
| | bitwise or | bitwise or | const x = u10 |
<< | bitwise shift left | shift bits left | const x = u1 << 7; |
>> | bitwise shift right | shift bits right | const x = u8 >> 7; |
The following words are reserved by crystalscript and can't used used as identifiers:
Keyword | Keyword |
---|---|
_countof | int |
_typedef | is-in-regtest |
as | list |
block-height | none |
bool | nonfungible-token |
buff | optional |
burn-block-height | persist |
const | principal |
contract-caller | private |
declare | public |
define | readonly |
delete | response |
else | return |
extern | string |
false | string-ascii |
foreach | string-utf8 |
function | stx-liquid-supply |
fungible-token | trait |
if | true |
implement | tx-sender |
implements | uint |
import | use |
A "const" is a constant. There are no standard variables in Clarity that allow you to update their value (except for persisted data).
Constants must be declared before any other statements, but can be used in any scope.
The following syntax is used to define a constant:
const NAME = EXPRESSION;
Example:
public function fn(bool b) {
const N = 10;
if (b) {
const NN = N + N;
return ok(NN);
}
return err(-1);
}
'If' statements have typical syntax:
if (EXPRESSION) {
STATEMENTS
}
else if (EXPRESSION) {
STATEMENTS
}
else {
STATEMENTS
}
However, braces are always required.
As discussed above in functions, function bodies may contain function definitions of their own, called "inner" functions. Inner functions must appear after "const" statements and before return, but are hoisted so that they may be called by "const" expressions and expression in the function body.
During compilation, inner functions are renamed and made global since Clarity does not natively support them. Access to constants of the parent scope is allowed through closure.
Here is an example:
public function example() {
const factor = 5;
const x = getx_via_y();
function getx_via_y() {
function getx() {
return 2 * factor;
}
return getx();
}
return ok(x);
}
The syntax for return is:
return EXPRESSION
Return exits the function, returning the value given by the expressions. All functions are required to return a value.
A system call ("syscall") is compiled to the equivalent Clarity function.
Familiarity with Clarity's functions is essential since crystalscript syscalls require the same number of arguments and return the same type as the equivalent Clarity function.
The syntax for making a system call is fn(args, ...)
. For instance, list concatenation in crystalscript using concat: concat([1,2], [3,4])
is compiled to (concat (list 1 2) (list 3 4))
in Clarity. Same function name, same number and types of arguments.
Please consult the Clarity docs for details on what arguments are acceptable and what return value to expect from making system calls.
Some Clarity functions are implemented as operators (eg. "&&" instead of "and", "||" instead of "or", etc) and some as type conversion functions ("int" instead of "to-int", etc). Crystalscript supports all Clarity functions either as an operator, or as a function call. The list below are the system calls that crystalscript supports as a function call.
Like in Clarity, functions that return a response or optional type end with '?', and those that could immediatly return from the function end with '!'.
Function | Description |
---|---|
append | append a single element to a list and returns the new list |
as-contract | execute an expression as the contract instead of the caller |
as-max-len? | changes the maximum size of a sequence |
asserts! | assert that an expression is true, or return from the function |
at-block | |
buff-to-int-be | convert a buff in big-endian to an integer |
buff-to-int-le | convert a buff in little-endian to an integer |
buff-to-uint-be | convert a buff in big-endian to an unsigned integer |
buff-to-uint-le | convert a buff in little-endian to an unsigned integer |
concat | concatenates two sequences and returns the new sequence |
contract-call? | call pubic or readonly functions of other contracts |
contract-of | get the principal implementing a trait |
default-to | returns the given value unless it's none, in which case the supplied "default value" is returned instead |
err | constructs an error response |
filter | calls a function for each element of a list and returns a new list containing elements for which the function returned true |
fold | obtain a value from a list where a function is called for each element of the list with the element plus the prior result |
from-consensus-buff? | deserialize a buffer into a Clarity value |
ft-burn? | remove tokens from the outstanding supply |
ft-get-balance | fungible token balance of a principal |
ft-get-supply | fungible token supply outstanding |
ft-mint? | mint fungible tokens and increase outstanding supply |
ft-transfer? | move fungible tokens between parties |
get-block-info? | |
get-burn-block-info? | |
hash160 | compute hash |
index-of? | find an element and return it's index within a sequence |
int-to-ascii | convert number to string-ascii form |
int-to-utf8 | convert number to string form |
is-err | test whether a response is an err |
is-none | test whether an optional is none |
is-ok | test whether a response is ok |
is-some | test whether an optional is not none |
is-standard | tests whether the principal matches the current network type |
keccak256 | compute hash |
len | obtain the length of a sequence |
log2 | base 2 logarithm |
map | construct a new list from the return values of a function that's called for every element of other lists |
merge | combine two maps into a new map |
nft-burn? | destroy an nft |
nft-get-owner? | obtain the owner of an nft |
nft-mint? | create an nft |
nft-transfer? | transfer an nft between parties |
ok | constructs an ok response |
principal-construct? | get a principal from a buffer |
principal-destruct? | convert principal into details about the principal |
principal-of? | get the principal from a public key |
output event | |
replace-at? | returns a new list with the element at the selected index replaced with the new value |
secp256k1-recover? | obtain the public key used to sign a message |
secp256k1-verify | verify a signature |
sha256 | compute hash |
sha512 | compute hash |
sha512-256 | compute hash |
slice? | get a portion of a sequence |
sqrti | integer square root |
string-to-int? | convert a string to integer |
string-to-uint? | convert a string to an unsigned integer |
stx-account | |
stx-burn? | destroy stx |
stx-get-balance | returns the principal's stx balance |
stx-transfer-memo? | transfer stx between two parties, with a memo |
stx-transfer? | transfer stx between two parties |
to-consensus-buff? | serialize any value into a buffer |
try! | return from the function if the response is err or the optional is none, otherwise returns the value |
unwrap! | return from the function if the response is err, or the optional's value is none, otherwise returns the value |
unwrap-err! | return from the function if the response is ok, otherwise returns the value |
unwrap-err-panic | return from the function if the response is ok, otherwise return the value |
unwrap-panic | return from the function if the response is err or the optional is none, otherwise return the value |
Please consult the Clarity docs for details on what arguments are acceptable and what return value to expect from making system calls.
Also see type conversion functions.
foreach is a built-in crystalscript function for which there is no equivalent function in Clarity.
foreach is a list iterator that constructs a new list from the return value of a function that's called for each list element.
Clarity does not support iteration by design. It does however have three system calls map
, fold
and filter
that iterate over lists. Using fold
as a generic iterator can be done manually but it's cumbersome. foreach
takes care of the details to make iteration of lists easy.
foreach takes two arguments, the list to iterate over and a function that accepts the iteration element and returns another value. All the values returned by the function are returned by foreach as a list.
The iteration function must return the same type for every iteration. However the returned value does not have to be the same type as the list being iterated over.
There is no way to change the size of the list with foreach. The input list size and the returned list will always be the same length.
The function argument to foreach can be an anonymous function that accepts one or two arguments or an existing function that takes a single argument. The first argument to either function is always an element of the list. When using an anonymous function a second argument, that is not required, is the index of the list element as a 'uint' type. To access the index and use an existing function, provide an anonymous function that calls the existing function yourself.
Examples of using foreach:
// anonymous function with w/o index argument
public function foreachtest1() {
const n = 100;
const newarray = foreach([1,2,3], (item) => {
return item * n;
});
return ok(newarray);
}
// anonymous function with index argument
public function foreachtest2() {
const newarray = foreach([1,2,3], (item, idx) => {
return uint(item) * idx;
});
return ok(newarray);
}
// existing function
private function foreachtest4-handler(n int) {
return n * 2;
}
public function foreachtest4() {
const newarray = foreach([1,2,3], foreachtest4-handler);
return ok(newarray);
}
Comments in crystalscript start with "//". Any text after the "//" up to the end of the line, is ignored.
// This is a comment
function cost() {
return u1000; // this is a comment
}
Comments are not passed through to the compiled Clarity code.
To include a comment in compiled code, add ";;" to the comment.
// This comment is dropped
// ;; This comment appears in generated Clarity code
function cost(name string[10]) {
// ;; name: supply the name of the item you want the cost of
if ( name == "abc" ) {
return 10; // ;; cost of "abc"
}
return -1;
}
The compiled code looks like:
;; This comment appears in generated Clarity code
;; returns int
(define-private (cost (name (string-utf8 10)))
;; name: supply the name of the item you want the cost of
(begin
(and (is-eq name u"abc")
;; cost of "abc"
(asserts! false 10))
-1))
When the -t
command line argument is given to the compiler, it will run tests embedded in the source code after the contract has been deployed.
To deploy locally and run embedded tests, 'clarity-cli' from stacks-blockchain must be installed and in the path (or its location specified by the CLARITY_CLI environment variable).
Clarity-cli provides a local mock Stacks environment.
Tests are added to source code in comments that start with "// TEST:". They have the following format:
// TEST: function-to-call(arguments) => ok|err|runtime-failure: javascript test script
- function-to-call is the name of a public function within the source code
- arguments are a comma separated list of arguments that the function should be called with. Arguments must be in Clarity syntax, so for example, a list argument must be supplied as the Clarity expression
(list 1 2 3)
. Because strings in crystalscript are utf-8, any string argument must be preceeded by 'u', for examplemyfunction(u"string")
. Use Clarity's "some" function to create an optional, egmyfunction((some 5))
. - ok|err|runtime-failure are the expected response type
- javscript test script is a script that returns true or false to indicate whether the function call returned the expected value.
For 'ok' and 'err' response types, the variable val
contains the function result. If a runtime-failure
occurs, val
will contain wording that can be matched by regular expression or whatever in javascript.
If a test fails, everything Stacks returned will be output for diagnostics.
There are lots of examples in the tests
directory.