Local Storage
Local state is associated with each account that opts into the application. Algorand smart contracts offer local storage, which enables accounts to maintain persistent key-value data. This data is accessible to authorized contracts and can be queried from external sources.
Manipulating Local State
Smart contracts can create, update, and delete values in local state. The number of values that can be written is limited by the initial configuration set during smart contract creation.
TEAL (Transaction Execution Approval Language) provides several opcodes for facilitating reading and writing to state including app_local_put
, app_local_get
and app_local_get_ex
. In addition to using TEAL, the local state values of a smart contract can be read externally using SDKs and the goal CLI. These reads are non-transactional queries that retrieve the current state of the contract.
Allocation
Local storage is allocated when an account opts into a smart contract by submitting an opt-in transaction. Each account can have between 0 and 16 key-value pairs in local storage, with a total of 2KB memory shared among them. The amount of local storage is determined during smart contract creation and cannot be edited later. The opted-in user account is responsible for funding the local storage by increasing their minimum balance requirement.
public localInt = LocalState<uint64>({ key: 'int' }) public localIntNoDefault = LocalState<uint64>() public localBytes = LocalState<bytes>() public localString = LocalState<string>() public localBool = LocalState<boolean>() public localAccount = LocalState<Account>()
def __init__(self) -> None: ## Initialise local storages self.local_int = LocalState(UInt64) # Uint64 self.local_bytes = LocalState(Bytes) # Bytes self.local_bool = LocalState(bool) # Bool self.local_asset = LocalState(Asset) # Asset self.local_application = LocalState(Application) # Application self.local_account = LocalState(Account) # Account
Reading from Local State
Local storage values are stored in the account’s balance record. Any account that sends a transaction to the smart contract can have its local storage modified by the smart contract, as long as the account has opted into the smart contract. Local storage can be read by any application call that has the smart contract’s app ID in its foreign apps array and the account in its foreign accounts array. In addition to the transaction sender, a smart contract call can reference up to four additional accounts whose local storage can be manipulated for the current smart contract, as long as those accounts have opted into the contract.
These five accounts can have their storage values read for any smart contract on Algorand by specifying the application ID of the smart contract, if the additional contract is included in the transaction’s applications array. This is a read-only operation and does not allow one smart contract to modify the local state of another. The additionally referenced accounts can be changed per smart contract call (transaction). The key-value pairs in local storage can be read on-chain directly or off-chain using SDKs and the goal CLI. Local storage is editable only by the smart contract itself, but it can be deleted by either the smart contract or the user account (using a ClearState call).
TEAL provides opcodes to read local state values for the current smart contract.
The app_local_get
opcode retrieves values from the current contract’s local storage.
The app_local_get_ex
opcode returns two values on the stack: a boolean
indicating whether the value was found, and the actual value
if it exists.
The _ex opcodes allow reading local states from other accounts and smart contracts, as long as the account and contract are included in the accounts and applications arrays. Branching logic is typically used after calling the _ex opcodes to handle cases where the value is found or not found.
/** * Reads and returns all local state values for the transaction sender. * @returns A tuple containing: * - [0] uint64: The value of localInt * - [1] uint64: The value of localIntNoDefault * - [2] bytes: The value of localBytes * - [3] string: The value of localString * - [4] boolean: The value of localBool * - [5] Address: The value of localAccount converted to Address type */ public readLocalState(): [uint64, uint64, bytes, string, boolean, arc4.Address] { const sender = Txn.sender // Convert Account reference type to native Address type for return value const accountAddress = new arc4.Address(this.localAccount(sender).value)
return [ this.localInt(sender).value, this.localIntNoDefault(sender).value, this.localBytes(sender).value, this.localString(sender).value, this.localBool(sender).value, accountAddress, ] }
@arc4.abimethod def get_item_local_data(self, for_account: Account) -> UInt64: return self.local_int[for_account]
# get function @arc4.abimethod def get_local_data_with_default_int(self, for_account: Account) -> UInt64: return self.local_int.get(for_account, default=UInt64(0)) # Uint64
# maybe function @arc4.abimethod def maybe_local_data(self, for_account: Account) -> tuple[UInt64, bool]: # used to get data or assert int result, exists = self.local_int.maybe(for_account) # Uint64 if not exists: result = UInt64(0) return result, exists
#pragma version 10
get_item_local_data: proto 1 1 frame_dig -1 int 0 byte "local_int" app_local_get_ex assert retsub
get_local_data_with_default_int: proto 1 1 frame_dig -1 int 0 byte "local_int" app_local_get_ex int 0 cover 2 select retsub
maybe_local_data: proto 1 2 frame_dig -1 int 0 byte "local_int" app_local_get_ex dup uncover 2 swap bnz maybe_local_data_after_if_else@2 int 0 frame_bury 1
maybe_local_data_after_if_else@2: frame_dig 1 frame_dig 0 uncover 3 uncover 3 retsub
Refer Sinppet Source to get local storage value for different data types
The local state values of a smart contract can also be read externally using goal CLI. The below command reads are non-transactional queries that retrieve the current state of the contract.
Example command:
goal app read --app-id 1 --guess-format --local --from <ADDRESS>
This command will return the local state for the account specified by --from
.
Example output with 3 key-value pairs
{ "Creator": { "tb": "FRYCPGH25DHCYQGXEB54NJ6LHQG6I2TWMUV2P3UWUU7RWP7BQ2BMBBDPD4", "tt": 1 }, "MyBytesKey": { "tb": "hello", "tt": 1 }, "MyUintKey": { "tt": 2, "ui": 50 }}
Interpretation:
- The keys are
Creator
,MyBytesKey
, andMyUintKey
. - The
tt
field indicates the type of the value: 1 for byte slices (byte-array values), 2 for uint64 values. - When
tt=1
, the value is stored in thetb
field. The--guess-format
option automatically converts theCreator
value to an Algorand address with a checksum (instead of displaying the raw 32-byte public key). - When
tt=2
, the value is stored in theui
field.
Writing to Local State
To write to local state, use the app_local_put
opcode. An additional account parameter is provided to specify which account’s local storage should be modified.
/** * Updates multiple local state values for the transaction sender. * Requires the account to be opted into the application. * @param valueString - New string value to store * @param valueBool - New boolean value to store * @param valueAccount - New account address to store */ public writeLocalState(valueString: string, valueBool: boolean, valueAccount: Account): void { // Dynamic keys must be explicitly reserved in the contract's stateTotals configuration const sender = Txn.sender
assert(sender.isOptedIn(Global.currentApplicationId), 'Account must opt in to contract first')
this.localString(sender).value = valueString this.localBool(sender).value = valueBool this.localAccount(sender).value = valueAccount
assert(this.localString(sender).value === valueString) assert(this.localBool(sender).value === valueBool) assert(this.localAccount(sender).value === valueAccount) }
@arc4.abimethod def set_local_int(self, for_account: Account, value: UInt64) -> None: self.local_int[for_account] = value # Uint64
#pragma version 10
set_local_int: proto 2 0 frame_dig -2 byte "local_int" frame_dig -1 app_local_put retsub
Refer Sinppet Source to set local storage value for different data types
Deletion of Local State
Deleting a smart contract does not affect its local storage. Accounts must clear out of the smart contract to recover their minimum balance. Every smart contract has an ApprovalProgram and a ClearStateProgram. An account holder can clear their local state for a contract at any time by executing a ClearState transaction, deleting their data and freeing up their locked minimum balance. An account can request to clear its local state using a closeout transaction or clear its local state for a specific contract using a clearstate transaction, which will always succeed, even after the contract is deleted.
/** * Clears all local state values for the transaction sender. * After calling this method, all local state values will be deleted. */ public clearLocalState(): void { const sender = Txn.sender
assert(sender.isOptedIn(Global.currentApplicationId), 'Account must opt in to contract first')
this.localInt(sender).delete() this.localIntNoDefault(sender).delete() this.localBytes(sender).delete() this.localString(sender).delete() this.localBool(sender).delete() this.localAccount(sender).delete() }
@arc4.abimethod def delete_local_data(self, for_account: Account) -> None: del self.local_account[for_account] # Uint64
#pragma version 10
delete_local_data: proto 1 0 frame_dig -1 byte "local_account" app_local_del err
Refer Sinppet Source to delete local storage value for different data types
Summary of Local State Operations
For manipulating local storage data like reading, writing, deleting and checking if exists:
TEAL: Different opcodes can be used
Function | Description |
---|---|
app_local_get | Get local data for the current app |
app_local_get_ex | Get local data for other app |
app_local_put | Set local data to the current app |
app_local_del | Delete local data from the current app |
app_local_get_ex | Check if local data exists for the current app |
app_local_get_ex | Check if local data exists for the other app |
Different functions of LocalState class can be used. The detailed api reference can be found here
Function | Description |
---|---|
LocalState(type_) | Initialize a local state with the specified data type |
getitem(account) | Get data for the given account |
get(account, default) | Get data for the given account, or a default value if not found |
maybe(account) | Get data for the given account, and a boolean indicating if it exists |
setitem(account, value) | Set data for the given account |
delitem(account) | Delete data for the given account |
contains(account) | Check if data exists for the given account |