Skip to main content

Contract Structure

Contracts in NexScript are somewhat similar to classes in object-oriented languages. A notable difference is that there is no mutable state. So once a contract is instantiated with certain parameters, these values cannot change. Instead, functions can be called on the contract that act on the contract's values to spend money from the contract. The extension for NexScript source code files is .nex, and the structure of these source files is explained below.

Pragma​

A contract file may start with a pragma directive to indicate the NexScript version the contract was written for. This ensures that a contract is not compiled with an unsupported compiler version, which could cause unintended side effects.

note

The pragma directive follows regular semantic versioning rules.

Example​

pragma nexscript ^0.1.0;
pragma nexscript >= 0.2.0 < 0.3.4;

Constructor​

A NexScript constructor works slightly differently than what you might be used to in regular object-oriented languages. It is not possible to define any statements inside the constructor, as the constructor is only used to store values in the contract. Because of this limited nature, there is no separate constructor function, but instead the parameters are specified directly on the class definition.

On nexa contract constructor parameters can be visible and hidden (private to the contract body). Visible arguments will be added to the tail of nexa contract address. See more information in the spec.

To indicate that NexScript's contract constructor parameter is visible to all potential spenders, the use of visible keyword is required.

In addition visible parameters can serve as a commitment message, playing a role when indexing contract addresses or giving a hint about this contract's behaviour. Such commitments might not even be used in the contract itself. To instruct NexScript compiler to ignore such a commitment parameter it shall be attributed with visible unused modifier. See example below.

contract OneOfTwo(bytes20 pkh1, bytes32 hash1, bytes20 visible pkh2, bytes32 visible unused hash2)

Note, that because visible parameters are pushed to the altstack after private ones, visible parameters should always be declared after private parameters. As shown in the above code snippet.

The following declaration would be wrong and render the script unspendable (because private parameter is declared after visible):

contract OneOfTwo(bytes20 pkh1, bytes32 hash1, bytes20 visible pkh2, bytes32 hash2)

Example​

pragma nexscript ^0.1.0;

contract HTLC(pubkey sender, pubkey recipient, int expiration, bytes32 hash) {
...
}
note

Upon initialization of the contract, constructor parameters are encoded and added to the contract's bytecode in the reversed order of their declaration. This can be important when manually initializing contracts for debugging purposes.

Functions​

The main construct in a NexScript contract is the function. A contract can contain one or multiple functions that can be executed to trigger transactions that spend money from the contract. In the basics the result of a function is just a yes or no answer to the question 'Can money be sent out of this contract?'. But by using a technique called covenants, it is possible to specify other conditions, like restricting where money can be sent. To read more about this technique, refer to the NexScript Covenants Guide.

Example​

pragma nexscript ^0.1.0;

contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
function transfer(sig recipientSig) {
...
}

function timeout(sig senderSig) {
...
}
}
note

Function parameters are passed in the reversed order of their declaration. This can be important when manually creating contract transactions for debugging purposes.

Statements​

NexScript functions are made up of a collection of statements that determine whether money may be spent from the contract.

require()​

The most important statement of NexScript contracts is the require statement. This statement takes a boolean expression and checks that it evaluates to true. If it evaluates to false instead, the transaction fails. This statement is used to ensure that the requirements are met to spend money from the contract.

Example​

pragma nexscript ^0.1.0;

contract P2PKH(bytes20 pkh) {
function spend(pubkey pk, sig s) {
require(hash160(pk) == pkh);
require(checkSig(s, pk));
}
}

Variable declaration​

Variables can be declared by specifying their type and name. All variables need to be initialised at the time of their declaration, but can be reassigned later on - unless specifying the constant keyword. Since NexScript is strongly typed and has no type inference, it is not possible to use keywords such as var or let to declare variables.

caution

NexScript disallows variable shadowing and unused variables.

Example​

int myNumber = 3000;
string constant myString = 'Nexa';

Variable assignment​

After their initial declaration, any variable can be reassigned later on. However, NexScript lacks any compound assignment operators such as += or -=.

Example​

i = i + 1;
hashedValue = sha256(hashedValue);
myString = 'Nexa rocks!';

Control structures​

NexScript has if / else statements and for loops with fixed bounds. There is no recursion and no return statement. If-else statements follow usual semantics known from languages like C or JavaScript.

note

There is no implicit type conversion from non-boolean to boolean types. So if (1) { ... } is not valid NexScript and should instead be written as if (bool(1)) { ... }

Example​

pragma nexscript ^0.1.0;

contract OneOfTwo(bytes20 pkh1, bytes32 hash1, bytes20 pkh2, bytes32 hash2) {
function spend(pubkey pk, sig s, bytes message) {
require(checkSig(s, pk));
bytes20 pkh = hash160(pk);

if (pkh == pkh1) {
require(sha256(message) == hash1);
} else if (pkh == pkh2) {
require(sha256(message) == hash2);
} else {
require(false); // fail
}
}
}

For loops​

A for loop repeats its body for each value of an int loop variable:

for (int i = 0; i < 3; i++) {
require(tx.outputs[i].value >= minAmount);
}

The loop is unrolled when the contract is compiled: the body is emitted once for every value of i, with i replaced by that value. This means:

  • the start and end must be integer literals, so the number of iterations is known at compile time (at most 1000);
  • the condition is <, <=, > or >=, and the update is i++ or i--;
  • the loop variable cannot be assigned in the body;
  • variables declared outside the loop can be updated in the body, e.g. to accumulate a total, and variables declared in the body only exist for one iteration;
  • every iteration adds the body's size to the contract, so large bodies or many iterations make the contract bigger. The compiler rejects a contract over the node's limits for a script template (100,000 bytes and 50,000 ops), or a contract in an MCP bundle over 10,000 bytes (the bundle runs it with OP_EXEC), since it could never be spent.

A loop can be the last statement of a function if its body ends with a require(), it runs at least once and it has no break or continue.

Break and continue​

break leaves the loop and continue skips the rest of the current iteration. Both apply to the innermost loop. Because the loop is unrolled, they are compiled into flags that the remaining code checks: after a break, the rest of the body and every later iteration are skipped, including any require() in them. A statement directly after break or continue in the same block is an error, since it could never run.

This lets the bounds of a loop be larger than the number of outputs it reads, as long as it stops at the last one:

contract FindPayment(bytes recipient) {
function spend(int minAmount) {
bool found = false;
for (int i = 0; i < 4; i++) {
if (i >= tx.outputs.length) break;
if (tx.outputs[i].lockingBytecode == recipient) {
require(tx.outputs[i].value >= minAmount);
found = true;
break;
}
}
require(found);
}
}

break and continue add a few opcodes for each iteration and for each statement they can skip. Loops that do not use them are unchanged.

Example​

contract Splitter() {
function spend(int expected) {
// the first three outputs must add up to the expected amount
int total = 0;
for (int i = 0; i < 3; i++) {
total = total + tx.outputs[i].value;
}
require(total == expected);
}
}

Comments​

Comments can be added anywhere in the contract file. Comment semantics are similar to languages like JavaScript or C. This means that single-line comments can be added with // ..., while multiline comments can be added with /* ... */.

Example​

// This is a single-line comment.

/*
This is a
multi-line comment.
*/