Skip to main content

Storage and Get Methods

Summary: In previous steps we learned how to use Blueprint SDK and it's project structure.

tip

If you are stuck on some of the examples you can find original template project with all modifications performed during this guide here.

Almost all smart contracts need to store their data between transactions. This guide explains standard ways of managing storage of smart contract and using get methods to obtain it from outside the blockchain.

Smart contract storage operations

There are two main instructions that provide access to smart contract storage:

  • get_data() returning current storage cell.
  • set_data() setting storage cell.

Lets examine Cell entity structure to understand how we could manage contract storage:

Cell structure

TON blockchain uses a data structure called Cell as the fundamental unit for storing data. Cells are the building blocks of the smart contract data and have those characteristics:

  • Cell can store up to 1023 bits (approximately 128 bytes) of data
  • Cell can reference up to 4 other Cells (children's)
  • Cell is immutable once created

You can think of Cell as the following structure:

// Conceptual representation of a Cell
interface Cell {
bits: BitString; // Up to 1023 bits
refs: Cell[]; // Up to 4 child cells
}

Implementation

Let's try to modify our smart contract folowing standard steps decsribed in previous Blueprint SDK overview section.

Step 1: Edit smart contract code

In case it's inconvenient to always serialize and deserialize storage cell, there is a pretty standard practice to define two wrapper methods that provide corresponding logic. If you didn't change smart contract code it should contain following lines:

/contracts/hello_world.fc
global int ctx_id;
global int ctx_counter;

;; load_data populates storage variables using stored data
() load_data() impure {
var ds = get_data().begin_parse();

ctx_id = ds~load_uint(32);
ctx_counter = ds~load_uint(32);

ds.end_parse();
}

;; save_data stores storage variables as a cell into persistent storage
() save_data() impure {
set_data(
begin_cell()
.store_uint(ctx_id, 32)
.store_uint(ctx_counter, 32)
.end_cell()
);
}

Managing storage

Let's try to modify our example a little bit. First, let's examine different approach of managing storage, widely used in smart contract development:

Instead of initializing global variables we will pass storage members as parameters to save_data(members...) and retrieve them as (members...) get_data() moving global variables ctx_id and ctx_counter to methods bodies. Also, let's add additional integer of 256-bit size into our storage.

Result of our modifications should look like this:

/contracts/hello_world.fc
(int, int, int) load_data() {
var ds = get_data().begin_parse();

int ctx_id = ds~load_uint(32);
int ctx_counter = ds~load_uint(32);
int ctx_counter_ext = ds~load_uint(256);

ds.end_parse();

return (ctx_id, ctx_counter, ctx_counter_ext);
}

() save_data(int ctx_id, int ctx_counter, int ctx_counter_ext) impure {
set_data(
begin_cell()
.store_uint(ctx_id, 32)
.store_uint(ctx_counter, 32)
.store_uint(ctx_counter_ext, 256)
.end_cell()
);
}

Don't forget to delete global variables ctx_id, ctx_counter and modify usage of the function like this, copying storage members locally:

/contracts/hello_world.fc
;; load_data() on:
var (ctx_id, ctx_counter, ctx_counter_ext) = load_data();

;; save_data() on:
save_data(ctx_id, ctx_counter, ctx_counter_ext);

Get methods

The primary use of get methods is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a transaction.

/contracts/hello_world.fc
(int, int) get_counters() method_id {
var (_, ctx_counter, ctx_counter_ext) = load_data();
return (ctx_counter, ctx_counter_ext);
}

And that's it! In practice all get methods follow this simple flow and don't require anything more. Note that you can omit values returned from functions using _ syntax.

Final smart contract code should look like this:

/contracts/hello_world.fc
#include "imports/stdlib.fc";

const op::increase = "op::increase"c; ;; create an opcode from string using the "c" prefix, this results in 0x7e8764ef opcode in this case

(int, int, int) load_data() {
var ds = get_data().begin_parse();

int ctx_id = ds~load_uint(32);
int ctx_counter = ds~load_uint(32);
int ctx_counter_ext = ds~load_uint(256);

ds.end_parse();

return (ctx_id, ctx_counter, ctx_counter_ext);
}

() save_data(int ctx_id, int ctx_counter, int ctx_counter_ext) impure {
set_data(
begin_cell()
.store_uint(ctx_id, 32)
.store_uint(ctx_counter, 32)
.store_uint(ctx_counter_ext, 256)
.end_cell()
);
}

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
return ();
}

slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) { ;; ignore all bounced messages
return ();
}

var (ctx_id, ctx_counter, ctx_counter_ext) = load_data(); ;; here we populate the storage variables

int op = in_msg_body~load_uint(32); ;; by convention, the first 32 bits of incoming message is the op
int query_id = in_msg_body~load_uint(64); ;; also by convention, the next 64 bits contain the "query id", although this is not always the case

if (op == op::increase) {
int increase_by = in_msg_body~load_uint(32);
ctx_counter += increase_by;
save_data(ctx_id, ctx_counter, ctx_counter_ext);
return ();
}

throw(0xffff); ;; if the message contains an op that is not known to this contract, we throw
}

int get_counter() method_id {
var (_, ctx_counter, _) = load_data();
return ctx_counter;
}

int get_id() method_id {
var (ctx_id, _, _) = load_data();
return ctx_id;
}

(int, int) get_counters() method_id {
var (_, ctx_counter, ctx_counter_ext) = load_data();
return (ctx_counter, ctx_counter_ext);
}

Don't forget to check the correctness of your changes by compiling the smart contract:

npx blueprint build

Expected output should look like this:

Build script running, compiling HelloWorld

✅ Compiled successfully! Cell BOC result:

{
"hash": "310e49288a12dbc3c0ff56113a3535184f76c9e931662ded159e4a25be1fa28b",
"hashBase64": "MQ5JKIoS28PA/1YROjU1GE92yekxZi3tFZ5KJb4foos=",
"hex": "b5ee9c7241010e0100d0000114ff00f4a413f4bcf2c80b01020120020d02014803080202ce0407020120050600651b088831c02456f8007434c0cc1caa42644c383c0040f4c7f4cfcc4060841fa1d93beea5f4c7cc28163c00b817c12103fcbc2000153b513434c7f4c7f4fff4600017402c8cb1fcb1fcbffc9ed548020120090a000dbe7657800b60940201580b0c000bb5473e002b70000db63ffe002606300072f2f800f00103d33ffa40fa00d31f30c8801801cb055003cf1601fa027001cb6a82107e8764ef01cb1f12cb3f5210cb1fc970fb0013a012f0020844ca0a"
}

✅ Wrote compilation artifact to build/HelloWorld.compiled.json

Step 2: Update wrapper

Now lets update our wrapper class corresponding to new storage layout and new get method. First, let's modify helloWorldConfigToCell function and HelloWorldConfig type to properly initialize our storage during samrt-contract creation with 256-bit ctxCounterExt field that we previously added to smart contract:

/wrappers/HelloWorld.ts
export type HelloWorldConfig = {
id: number;
ctxCounter: number;
ctxCounterExt: bigint;
};

export function helloWorldConfigToCell(config: HelloWorldConfig): Cell {
return beginCell()
.storeUint(config.id, 32)
.storeUint(config.ctxCounter, 32)
.storeUint(config.ctxCounterExt, 256)
.endCell();
}

Second, add a method to perform a request for the newly created smart contract method get_counters that will retrieve both counters at once:

/HelloWorld.ts
async getCounters(provider: ContractProvider) : Promise<[number, bigint]> {
const result = await provider.get('get_counters', []);
const counter = result.stack.readNumber();
const counterExt = result.stack.readBigNumber();

return [counter, counterExt]
}

Step 3: Update tests

And finally, let's test our new functionality through previously updated wrapper:

  • Create and initialize smart contract storage members with some value through helloWorldConfig.
  • Call each of get methods and retrieve storage members.
  • Verify that they have stayed the same as during creation.

Here is an example of such implementation:

/tests/HelloWorld.spec.ts
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { Cell, toNano} from '@ton/core';
import { HelloWorld } from '../wrappers/HelloWorld';
import '@ton/test-utils';
import { compile } from '@ton/blueprint';

describe('HelloWorld', () => {
let code: Cell;

beforeAll(async () => {
code = await compile('HelloWorld');
});

let blockchain: Blockchain;
let deployer: SandboxContract<TreasuryContract>;
let helloWorld: SandboxContract<HelloWorld>;

beforeEach(async () => {
blockchain = await Blockchain.create();

helloWorld = blockchain.openContract(
HelloWorld.createFromConfig(
{
id: 0,
ctxCounter: 0,
ctxCounterExt: 0n,
},
code
)
);

deployer = await blockchain.treasury('deployer');

const deployResult = await helloWorld.sendDeploy(deployer.getSender(), toNano('1.00'));

expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: helloWorld.address,
deploy: true,
success: true,
});
});

it('should correctly initialize and return the initial data', async () => {
// Define the expected initial values (same as in beforeEach)
const expectedConfig = {
id: 0,
counter: 0,
counterExt: 0n
};

// Log the initial configuration values before verification
console.log('Initial configuration values (before deployment):');
console.log('- ID:', expectedConfig.id);
console.log('- Counter:', expectedConfig.counter);
console.log('- CounterExt:', expectedConfig.counterExt);

console.log('Retrieved values after deployment:');
// Verify counter value
const counter = await helloWorld.getCounter();
console.log('- Counter:', counter);
expect(counter).toBe(expectedConfig.counter);

// Verify ID value
const id = await helloWorld.getID();
console.log('- ID:', id);
expect(id).toBe(expectedConfig.id);

// Verify counterExt
const [_, counterExt] = await helloWorld.getCounters();
console.log('- CounterExt', counterExt);
expect(counterExt).toBe(expectedConfig.counterExt);
});

// ... previous tests
});

And now run your new test script by executing the following command:

npx blueprint test

Expected output should look like this:

# "custom log messages"

PASS tests/HelloWorld.spec.ts
HelloWorld
✓ should correctly initialize and return the initial data (431 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.591 s, estimated 6 s
Was this article useful?