Spaces:
Runtime error
Runtime error
| 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>||</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. | |
| ::: |