TONTONDocs
Type system

Strings

Tolk provides a dedicated string type. At the TVM level, a string is a cell that stores data in snake format: a chain of cells where each cell holds a portion of the string and a reference to the next.

String literals

Every string literal produces a value of type string:

// string
val str = "hello";

// array<string>
val arr = ["one", "two"];

However, on the TVM, each literal is a cell:

// a cell with 40 bits of data
val str = "hello";

A literal cell is created at compile-time, there is no expensive 500-gas cell creation at runtime.

Snake format

Since a single cell can hold at most 1023 bits (127 bytes), longer strings use the snake format: the start of the string is placed in the root cell, with its continuation in a referenced cell, recursively:

val str = "very1 very2 ... very99 long";
// stored approximately as:
// "very1 ... very30"
//   -> ref "very31 ... very60"
//     -> ref "very61 ... very89"
//       -> ref "very90 ... very99 long"

Short strings may also be stored in snake format — for example, after concatenation. The logical value "abcd" and the snaked "ab" -> ref "cd" are treated identically.

Because the same logical string can have different low-level representations, comparing strings via cell hashes is incorrect. Use the string.hash method instead.

Traversing

To manually traverse contents of a string, call a beginParse() method that returns slice:

val bin = str.beginParse();  // slice
bin.remainingBitsCount();    // 40

Be careful traversing the string manually. Strings are stored in snake format, so longer strings span multiple cells linked by references.

String methods

Standard library methods for strings require an explicit import:

import "@stdlib/strings"

string.calculateLength

Loops over all references and returns the total number of characters:

val len = someStr.calculateLength();

Both "abcd" and "a" -> ref "bc" -> ref "d" return 4.

string.equalTo

Compares two strings for equality, accounting for their snaked internals:

if (option.equalTo("--disable")) {
    // ...
}

For instance, "--disable" equals "--dis" -> ref "able".

string.hash

Returns a hash of the string regardless of its internal representation. The hash of "abcd" and "a" -> ref "bcd" is identical.

Compile-time methods

Several methods operate on string literals at compile-time. Such strings are evaluated during compilation and embedded into the contract as constants.

// calculates crc32 of a string
const crc32 = "some_str".crc32() // = 4013618352 = 0xEF3AF4B0

// calculates crc16 (XMODEM) of a string
const crc16 = "some_str".crc16() // = 53407 = 0xD09F

// calculates sha256 of a string and returns 256-bit integer
const hash = "some_crypto_key".sha256()

// calculates sha256 of a string and takes the first 32 bits
const minihash = "some_crypto_key".sha256_32()

// interprets an N-chars ascii string as a number in base 256
const base256 = "AB".toBase256() // = 16706 (65*256 + 66)

// a slice with 2 bytes: 16, 32 (constructed from hex)
const hexSlice = "1020".hexToSlice()

// a slice with 4 bytes: 97, 98, 99, 100
const rawSlice = "abcd".literalSlice()

These methods work only on constant strings: calling them on variables produces a compilation error.

StringBuilder

StringBuilder from @stdlib/strings concatenates several strings into one snake-encoded string:

import "@stdlib/strings"

fun build() {
    var sb = StringBuilder.create();
    sb.append("ab");
    sb.append("cd");
    // "abcd", stored as "ab" -> ref "cd"
    var str = sb.build();
}

StringBuilder.append

The method StringBuilder.append returns self and can be chained:

var str = StringBuilder.create()
         .append("ab")
         .append("cd")
         .build();

A practical example: an NFT collection with common content and per-item content:

get fun get_nft_content(itemIndex: int, individualLink: string) {
    // ...
    var link = StringBuilder.create()
              .append(commonContent)
              .append(individualLink)
              .build();
    // ...
}

The method StringBuilder.append always concatenates strings in the snake format. This does not affect the final representation, since both "abcd" and "ab" -> ref "cd" are the same logical string.

StringBuilder.appendInt

Stores an integer in decimal format. This method is gas-expensive. It is suitable for contract getters executed off-chain, but should be avoided in on-chain production environments:

var link = StringBuilder.create()
           .append("/images/")
           .appendInt(itemIndex)
           .append(".png")
           .build();

TEP-compliant encoding

Per the token metadata standard, strings returned from get-methods must contain prefixes for off-chain explorers and APIs:

  • 0x00 + "<STRING>" — a regular snake-encoded string
  • 0x01 + "<STRING>" — an off-chain content URL

Prefixing does not create a new entity — the result remains a string, a concatenation of one character and the original content.

Standard library contains an alias: type string_prefixed0x = string. Use this alias to indicate that a string is intentionally prefixed. Because it is an alias, the value remains a string (a TVM cell) at runtime and stays opaque to clients.

Use specialized methods string.prefixWith00 and string.prefixWith01 for prefixing metadata strings:

NFT example
get fun get_nft_content(itemIndex: int, individualLink: string): string_prefixed0x {
    // ...
    return StringBuilder.create()
        .append(content.commonContent)
        .append(individualLink)
        .build()
        // to comply with TEPs off-chain content layout
        .prefixWith01()
}
Jetton example
metadata.contentDict.set("uri".sha256(), storage.metadataUri.prefixWith00());
metadata.contentDict.set("decimals".sha256(), "9".prefixWith00());

Stack layout and serialization

A string is backed by the snake-encoded TVM CELL. Serialized as a single cell reference.

Last updated on

On this page