Storage and get methods
Summary: In the previous steps, we learned how to use the
Blueprint
and its project structure.
If you're stuck on any of the examples, you can find the original template project with all modifications made during this guide here.
Almost all smart contracts need to store their data
between transactions. This guide explains standard ways to manage storage
for smart contracts and how to use get methods
to access it from outside the blockchain.
Smart contract storage operations
- FunC
- Tolk
There are two main instructions that provide access to smart contract storage:
get_data()
returning the current storage cell.set_data()
setting storage cell.
There are two main instructions that provide access to smart contract storage:
getContractData()
returning current storageCell
.setContractData()
setting storageCell
.
Let's examine the Cell structure to understand how to 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 smart contract data and have the following characteristics:
- A Cell can store up to 1023 bits (approximately 128 bytes) of data.
- A Cell can reference up to 4 other Cells (children).
- A Cell is immutable once created.
You can think of a 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 modify our smart contract by following the standard steps described in the previous Blueprint overview section.
Step 1: edit smart contract code
If manually serializing and deserializing the storage cell becomes inconvenient, a common practice is to define two wrapper methods that handle this logic. If you haven't modified the smart contract code, it should include the following lines in the /contracts/hello_world.fc
:
- FunC
- Tolk
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()
);
}
global ctxID: int;
global ctxCounter: int;
// loadData populates storage variables from persistent storage
fun loadData() {
var ds = getContractData().beginParse();
ctxID = ds.loadUint(32);
ctxCounter = ds.loadUint(32);
ds.assertEndOfSlice();
}
// saveData stores storage variables as a cell into persistent storage
fun saveData() {
setContractData(
beginCell()
.storeUint(ctxID, 32)
.storeUint(ctxCounter, 32)
.endCell());
}
Managing storage
Let's modify our example slightly by exploring another common storage management approach in smart contract development:
Rather than initializing global variables, we'll:
- Pass storage members as parameters via
save_data(members...)
. - Retrieve them using
(members...) = get_data()
. - Move the global variables
ctx_id
andctx_counter
into the method bodies.
Also let's add an additional 256-bit integer to our storage as ctx_counter_ext
global variable. The modified implementation should appear as follows:
- FunC
- Tolk
(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()
);
}
// load_data retrieves variables from TVM storage cell
// impure because of writting into global variables
fun loadData(): (int, int, int) {
var ds = getContractData().beginParse();
// id is required to be able to create different instances of counters
// since addresses in TON depend on the initial state of the contract
var ctxID = ds.loadUint(32);
var ctxCounter = ds.loadUint(32);
var ctxCounterExt = ds.loadUint(256);
ds.assertEndOfSlice();
return (ctxID, ctxCounter, ctxCounterExt);
}
// saveData stores storage variables as a cell into persistent storage
fun saveData(ctxID: int, ctxCounter: int, ctxCounterExt: int) {
setContractData(
beginCell()
.storeUint(ctxID, 32)
.storeUint(ctxCounter, 32)
.storeUint(ctxCounterExt, 256)
.endCell()
);
}
Remember to:
- Remove the global variables
ctx_id
andctx_counter
- Update the function usage by copying storage members locally as shown:
- FunC
- Tolk
;; 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);
// loadData() on:
var (ctxID, ctxCounter, ctxCounterExt) = loadData();
// saveData() on:
saveData(ctxID, ctxCounter, ctxCounterExt);
Get methods
The main purpose of get methods
is to provide an external read access to storage data through a convenient interface — primarily to extract the information needed for preparing transactions.
Let's add a getter function that returns both the main and extended counter values stored in the smart contract.
- FunC
- Tolk
(int, int) get_counters() method_id {
var (_, ctx_counter, ctx_counter_ext) = load_data();
return (ctx_counter, ctx_counter_ext);
}