Memory maps provide a convenient interface for address-keyed storage with automatic type handling. They're the recommended way to implement Solidity-style mapping(address => T) patterns.
import {
AddressMemoryMap,
Blockchain,
Address,
} from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Allocate storage pointer
private balancesPointer: u16 = Blockchain.nextPointer;
// Create memory map
private balances: AddressMemoryMap;
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}
// Usage
const balance = this.balances.get(userAddress);
this.balances.set(userAddress, newBalance);The primary memory map type for address-keyed storage. It stores and returns u256 values directly.
class AddressMemoryMapprivate balancesPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}// Get value for address
public get(key: Address): u256
// Set value for address (returns this for chaining)
public set(key: Address, value: u256): this
// Get raw bytes
public getAsUint8Array(key: Address): Uint8Array
// Set raw bytes
public setAsUint8Array(key: Address, value: Uint8Array): this
// Check if key has non-default value
public has(key: Address): bool
// Delete (set to default, returns true if key existed)
public delete(key: Address): boolWhen you access an AddressMemoryMap, the address is converted to a storage key via SHA256:
---
config:
theme: dark
---
flowchart LR
subgraph map["AddressMemoryMap Instance"]
A["pointer: u16<br/>Storage base pointer"]
B["Internal Map<br/>Address -> u256"]
end
subgraph addrops["Address Key Operations"]
C["User Address"]
D["Address.toBytes()"]
E["32-byte key"]
end
subgraph keygen["Storage Key Generation"]
F["encodePointer()"]
G["pointer + address bytes"]
H["SHA256 hash"]
I["32-byte storage key"]
end
C --> D
D --> E
A --> F
E --> F
F --> G
G --> H
H --> I
I --> J[("Blockchain Storage")]
The complete flow from address to storage access:
flowchart LR
subgraph input["Input"]
A["Address<br/>0x1234...abcd"]
end
subgraph getaddr["AddressMemoryMap.get(address)"]
B["toBytes()"]
C["32-byte Uint8Array"]
D["encodePointer()<br/>pointer + addressBytes"]
end
subgraph storage["Storage Access"]
E["32-byte storage key"]
F["Blockchain.getStorageAt()"]
G["Raw bytes from storage"]
H["u256.fromUint8ArrayBE()"]
end
subgraph output["Output"]
I["u256 value<br/>or u256.Zero if not set"]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
| Solidity | OP_NET AddressMemoryMap |
|---|---|
mapping(address => uint256) |
AddressMemoryMap |
balances[addr] |
balances.get(addr) |
balances[addr] = val |
balances.set(addr, val) |
Default value: 0 |
Default value: u256.Zero |
| Implicit initialization | Explicit constructor initialization |
| No existence check | balances.has(addr) available |
delete balances[addr] |
balances.delete(addr) |
| Operation | Solidity | OP_NET |
|---|---|---|
| Declare | mapping(address => uint256) public balances; |
private balances: AddressMemoryMap; |
| Initialize | Automatic | this.balances = new AddressMemoryMap(this.balancesPointer); |
| Read | balances[addr] |
balances.get(addr) |
| Write | balances[addr] = amount; |
balances.set(addr, amount) |
| Add to value | balances[addr] += amount; |
balances.set(addr, SafeMath.add(balances.get(addr), amount)) |
| Subtract | balances[addr] -= amount; |
balances.set(addr, SafeMath.sub(balances.get(addr), amount)) |
| Check non-zero | balances[addr] > 0 |
!balances.get(addr).isZero() |
| Delete/reset | delete balances[addr]; |
balances.delete(addr) or balances.set(addr, u256.Zero) |
| Check exists | N/A (always 0 default) | balances.has(addr) |
| Pattern | Solidity | OP_NET |
|---|---|---|
| Transfer balance | balances[from] -= amt; balances[to] += amt; |
balances.set(from, SafeMath.sub(balances.get(from), amt)); balances.set(to, SafeMath.add(balances.get(to), amt)); |
| Check sufficient | require(balances[addr] >= amount); |
if (balances.get(addr) < amount) throw new Revert("Insufficient"); |
| Mint tokens | balances[to] += amount; |
balances.set(to, SafeMath.add(balances.get(to), amount)); |
| Burn tokens | balances[from] -= amount; |
balances.set(from, SafeMath.sub(balances.get(from), amount)); |
| Zero balance check | balances[addr] == 0 |
balances.get(addr).isZero() |
| Get sender balance | balances[msg.sender] |
balances.get(Blockchain.tx.sender) |
| Aspect | Solidity | OP_NET |
|---|---|---|
| Key type | address (20 bytes) |
Address (32 bytes) |
| Value type | Any | u256 only |
| Storage slot | keccak256(key . slot) |
SHA256(pointer + address) |
| Reentrancy safe | Developer responsibility | Developer responsibility |
| Arithmetic | Native operators | SafeMath required |
| ERC-20 Function | Solidity | OP_NET |
|---|---|---|
balanceOf(address) |
return balances[owner]; |
return this.balances.get(owner); |
transfer(to, amount) |
balances[msg.sender] -= amount; balances[to] += amount; |
this.balances.set(sender, SafeMath.sub(...)); this.balances.set(to, SafeMath.add(...)); |
approve(spender, amount) |
allowances[msg.sender][spender] = amount; |
Use MapOfMap<u256> for nested mapping |
For a complete token implementation using AddressMemoryMap, see Basic Token Example.
Solidity:
contract TokenBalances {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function mint(address to, uint256 amount) external {
balances[to] += amount;
totalSupply += amount;
}
function burn(address from, uint256 amount) external {
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
totalSupply -= amount;
}
function transfer(address from, address to, uint256 amount) external {
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
balances[to] += amount;
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}OP_NET:
@final
export class TokenBalances extends OP_NET {
private balancesPointer: u16 = Blockchain.nextPointer;
private totalSupplyPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
private _totalSupply: StoredU256 = new StoredU256(this.totalSupplyPointer, EMPTY_POINTER);
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}
public mint(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
this._totalSupply.value = SafeMath.add(this._totalSupply.value, amount);
return new BytesWriter(0);
}
public burn(calldata: Calldata): BytesWriter {
const from = calldata.readAddress();
const amount = calldata.readU256();
const balance = this.balances.get(from);
if (balance < amount) {
throw new Revert('Insufficient balance');
}
this.balances.set(from, SafeMath.sub(balance, amount));
this._totalSupply.value = SafeMath.sub(this._totalSupply.value, amount);
return new BytesWriter(0);
}
public transfer(calldata: Calldata): BytesWriter {
const from = calldata.readAddress();
const to = calldata.readAddress();
const amount = calldata.readU256();
const fromBalance = this.balances.get(from);
if (fromBalance < amount) {
throw new Revert('Insufficient balance');
}
this.balances.set(from, SafeMath.sub(fromBalance, amount));
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
return new BytesWriter(0);
}
public balanceOf(calldata: Calldata): BytesWriter {
const account = calldata.readAddress();
const writer = new BytesWriter(32);
writer.writeU256(this.balances.get(account));
return writer;
}
public totalSupply(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._totalSupply.value);
return writer;
}
}Solidity:
contract Staking {
mapping(address => uint256) public stakedAmount;
mapping(address => uint256) public stakedTimestamp;
mapping(address => uint256) public rewards;
function stake(uint256 amount) external {
stakedAmount[msg.sender] += amount;
stakedTimestamp[msg.sender] = block.timestamp;
}
function unstake(uint256 amount) external {
require(stakedAmount[msg.sender] >= amount, "Not enough staked");
stakedAmount[msg.sender] -= amount;
}
function claimRewards() external {
uint256 reward = calculateReward(msg.sender);
rewards[msg.sender] = 0;
// Transfer reward...
}
function calculateReward(address user) public view returns (uint256) {
uint256 duration = block.timestamp - stakedTimestamp[user];
return stakedAmount[user] * duration / 365 days;
}
function getStakeInfo(address user) external view returns (uint256, uint256, uint256) {
return (stakedAmount[user], stakedTimestamp[user], rewards[user]);
}
}OP_NET:
@final
export class Staking extends OP_NET {
private stakedAmountPointer: u16 = Blockchain.nextPointer;
private stakedTimestampPointer: u16 = Blockchain.nextPointer;
private rewardsPointer: u16 = Blockchain.nextPointer;
private stakedAmount: AddressMemoryMap;
private stakedTimestamp: AddressMemoryMap;
private rewards: AddressMemoryMap;
constructor() {
super();
this.stakedAmount = new AddressMemoryMap(this.stakedAmountPointer);
this.stakedTimestamp = new AddressMemoryMap(this.stakedTimestampPointer);
this.rewards = new AddressMemoryMap(this.rewardsPointer);
}
public stake(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
this.stakedAmount.set(sender, SafeMath.add(this.stakedAmount.get(sender), amount));
this.stakedTimestamp.set(sender, u256.fromU64(Blockchain.block.medianTime));
return new BytesWriter(0);
}
public unstake(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
const staked = this.stakedAmount.get(sender);
if (staked < amount) {
throw new Revert('Not enough staked');
}
this.stakedAmount.set(sender, SafeMath.sub(staked, amount));
return new BytesWriter(0);
}
public claimRewards(_calldata: Calldata): BytesWriter {
const sender = Blockchain.tx.sender;
const reward = this.calculateReward(sender);
this.rewards.set(sender, u256.Zero);
// Transfer reward...
const writer = new BytesWriter(32);
writer.writeU256(reward);
return writer;
}
private calculateReward(user: Address): u256 {
const timestamp = this.stakedTimestamp.get(user);
const currentTime = u256.fromU64(Blockchain.block.medianTime);
const duration = SafeMath.sub(currentTime, timestamp);
const staked = this.stakedAmount.get(user);
// Simplified: staked * duration / YEAR_IN_SECONDS
const YEAR_SECONDS = u256.fromU64(31536000);
return SafeMath.div(SafeMath.mul(staked, duration), YEAR_SECONDS);
}
public getStakeInfo(calldata: Calldata): BytesWriter {
const user = calldata.readAddress();
const writer = new BytesWriter(96);
writer.writeU256(this.stakedAmount.get(user));
writer.writeU256(this.stakedTimestamp.get(user));
writer.writeU256(this.rewards.get(user));
return writer;
}
}@final
export class Token extends OP_NET {
private balancesPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}
public balanceOf(calldata: Calldata): BytesWriter {
const account = calldata.readAddress();
const balance = this.balances.get(account);
const writer = new BytesWriter(32);
writer.writeU256(balance);
return writer;
}
public _transfer(from: Address, to: Address, amount: u256): void {
const fromBalance = this.balances.get(from);
if (fromBalance < amount) {
throw new Revert('Insufficient balance');
}
this.balances.set(from, SafeMath.sub(fromBalance, amount));
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
}
}import { encodePointer } from '@btc-vision/btc-runtime/runtime';
// Nested mapping: owner => (spender => amount)
// Using composite storage
private allowancesPointer: u16 = Blockchain.nextPointer;
// For nested maps, create helper methods
private getAllowance(owner: Address, spender: Address): u256 {
const subPointer = this.computeAllowanceKey(owner, spender);
const pointerHash = encodePointer(this.allowancesPointer, subPointer.toUint8Array(true));
const stored = Blockchain.getStorageAt(pointerHash);
return u256.fromUint8ArrayBE(stored);
}
private setAllowance(owner: Address, spender: Address, amount: u256): void {
const subPointer = this.computeAllowanceKey(owner, spender);
const pointerHash = encodePointer(this.allowancesPointer, subPointer.toUint8Array(true));
Blockchain.setStorageAt(pointerHash, amount.toUint8Array(true));
}
private computeAllowanceKey(owner: Address, spender: Address): u256 {
const combined = new Uint8Array(64);
combined.set(owner.toBytes(), 0);
combined.set(spender.toBytes(), 32);
return u256.fromBytes(Blockchain.sha256(combined));
}// Track staked amount and timestamp per user
private stakedAmountPointer: u16 = Blockchain.nextPointer;
private stakedTimePointer: u16 = Blockchain.nextPointer;
private stakedAmount: AddressMemoryMap;
private stakedTime: AddressMemoryMap;
constructor() {
super();
this.stakedAmount = new AddressMemoryMap(this.stakedAmountPointer);
this.stakedTime = new AddressMemoryMap(this.stakedTimePointer);
}
public stake(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
// Update staked amount
const current = this.stakedAmount.get(sender);
this.stakedAmount.set(sender, SafeMath.add(current, amount));
// Update stake time
this.stakedTime.set(sender, u256.fromU64(Blockchain.block.medianTime));
return new BytesWriter(0);
}
public getStakeInfo(calldata: Calldata): BytesWriter {
const user = calldata.readAddress();
const writer = new BytesWriter(64);
writer.writeU256(this.stakedAmount.get(user));
writer.writeU256(this.stakedTime.get(user));
return writer;
}// AddressMemoryMap wraps persistent storage
// Changes persist across transactions
public deposit(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
const current = this.deposits.get(sender); // Reads from storage
this.deposits.set(sender, SafeMath.add(current, amount)); // Writes to storage
return new BytesWriter(0);
}// For temporary collections within a single call
// Use standard AssemblyScript Map
public processAddresses(calldata: Calldata): BytesWriter {
const addresses = calldata.readAddressArray();
// Temporary map for deduplication
const seen = new Map<string, bool>();
for (let i = 0; i < addresses.length; i++) {
const addrStr = addresses[i].toBytes().toString();
if (seen.has(addrStr)) {
continue; // Skip duplicate
}
seen.set(addrStr, true);
// Process unique address...
}
return new BytesWriter(0);
}When working with persistent storage, always use the btc-runtime Map:
---
config:
theme: dark
---
flowchart LR
A["WRONG:<br/>AssemblyScript Map"] --> B["NOT blockchain-optimized"]
B --> C["Broken comparisons"]
C --> D["Data corruption"]
E["CORRECT:<br/>btc-runtime Map"] --> F["Blockchain-optimized"]
F --> G["Proper equality"]
G --> H["Safe operations"]
To track all keys in a map:
// Combine map with array for enumeration
private balancesPointer: u16 = Blockchain.nextPointer;
private holdersPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
private holders: StoredAddressArray;
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
this.holders = new StoredAddressArray(this.holdersPointer);
}
public _mint(to: Address, amount: u256): void {
// Track new holder
if (this.balances.get(to).isZero()) {
this.holders.push(to);
}
// Update balance
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
}
public getHolders(_calldata: Calldata): BytesWriter {
const count = this.holders.length;
const writer = new BytesWriter(32 * i32(count) + 4);
writer.writeU32(u32(count));
for (let i: u64 = 0; i < count; i++) {
writer.writeAddress(this.holders.get(i));
}
return writer;
}// Values initialize to default when first accessed
public ensureAccount(addr: Address): void {
// get() returns default (u256.Zero) if not set
// No explicit initialization needed
const balance = this.balances.get(addr);
// First set creates the storage entry
if (balance.isZero()) {
// Optional: Initialize with some value
this.balances.set(addr, u256.One); // e.g., welcome bonus
}
}public addToBalance(addr: Address, amount: u256): void {
// Read current
const current = this.balances.get(addr);
// Modify
const newBalance = SafeMath.add(current, amount);
// Write back
this.balances.set(addr, newBalance);
}constructor() {
super();
// Always initialize maps in constructor
this.balances = new AddressMemoryMap(this.balancesPointer);
this.stakes = new AddressMemoryMap(this.stakesPointer);
}// AddressMemoryMap always uses u256.Zero as the default value
// Unset addresses will return u256.Zero when queried
new AddressMemoryMap(ptr);public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
// Validate before map operations
if (to.equals(Address.zero())) {
throw new Revert('Invalid recipient');
}
// Then use map
this.balances.set(to, amount);
}// Always use SafeMath when updating values
const current = this.balances.get(addr);
const newValue = SafeMath.add(current, amount); // Checks overflow
this.balances.set(addr, newValue);Navigation:
- Previous: Stored Maps
- Next: Cross-Contract Calls