Storage Developer Guide
Learn how to integrate with Net Storage in your applications. This guide covers smart contract interactions, querying patterns, and best practices for building on Storage.
How Storage Works
Basic Concept
Net Storage is a key-value store where:
- Key: A unique identifier for your data (like a filename)
- Operator: The address that stored the data (the "owner")
- Text: Metadata or description (like file metadata or text content)
- Data: The actual content you want to store
Key insight: Multiple users can store data with the same key. The operator address ensures you get the right data.
Using the TypeScript SDK
For TypeScript/JavaScript applications, see the @net-protocol/storage SDK for React hooks and client classes.
Your First Storage Call
// Store data - msg.sender automatically becomes the operator
storage.put(keccak256("my-profile"), "User Profile", profileData);
Reading Your Data
// Read your own data - use your address as operator
(string memory text, bytes memory data) = storage.get(
keccak256("my-profile"),
msg.sender
);
Reading Someone Else's Data
// Read Alice's profile - use her address as operator
(string memory text, bytes memory data) = storage.get(
keccak256("my-profile"),
aliceAddress
);
Common Patterns
User Profiles:
// Store profile with descriptive text
bytes32 profileKey = keccak256(abi.encodePacked("profile", userAddress));
storage.put(profileKey, "User Profile - Bio, Avatar, Links", profileData);
// Read profile - text contains the description
(string memory description, bytes memory data) = storage.get(profileKey, userAddress);
App Configuration:
// Store config with metadata
bytes32 configKey = keccak256("app-config");
storage.put(configKey, "App Settings - Theme, Language, Features", configData);
// Read config - text contains the metadata
(string memory metadata, bytes memory data) = storage.get(configKey, msg.sender);
Contract Information
Version History
Storage automatically creates version history for every key-operator pair:
How version history works:
- Simple: Every time you store data with the same key, it creates a new version (like saving over a file), and you can go back to any previous version by asking for "version 0", "version 1", etc.
- Technical: Each
put()call creates a new message in Net Protocol's message history for that key-operator pair, andgetValueAtIndex()retrieves any previous message by index (0 = first version, 1 = second version, etc.).
Core Functions
put(key, text, data)
Store data for a given key. The caller's address (msg.sender) automatically becomes the operator.
Parameters:
key: 32-byte key identifiertext: Text descriptiondata: Data to store
Purpose: Store data and automatically create a new version in the history.
Example:
// When Alice calls this, operator = Alice's address
storage.put(keccak256("my-profile"), "User Profile", profileData);
get(key, operator)
Retrieve the latest version of stored data.
Parameters:
key: 32-byte key identifieroperator: Address that stored the data (the "owner")
Returns: (text, data) - Latest stored value
Purpose: Get the most recent version of data for a key-operator pair.
Examples:
// Read your own data
(string memory text, bytes memory data) = storage.get(key, msg.sender);
// Read someone else's data
(string memory text, bytes memory data) = storage.get(key, aliceAddress);
getValueAtIndex(key, operator, idx)
Retrieve a specific historical version.
Parameters:
key: 32-byte key identifieroperator: Address that stored the data (the "owner")idx: Version index (0 = first version, 1 = second version, etc.)
Returns: (text, data) - Historical stored value
Purpose: Access any previous version of stored data.
Example:
// Read the first version of Alice's data
(string memory text, bytes memory data) = storage.getValueAtIndex(key, aliceAddress, 0);
getTotalWrites(key, operator)
Get the total number of versions stored.
Parameters:
key: 32-byte key identifieroperator: Address that stored the data (the "owner")
Returns: Total number of versions (write count)
Purpose: Query how many times a key has been updated. Use this to iterate through historical versions.
Example:
// Check how many versions Alice has stored
uint256 totalVersions = storage.getTotalWrites(key, aliceAddress);
Example: Reading version history
// Get total versions for your data
uint256 totalVersions = storage.getTotalWrites(myKey, msg.sender);
// Read most recent version (index = totalVersions - 1)
(string memory text, bytes memory data) = storage.getValueAtIndex(
myKey,
msg.sender,
totalVersions - 1
);
// Read first version (index = 0)
(string memory firstText, bytes memory firstData) = storage.getValueAtIndex(
myKey,
msg.sender,
0
);
Storing Data from Your Contract
import {IStorage} from "./interfaces/IStorage.sol";
contract MyApp {
IStorage public storage = IStorage(0x00000000DB40fcB9f4466330982372e27Fd7Bbf5);
function storeUserData(address user, string memory data) public {
bytes32 key = keccak256(abi.encodePacked("user-data", user));
storage.put(key, "User data", bytes(data));
}
function storeAppConfig(string memory config) public {
bytes32 key = keccak256(abi.encodePacked("app-config"));
storage.put(key, "App configuration", bytes(config));
}
}
ChunkedStorage Contract
Purpose: Store files larger than gas limits allow
Why ChunkedStorage Exists
Solidity has strict gas limits per transaction. Large files cannot be stored in a single transaction. ChunkedStorage solves this by:
- Compression: Reduces file size with gzip (happens client-side)
- Chunking: Splits compressed data into 20KB pieces
- Delegation: Stores each chunk in Storage.sol separately
- Reassembly: Provides functions to retrieve and reassemble chunks
Key insight: ChunkedStorage doesn't store data itself - it's a coordinator that uses Storage.sol as the actual storage backend.
How ChunkedStorage Works
ChunkedStorage stores data in Storage.sol using a specific key structure:
Limits:
- Maximum chunks: 255
- Chunk size: 20KB (20,000 bytes)
Core Functions
put(key, text, chunks[])
Store pre-chunked data.
function put(
bytes32 key,
string calldata text,
bytes[] calldata chunks
) public
Parameters:
key: Storage keytext: Original text descriptionchunks: Array of pre-compressed and chunked data
Purpose: Accept pre-processed chunks from client and store them efficiently. Client handles compression to save gas.
get(key, operator)
Retrieve and reassemble chunked data.
function get(
bytes32 key,
address operator
) public view returns (string memory, bytes memory)
Returns: (originalText, reassembledData)
Purpose: Fetch all chunks for a key and reassemble them into the original compressed data. Client must decompress.
getMetadata(key, operator)
Get chunk count without retrieving data.
function getMetadata(
bytes32 key,
address operator
) external view returns (uint8 chunkCount, string memory originalText)
Purpose: Check if data exists and how many chunks it has without the gas cost of retrieving all chunks. Used by StorageRouter.
getChunk(key, operator, chunkIndex)
Get a single chunk by index.
function getChunk(
bytes32 key,
address operator,
uint8 chunkIndex
) external view returns (bytes memory chunkData)
Purpose: Retrieve individual chunks for parallel or partial loading.
getChunks(key, operator, startIndex, endIndex)
Get multiple chunks at once.
function getChunks(
bytes32 key,
address operator,
uint8 startIndex,
uint8 endIndex
) external view returns (bytes[] memory chunks)
Purpose: Batch fetch chunks to reduce RPC calls. More efficient than calling getChunk repeatedly.
StorageRouter Contract
Purpose: Automatically detect whether data is in regular Storage or ChunkedStorage
Why StorageRouter Exists
Applications don't know in advance whether a key was stored using regular Storage or ChunkedStorage. StorageRouter automatically detects which storage type has the data and returns the appropriate information.
Use case: Generic storage displays (like profile canvases) that need to work with any storage type without knowing beforehand.
How StorageRouter Works
StorageRouter tries both storage types and returns which one has the data, plus metadata about how to retrieve it.
Strategy: Check ChunkedStorage first (cheaper metadata call), then fall back to regular Storage.
Return Values
If data is in ChunkedStorage:
isChunkedStorage:truetext: Original text from metadatadata: Encoded chunk count (NOT the actual data)
If data is in regular Storage:
isChunkedStorage:falsetext: Text from Storagedata: Actual stored data
Critical detail: When isChunkedStorage is true, you must make additional calls to fetch chunks using ChunkedStorage contract. The router only tells you the data exists and how many chunks it has.
Purpose of this design: Saves gas by not fetching all chunks unless the caller actually needs them.
ChunkedStorageReader Contract
Purpose: Read ChunkedStorage data with historical version support
Why ChunkedStorageReader Exists
ChunkedStorage.sol only has functions to read the latest version (get, getMetadata, getChunk, getChunks). It has no historical version functions.
ChunkedStorageReader is a separate read-only contract that queries Storage.sol directly with historical indexes, bypassing ChunkedStorage's latest-only functions.
How ChunkedStorageReader Works
Key insight: ChunkedStorageReader queries Storage.sol directly using the ChunkedStorage contract address as the operator. This is the same address that ChunkedStorage.sol uses when it stores chunks.
Core Functions
Latest Version Functions
Same as ChunkedStorage but implemented as a reader:
get(key, operator)- Latest versiongetMetadata(key, operator)- Latest metadatagetChunk(key, operator, chunkIndex)- Latest chunkgetChunks(key, operator, startIndex, endIndex)- Latest chunks
Historical Version Functions
getValueAtIndex(key, operator, idx)
Retrieve a complete historical version by fetching metadata and all chunks at a specific index.
getMetadataAtIndex(key, operator, idx)
Check historical metadata without fetching all chunks. Used by XML Storage to verify historical references.
getChunkAtIndex(key, operator, chunkIndex, idx)
Get a specific chunk at a specific historical version.
getChunksAtIndex(key, operator, startIndex, endIndex, idx)
Get multiple chunks at a specific historical version.
Usage with XML Storage
XML Storage requires ChunkedStorageReader for historical versions. The i="..." attribute in XML references specifies which historical index to fetch.
Example XML reference with historical index:
<net k="0x..." v="0.0.1" i="2" o="0xoperator" />
This tells the system to fetch version 2 (third version) of the chunk using getMetadataAtIndex and getChunksAtIndex.
Querying Patterns
Direct Contract Queries
Use the Storage contract functions to read data:
get(key, operator)- Get latest value for a keygetValueAtIndex(key, operator, index)- Get specific versiongetTotalWrites(key, operator)- Get total number of versionsbulkGet(keys, operator)- Get multiple values at once
Net Protocol Queries
Storage data is stored as Net messages, so you can also query directly from Net:
Query Storage data through Net Protocol:
getMessageForAppUser(app, user, start, count)- Get all Storage messages for a usergetMessageForAppTopic(app, topic, start, count)- Get messages for a specific key
Events
Storage Events
Stored(bytes32 key, address operator)- Emitted when data is stored
XML Storage Pattern
What it is: Frontend pattern for storing very large files (multi-MB)
Not a contract: Uses ChunkedStorage as backend storage
Why XML Storage Exists
ChunkedStorage works great for files up to 80KB. For larger files, XML Storage automatically handles the complexity.git status
How it works: Splits large files into 80KB pieces, stores each piece using ChunkedStorage, and maintains references as XML metadata.
What you can store: Videos, large images, documents, datasets - files of any size.
Two-Level Chunking System
Level 1: XML Chunks (80KB each)
Large file is split into 80KB pieces called "XML chunks"
Level 2: ChunkedStorage Chunks (20KB each)
Each 80KB XML chunk is:
- Compressed with gzip
- Split into 20KB chunks
- Stored in ChunkedStorage
Why 80KB? Each XML chunk becomes ~4 ChunkedStorage chunks after compression, staying well under the 255-chunk limit while maximizing efficiency.
XML Metadata Format
XML Reference Format:
<net k="0x..." v="0.0.1" i="0" o="0xoperator" s="d" />
Attributes:
k: ChunkedStorage key (hash of XML chunk + operator)v: Version string (always "0.0.1")i: Historical index (0 for first version, 1 for second, etc.)o: Operator address (lowercase)s: Source type (optional)
The s attribute specifies where to read the data from:
s="d"- Read from Storage.sol directly without decompression- Omit
s- Read from ChunkedStorage with decompression (default)
Examples:
<!-- Read from Storage.sol -->
<net k="0x..." v="0.0.1" s="d" />
<!-- Read from ChunkedStorage -->
<net k="0x..." v="0.0.1" />
Embedding Existing Content
You can embed already stored Net content in new storage items using <net /> tags. Same format as XML Storage references, but works for any stored content (not just chunks).
Export feature: On any storage page, click "Export" button. Dialog shows embed tag with description "Copy this embed to include this content in other Net stored content". Copy button copies the embed tag to clipboard. Tag format automatically includes s="d" for regular Storage or omits it for ChunkedStorage (detected via StorageRouter).
Embed tag format:
<!-- Regular Storage -->
<net k="0x..." v="0.0.1" o="0x..." s="d" />
<!-- ChunkedStorage -->
<net k="0x..." v="0.0.1" o="0x..." />
Attributes (k, v, o, s) are explained in XML Metadata Format section above.
Example: Store content that embeds another stored item:
Key: "my-page"
Value: "# My Page\n\n<net k="0x..." v="0.0.1" o="0x..." />\n\nMore content here."
How it works: When reading storage, the renderer identifies <net /> tags, loads the actual content associated with them, and replaces the tags with that content. Works with both regular Storage and ChunkedStorage. Same mechanism as XML Storage chunking (see Reading Process section for technical details).
Storage Process
Complete flow:
- Split large file into 80KB XML chunks
- For each XML chunk:
- Compress with gzip
- Split into 20KB chunks
- Generate key:
keccak256(xmlChunk + operatorAddress) - Store in ChunkedStorage using
put(key, "", chunks)
- Generate XML metadata with all keys
- Store metadata in regular Storage using
put(key, filename, xmlMetadata)
Reading Process
Complete flow:
- Read metadata from regular Storage
- Parse XML references (extract
k="...",i="...",o="..."values) - For each reference:
- If
iattribute exists: Use ChunkedStorageReader'sgetMetadataAtIndexandgetChunksAtIndex - If no
iattribute: Use ChunkedStorage'sgetMetadataandgetChunks - Decompress chunks
- Reassemble XML chunk
- If
- Replace XML tags with actual data
- Return complete file
Why ChunkedStorageReader is required: The i="..." attribute specifies historical versions, which ChunkedStorage.sol doesn't support. Only ChunkedStorageReader has getMetadataAtIndex and getChunksAtIndex.
Historical Versions
XML Storage supports historical versions via the i="..." attribute.
Version format:
i="0": First version (initial storage)i="1": Second version (first update)i="2": Third version (second update)
How it works: When storing a new version, the XML metadata is updated with new i="..." values. Each XML chunk reference can point to a different historical version of that chunk in ChunkedStorage.
Purpose: Enables complete version history for multi-megabyte files by tracking which version of each chunk to fetch.
When to Use Which Storage Type
By File Size
Under 20KB: Use regular Storage
- Direct storage as Net message
- Simplest option
- Lowest gas cost for small data
- No compression overhead
20KB to 80KB: Use ChunkedStorage
- Automatic compression
- Split into 20KB chunks
- Maximum 255 chunks (theoretical limit: 5.1MB)
- Practical limit: ~80KB due to RPC constraints
- Good for profile canvases, HTML pages
Over 5MB: Use XML Storage pattern
- Two-level chunking
- Handles very large files
- Frontend implementation required
- Good for large media files, datasets
By Version History Needs
Regular Storage:
getTotalWrites(key, operator)- Get version countgetValueAtIndex(key, operator, idx)- Get specific version- Use when: Simple key-value data with frequent updates
ChunkedStorage:
- Use ChunkedStorageReader contract
getValueAtIndex(key, operator, idx)- Get historical versiongetMetadataAtIndex(key, operator, idx)- Get historical metadata- Use when: Large files with version history requirements
XML Storage:
- Set
i="0",i="1", etc. in XML metadata - Uses ChunkedStorageReader for historical chunk access
- Use when: Very large files with version history requirements
By Reading Pattern
Use StorageRouter when:
- You don't know if data is in regular Storage or ChunkedStorage
- Building generic storage displays
- Want automatic detection
Use Storage.sol directly when:
- You know data is small (< 20KB)
- You control the storage process
- Want to minimize RPC calls
Use ChunkedStorage directly when:
- You know data is chunked
- You control the storage process
- Want to minimize RPC calls
Use ChunkedStorageReader when:
- You need historical versions of chunked data
- Implementing XML Storage
- Building version history features
Contract Addresses
All contracts deployed at the same address across all supported chains:
| Contract | Address |
|---|---|
| Storage | 0x00000000DB40fcB9f4466330982372e27Fd7Bbf5 |
| ChunkedStorage | 0x000000A822F09aF21b1951B65223F54ea392E6C6 |
| StorageRouter | 0x000000C0bbc2Ca04B85E77D18053e7c38bB97939 |
| ChunkedStorageReader | 0x00000005210a7532787419658f6162f771be62f8 |