scrypt / how-to-write-a-contract.md
codenlighten's picture
Upload 34 files
711e9c6
---
sidebar_position: 1
---
# How to Write a Contract
A smart contract is a class that extends the `SmartContract` base class. A simple example is shown below.
```ts
import { SmartContract, method, prop, assert } from "scrypt-ts"
class Demo extends SmartContract {
@prop()
readonly x: bigint
constructor(x: bigint) {
super(...arguments)
this.x = x
}
@method()
public unlock(x: bigint) {
assert(this.add(this.x, 1n) == x, 'incorrect sum')
}
@method()
add(x0: bigint, x1:bigint) : bigint {
return x0 + x1
}
}
```
Class members decorated with `@prop` and `@method` will end up on the blockchain and thus must be a strict subset of TypeScript. Everywhere decorated with them can be regarded in the on-chain context. Members decorated with neither are regular TypeScript and are kept off chain. The significant benefit of `sCrypt` is that both on-chain and off-chain code are written in the same language: TypeScript.
:::note
You can use [the sCrypt template Repl](https://replit.com/@msinkec/sCrypt) and play with the code in your browser!
:::
## Properties
A smart contract can have two kinds of properties:
1. With `@prop` decorator: these properties are **only allowed to have [types](#data-types) specified below** and they shall only be initialized in the constructor.
2. Without `@prop` decorator: these properties are regular TypeScript properties without any special requirement, meaning they can use any types. Accessing these properties is prohibited in methods decorated with the `@method` decorator.
### `@prop` decorator
Use this decorator to mark any property that intends to be stored on chain.
This decorator takes a `boolean` parameter. By default, it is set to `false`, meaning the property cannot be changed after the contract is deployed. If the value is `true`, the property is a so-called [stateful](./stateful-contract) property and its value can be updated in subsequent contract calls.
```ts
// good, `a` is stored on chain, and it's readonly after the contract is deployed
@prop()
readonly a: bigint
// valid, but not good enough, `a` cannot be changed after the contract is deployed
@prop()
a: bigint
// good, `b` is stored on chain, and its value can be updated in subsequent contract calls
@prop(true)
b: bigint
// invalid, `b` is a stateful property that cannot be readonly
@prop(true)
readonly b: bigint
// good
@prop()
static c: bigint = 1n
// invalid, static property must be initialized when declared
@prop()
static c: bigint
// invalid, stateful property cannot be static
@prop(true)
static c: bigint = 1n
// good, `UINT_MAX` is a compile-time constant, and no need to typed explicitly
static readonly UINT_MAX = 0xffffffffn
// valid, but not good enough, `@prop()` is not necessary for the CTC
@prop()
static readonly UINT_MAX = 0xffffffffn
// invalid
@prop(true)
static readonly UINT_MAX = 0xffffffffn
```
## Constructor
A smart contract must have an explicit constructor if it has at least one `@prop` that is not `static`.
The `super` method **must** be called in the constructor and all the arguments of the constructor should be passed to `super`
in the same order as they are passed into the constructor. For example,
```ts
class A extends SmartContract {
readonly p0: bigint
@prop()
readonly p1: bigint
@prop()
readonly p2: boolean
constructor(p0: bigint, p1: bigint, p2: boolean) {
super(...arguments) // same as super(p0, p1, p2)
this.p0 = p0
this.p1 = p1
this.p2 = p2
}
}
```
[`arguments`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) is an array containing the values of the arguments passed to that function. `...` is the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax).
## Methods
Like properties, a smart contract can also have two kinds of methods:
1. With `@method` decorator: these methods can only call **methods also decorated by `@method` or [functions](#functions) specified below**. Also, **only the properties decorated by `@prop` can be accessed**.
2. Without `@method` decorator: these methods are just regular TypeScript class methods.
### `@method` decorator
1. Use this decorator to mark any method that intends to run on chain.
2. It takes a [sighash flag](./scriptcontext.md#sighash-type) as a parameter.
### Public `@method`s
Each contract **must** have at least one public `@method`. It is denoted with the `public` modifier and does not return any value. It is visible outside the contract and acts as the main method into the contract (like `main` in C and Java).
A public `@method` can be called from an external transaction. The call succeeds if it runs to completion without violating any conditions in [assert()](./built-ins.md#assert). An example is shown below.
```ts
@method()
public unlock(x: bigint) {
// only succeeds if x is 1
assert(this.add(this.x, 1n) == x, "unequal")
}
```
:::note
The last function call of a public `@method` method **must** be an `assert()` function call, unless it is a `console.log()` call.
:::
```ts
class PublicMethodDemo extends SmartContract {
@method()
public foo() {
// invalid, the last statement of public method should be an `assert` function call
}
@method()
public bar() {
assert(true);
return 1n; // invalid, because a public method cannot return any value
}
@method()
public foobar() {
console.log();
// valid, `console.log` calling will be ignored when verifying the last `assert` statement
assert(true);
console.log();
console.log();
}
}
```
### Non-public `@method`s
Without a `public` modifier, a `@method` is internal and cannot be directly called from an external transaction.
```ts
@method()
add(x0: bigint, x1:bigint) : bigint {
return x0 + x1
}
```
:::note
**Recursion is disallowed**. A `@method`, public and not, cannot call itself, either directly in its own body or indirectly calls another method that transitively calls itself.
:::
```ts
class MethodsDemo extends SmartContract {
@prop()
readonly x: bigint;
@prop()
readonly y: bigint;
constructor(x: bigint, y: bigint) {
super(...arguments);
this.x = x;
this.y = y;
}
// good, non-public static method without access `@prop` properties
@method()
static sum(a: bigint, b: bigint): bigint {
return a + b;
}
// good, non-public method
@method()
xyDiff(): bigint {
return this.x - this.y
}
// good, public method
@method()
public add(z: bigint) {
// good, call `sum` with the class name
assert(z == MethodsDemo.sum(this.x, this.y), 'add check failed');
}
// good, another public method
@method()
public sub(z: bigint) {
// good, call `xyDiff` with the class instance
assert(z == this.xyDiff(), 'sub check failed');
}
// valid but bad, public static method
@method()
public static alwaysPass() {
assert(true)
}
}
```
## Data Types
Types used in `@prop` and `@method` are restricted to these kinds:
### Basic Types
#### boolean
A simple value `true` or `false`.
```ts
let isDone: boolean = false
```
#### `bigint`
`bigint` can represent arbitrarily large integers. A [bigint literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) is a number with suffix `n`:
```ts
11n
0x33FEn
const previouslyMaxSafeInteger = 9007199254740991n
const alsoHuge = BigInt(9007199254740991)
// 9007199254740991n
const hugeHex: bigint = BigInt("0x1fffffffffffff")
// 9007199254740991n
```
#### `ByteString`
In a smart contract context (i.e., in `@method`s or `@prop`s), a `ByteString` represents a byte array.
A literal `string` can be converted in to a `ByteString` using function `toByteString(literal: string, isUtf8: boolean = false): ByteString`:
* If not passing `isUtf8` or `isUtf8` is `false`, then `literal` should be in the format of hex literal, which can be represented by the regular expression: `/^([0-9a-fA-F]{2})*$/`
* Otherwise, `literal` should be in the format of utf8 literal, e.g., `hello world`.
:::note
`toByteString` **ONLY** accepts string literals for its first argument, and boolean literals for the second.
:::
```ts
let a = toByteString('0011') // valid, `0011` is a valid hex literal
// 0011
let b = toByteString('hello world', true) // valid
// 68656c6c6f20776f726c64
toByteString('0011', false) // valid
// 30303131
toByteString(b, true) // invalid, not passing string literal to the 1st parameter
toByteString('001') // invalid, `001` is not a valid hex literal
toByteString('hello', false) // invalid, `hello` is not a valid hex literal
toByteString('hello', 1 === 1) // invalid, not passing boolean literal to the 2nd parameter
let c = true
toByteString('world', c) // invalid, not passing boolean literal to the 2nd parameter
```
`ByteString` has the following operators and methods:
* `==` / `===`: compare
* `+`: concatenate
```ts
const str0 = toByteString('01ab23ef68')
const str1 = toByteString('656c6c6f20776f726c64')
// comparison
str0 == str1
str0 === str1
// false
// concatenation
str0 + str1
// '01ab23ef68656c6c6f20776f726c64'
```
#### `number`
Type `number` is not allowed in `@prop`s and `@method`s, except in the following cases. We can use `Number()` function to convert `bigint` to `number`.
* Array index
```ts
let arr: FixedArray<bigint, 3> = [1n, 3n, 3n]
let idx: bigint = 2n
let item = arr[Number(idx)]
```
* Loop variable
``` ts
for (let i: number = 0 i < 10 i++) {
let j: bigint = BigInt(i) // convert number to bigint
}
```
It can also be used in defining [compile-time constants](#compile-time-constant).
### Fixed Size Array
All arrays **must** be of fixed size and be declared as type of `FixedArray<T, SIZE>`, whose `SIZE` must be a [CTC](#compile-time-constant) described later.
The common TypeScript arrays declared as `T[]` or `Array<T>` are not allowed in `@prop`s and `@method`s, as they are of dynamic size.
```ts
let aaa: FixedArray<bigint, 3> = [1n, 3n, 3n]
// set to all 0s
const N = 20
let aab: FixedArray<bigint, N> = fill(0n, N)
// 2-dimensional array
let abb: FixedArray<FixedArray<bigint, 2>, 3> = [[1n, 3n], [1n, 3n], [1n, 3n]]
```
:::caution
A `FixedArray` behaves differently in an on-chain and off-chain context, when passed as a function argument. It is *passed by reference* off chain, as a regular TypeScript/JavaScript array, while *passed by value* on chain. It is thus strongly recommended to NEVER mutate a `FixedArray` parameter's element inside a function.
```ts
class DemoContract extends SmartContract {
@prop(true)
readonly a: FixedArray<bigint, 3>
constructor(a: FixedArray<bigint, 3>) {
super(...arguments)
this.a = a
}
@method()
onchainChange(a: FixedArray<bigint, 3>) {
a[0] = 0
}
offchainChange(a: FixedArray<bigint, 3>) {
a[0] = 0
}
@method()
public main(a: FixedArray<bigint, 3>) {
this.onchainChange(this.a)
// note: a[0] is not changed on chain
assert(this.a[0] == 1n)
}
}
const arrayA: FixedArray<bigint, 3> = [1n, 2n, 3n]
const instance = new DemoContract(arrayA);
instance.offchainChange(arrayA)
// note: arrayA[0] is changed off chain
assert(arrayA[0] = 0n)
```
:::
### User-defined Types
Users can be define customized types using `type` or `interface`, made of basic types.[^1]
```ts
type ST = {
a: bigint
b: boolean
}
interface ST1 {
x: ST
y: ByteString
}
type Point = {
x: number
y: number
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x)
console.log("The coordinate's y value is " + pt.y)
}
interface Point2 {
x: number
y: number
}
// Exactly the same as the earlier example
function printCoord(pt: Point2) {
console.log("The coordinate's x value is " + pt.x)
console.log("The coordinate's y value is " + pt.y)
}
```
[^1]: A user-defined type is also passed by value on chain, and by reference off chain, same as a `FixedArray`. It is thus strongly recommended to NEVER mutate the field of a parameter, which is of a user-defined type, inside a function.
### Domain Types
There are several domain types, specific to the Bitcoin context, used to further improve type safety. They are all subtypes of `ByteString`. That is, they can be used where a `ByteString` is expected, but not vice versa.
* `PubKey` - a public key
* `Sig` - a signature type in [DER format](https://academy.bit2me.com/en/que-son-firmas-estrictas-der), including sighash flags at the end
* `Ripemd160` - a RIPEMD-160 hash
* `PubKeyHash` - an alias for `Ripemd160`, usually representing a bitcoin address.
* `Sha1` - a SHA-1 hash
* `Sha256` - a SHA-256 hash
* `SigHashType` - a sighash
* `SigHashPreimage` - a sighash preimage
* `OpCodeType` - a Script [opcode](https://wiki.bitcoinsv.io/index.php/Opcodes_used_in_Bitcoin_Script)
```ts
@method()
public unlock(sig: Sig, pubkey: PubKey) {
// hash160() takes a ByteString as input, but can accept pubkey here, which if of type PubKey
assert(hash160(pubkey) == this.pubKeyHash)
assert(this.checkSig(sig, pubkey), 'signature check failed')
}
```
## Statements
There are some constraints on these following statements within `@method`s, except [variable declarations](#Variable-declarations).
### Variable declarations
Variables can be declared in `@method`s by keywords `const` / `var` / `let`, like in normal TypeScript.
```ts
let a : bigint = 1n
var b: boolean = false
const byte: ByteString = toByteString("ff")
```
### `for`
Bitcoin does not allow unbounded loops for security reasons, to prevent DoS attacks. All loops must be bounded at compile time. So if you want to loop inside `@method`, you must strictly use the following format:
```ts
for (let $i = 0; $i < $maxLoopCount; $i++) {
...
}
```
:::note
* the initial value must be `0` or `0n`, the operator `<` (no `<=`), and increment `$i++` (no pre-increment `++$i`).
* `$maxLoopCount` must be a [CTC](#compile-time-constant).
* `$i` can be arbitrary name, e.g., `i`, `j`, or `k`. It can be both a `number` or a `bigint` type.
* `break` and `continue` are currently not allowed, but can be emulated like
:::
```ts
// emulate break
let x = 3n
let done = false
for (let i = 0; i < 3; i++) {
if (!done) {
x = x * 2n
if (x >= 8n) {
done = true
}
}
}
```
### `return`
Due to the lack of native return semantics support in Bitcoin Script, a function currently must end with a `return` statement and it is the only valid place for a `return` statement. This requirement may be relaxed in the future.
```ts
@method() m(x: bigint): bigint {
if (x > 2n) return x // invalid
return x + 1n // valid
}
```
This is usually not a problem since it can be circumvented as follows:
```ts
@method()
abs(a: bigint): bigint {
if (a > 0) {
return a
} else {
return -a
}
}
```
can be rewritten as
```ts
@method()
abs(a: bigint): bigint {
let ret : bigint = 0
if (a > 0) {
ret = a
} else {
ret = -a
}
return ret
}
```
## Compile-time Constant
A compile-time constant, CTC for short, is a special variable whose value can be determined at compile time. A CTC must be defined in one of the following ways.
* A number literal like:
```ts
3
```
* A `const` variable, whose value must be a numeric literal. Expressions cannot be used for now.
```ts
const N1 = 3 // valid
const N2: number = 3 // invalid, no explicit type `number` allowed
const N3 = 3 + 3 // invalid, no expression allowed
```
* A `static` `readonly` property:
```ts
class X {
static readonly M1 = 3 // valid
static readonly M2: number = 3 // invalid
static readonly M3 = 3 + 3 // invalid
}
```
A CTC is required in these cases.
* Array size
```ts
let arr1: FixedArray<bigint, 3> = [1n, 2n, 3n]
// `typeof` is needed since FixedArray takes a type as the array size, not a value
let arr1: FixedArray<bigint, typeof N1> = [1n, 2n, 3n]
let arr2: FixedArray<bigint, typeof X.M1> = [1n, 2n, 3n]
```
* Loop count in `for` statement
```ts
for(let i=0; i< 3; i++) {}
for(let i=0; i< N1; i++) {}
for(let i=0; i< X.M1; i++) {}
```
## Functions
### Built-in Functions
You can refer to [Built-ins](./built-ins.md) for a full list of functions and libraries built into `scryptTS`.
### Whitelisted Functions
Be default, all Javascript/TypeScript built-in functions and global variables are not allowed in `@method`s, except the following kinds.
#### `console.log`
`console.log` can be used for debugging purposes.
```ts
@method()
add(x0: bigint, x1:bigint) : bigint {
console.log(x0)
return x0 + x1
}
```
## Operators
**sCrypt** is a subset of TypeScript. Only the following operators can be used directly.
| Operator | Description |
| :-----| :----: |
| `+` | Addition |
| `-` | Subtraction |
| `*` | Multiplication |
| `/` | Division |
| `%` | Remainder |
| `++` | Increment |
| `--` | Decrement |
| `==` | Equal to |
| `!=` | Not equal to |
| `===` | Same as `==` |
| `!==` | Same as `!=` |
| `>` | Greater than |
| `>=` | Greater than or equal to |
| `<` | Less than |
| `<=` | Less than or equal to |
| `&&` | Logical AND |
| <code>&#124;&#124;</code> | Logical OR |
| `!` | Logical NOT |
| `cond ? expr1 : expr2 ` | ternary |
| `+=` | Add and assign |
| `-=` | Subtract and assign |
| `*=` | Multiply and assign |
| `/=` | Divide and assign |
| `%=` | Assign remainder |
:::note
`**` is not supported currently.
:::