This document is in progress and will be refined.
ethers v6 and a single deployed contract.
Installation
Network
The SDK ships with multiple network modes. Default issepolia. Switch with setNetwork() typically once at app startup.
robinhood requires SDK >= 0.3.0).
Ethereum mainnet is not deployed yet.
Reader functions resolve their RPC from (in priority order): explicit override via
setNetwork(mode, rpcUrl) or setRpcUrl(url), then env vars (IQLABS_RPC_ENDPOINT, ETHEREUM_RPC_URL, RPC_URL), then the active mode’s default RPC. Writer functions use whatever provider is attached to the Signer you pass in. Be sure that provider is on the same chain as the active mode.Wallet / Signer Setup
Every writer function takes anethers.Signer. The two common ways to obtain one:
Node.js (private key)
Browser (MetaMask / injected wallet)
setRpcUrl().
Core Concepts
These are the key concepts to know before using the IQLabs Ethereum SDK.Data Storage (Code In)
This is how you store any data (files, text, JSON) on-chain. Data is inscribed into transaction calldata; nothing is written to contract storage. Reads reconstruct data by walking a linked list of transactions.How is it stored?
Depending on data size, the SDK picks the optimal method:- Inline (small): data fits in a single transaction’s metadata field, no chunking
- Linked list (large): data is split into
CHUNK_SIZEchunks, uploaded viasendCode()calls in batches up to ~96 KB each, and the tail tx hash is recorded
Key related functions
codeIn(): upload data and get a transaction hashreadCodeIn(): read data back from a transaction hash
User State
Each address has an on-chain record managed by the contract. There is no separate account/PDA to initialize.What gets stored?
- User-set metadata (name, profile, bio, anything you serialize and pass to
updateUserMetadata) userTxChainTail: the most recent inventory write, used as the head of the user’s tx-chain
When is it created?
There is no explicit “create user” step. The firstcodeIn() call writes both the inventory entry and advances the chain tail. Each codeIn() charges basicFee (0.0001 ETH) for an inline payload or linkedListFee (0.0003 ETH) when the data is chunked via a sendCode linked list. The fee is collected on userInventoryCodeIn itself; the trailing pointer bump is free.
Connection State
An on-chain relationship between two addresses (friends, DM channels, etc.).What states can it have?
- pending (
0): a request was sent but not accepted yet - approved (
1): the request was accepted and the users are connected - blocked (
2): one side blocked the other
deriveDmSeed(userA, userB) (sorted lowercase + keccak256), so either party can recompute it.
Key related functions
requestConnection(): send a friend request (creates pending)manageConnection(): approve/block/unblock a requestreadConnection(): check current relationship statuswriteConnectionRow(): exchange messages/data with a connected partyfetchUserConnections(): fetch all of a user’s connections
Database Tables
Store JSON data in tables like a database.How are tables created?
- Call
initializeDbRoot()once perdbRootId. Only the address that calls this becomes the DbRoot creator (the only one allowed to update permissions or schema). - Call
createTable()(public) orcreateTable(..., isPrivate=true)(private) to create a table.tableCreationFee(0.0003 ETH default on Sepolia) is charged here and split 31/69 betweenfeeReceiverand the DbRoot’s creator. - Call
writeRow()to append rows.
A table is uniquely identified by the combination of
dbRootId and tableName. Both are hashed with keccak256 internally to form mapping keys, but the raw names are also stored on-chain so the SDK can list them without a hardcoded lookup.writeRow() is called. writeRow reads the table’s txChainTail for a staleness check, so there is no implicit creation.
Key related functions
initializeDbRoot(): claim adbRootIdand become its creatormanageTableCreators(): set the public/private creator allowlistscreateTable()/updateTable(): create or modify a table’s schema/gatewriteRow(): append a rowreadTableRows(): read rows from a tablegetTablelistFromRoot(): list all tables in a database
Token & Collection Gating
Tables can be gated so that only users holding a specific ERC-20 token or ERC-721 collection can write data.Gate Types
How it works
The contract’s gate check (_requireGate) calls balanceOf(msg.sender) on the configured contract:
- If
tokenAddress == ZeroAddress→ no gate (public) - If
gateType == 0(ERC-20) →IERC20.balanceOf(user) >= amount - If
gateType == 1(ERC-721) →IERC721.balanceOf(user) >= 1(amountis ignored)
Gate parameter
gate argument is omitted, it defaults to { tokenAddress: ZeroAddress, amount: 0, gateType: 0 }, i.e. public.
Table Creation Permissions
The DbRoot creator (the address that calledinitializeDbRoot()) controls who is allowed to create tables.
Two levels of table creation
Both
tableSeeds and globalTableSeeds are stored on-chain in lockstep with their human-readable tableNames / globalTableNames. The SDK returns them already zipped as TableEntry { name, seedHex }.- If the permission list is empty, anyone can create tables in that bucket (default).
- If the permission list has addresses, only those addresses can create tables in that bucket. The DbRoot creator is not automatically allowed. They must add themselves to the list if they want to create tables under a non-empty allowlist.
Managing permissions
The DbRoot creator can set both lists in a single call:Onboarding (private → public)
The contract supports promoting a private table (inglobalTables only) into the public tables list via onboardTable(dbRootId, tableName). This is callable by anyone in tableCreators.
This is exposed at the contract ABI level but does not have a high-level SDK wrapper yet. Call it via the contract interface directly:The same caveat applies to
updateDbRootTableList (replace the public table list wholesale): ABI-only, no SDK wrapper.Encryption (Crypto)
The SDK includes a built-in encryption module (iqlabs.crypto) for encrypting data before storing it on-chain. The primitives are identical to the Solana SDK, so the same plaintext can flow across chains.
Three encryption modes
- DH Encryption (single recipient): Ephemeral X25519 ECDH → HKDF-SHA256 → AES-256-GCM. Use when encrypting data for one specific recipient.
- Password Encryption: PBKDF2-SHA256 (250k iterations) → AES-256-GCM. Use for password-protected data that anyone with the password can decrypt.
- Multi-recipient Encryption (PGP-style hybrid): Generates a random content encryption key (CEK), encrypts data once, then wraps the CEK for each recipient via ECDH. Use when encrypting data for multiple recipients.
Key derivation
Users can derive a deterministic X25519 keypair from their wallet signature usingderiveX25519Keypair(). Their wallet is the key, no separate keystore.
Key related functions
deriveX25519Keypair(): derive encryption keypair from walletdhEncrypt()/dhDecrypt(): single-recipient encryptionpasswordEncrypt()/passwordDecrypt(): password-based encryptionmultiEncrypt()/multiDecrypt(): multi-recipient encryption
Fees
Fees are owner-mutable per network and read from the contract by the SDK (utils.getBasicFee / getLinkedListFee / getTableCreationFee). The defaults shipped at deploy time are below.
Where the value goes:
- For
dbCodeIn/walletConnectionCodeIn/userInventoryCodeIn: 100% tofeeReceiver. - For
createTable: split 31% tofeeReceiver/ 69% to the DbRoot’screator. Root creators can pin their own value (including 0) viasetRootTableCreationFee— see below.
updateTableTxChainTail, updateConnectionTxChainTail, updateUserTxChainTail, requestConnection, manageConnection, dbInstructionCodeIn. These are nonpayable in the current contract.
writeRow() and writeConnectionRow() still fire two transactions internally (the code-in + the pointer update), but the pointer update is now free — the fee is collected upstream on the code-in itself, exactly once per write.
Function Details
Data Storage and Retrieval
codeIn()
- (For large data) a series of
sendCode()calls forming the linked list userInventoryCodeIn(handle, tailTx, ...): charges the fee here —basicFeeiftailTx === ""(inline payload), otherwiselinkedListFeeupdateUserTxChainTail(myTxHash): free pointer bump
readCodeIn()
data is read directly from the handle field. For chunked uploads, the SDK walks the sendCode linked list starting from tailTx and concatenates the chunks.
Connection Management
requestConnection()
(senderAddress, receiverAddress) via deriveDmSeed(). requestConnection is free in this version.
manageConnection()
Approve, block, or unblock a connection. Status semantics:
0: pending (initial state, set byrequestConnection)1: approved2: blocked
readConnection()
'unknown' means the connection record does not exist on-chain.
writeConnectionRow()
signer.getAddress()) and otherParty. Internally fires walletConnectionCodeIn (charges basicFee for inline payload, linkedListFee when chunked) then updateConnectionTxChainTail (free pointer bump).
readConnectionRows()
Read all rows previously written between two parties.
fetchUserConnections()
Fetch all connection records for a user. The contract maintains an indexed list of connection keys per address, so this is a direct on-chain read (no transaction-history scanning).
Unlike the Solana SDK’s
fetchUserConnections, the result does not include dbRootId, requester, blocker, or timestamp. To get those, call readConnection() for the specific pair.Table Management
initializeDbRoot()
Claim a dbRootId and register the caller as its creator. Only the creator can later modify table-creator allowlists or schema.
dbRootId was already initialized.
manageTableCreators()
Set both creator allowlists. Caller must be the DbRoot creator.
createTable()
Create a new table. Charges tableCreationFee (0.0003 ETH default on Sepolia, or whatever the DbRoot creator pinned via setRootTableCreationFee()). The fee is split 31% to feeReceiver and 69% to DbRoot.creator.
writers is a per-table allowlist enforced inside dbCodeIn/updateTableTxChainTail. Empty array = anyone (subject to gate). The writers check is independent of the table-creator check.updateTable()
Modify an existing table’s schema, gate, or writer list. Caller must be the DbRoot creator. Existing rows (txChainTail) are preserved.
writeRow()
Append a row to an existing table. Charges basicFee (0.0001 ETH) when the row fits inline, or linkedListFee (0.0003 ETH) when it has to be chunked through sendCode.
dbCodeIn (write — fee charged here, based on payload size) and updateTableTxChainTail (free pointer update). The SDK awaits both before returning.
readTableRows()
Walk the table’s tx-chain backwards from the tail and reconstruct each row.
getTablelistFromRoot()
Returns the public and global table lists for a DbRoot, plus the root’s tableCreationFee override state.
tables only contains public tables (created with isPrivate=false). globalTables contains every table ever created under this root, public or private.
setTableCreationFee()
IQ-protocol owner only. Updates the contract-wide default tableCreationFee. DbRoots that haven’t pinned their own value will see the new amount on the next createTable call.
setRootTableCreationFee()
DbRoot creator only. Pins this root’s tableCreationFee to a specific value (including 0n) so it ignores future changes to the global default. Lasts until clearRootTableCreationFee().
feeReceiver / 69% DbRoot.creator, so a 0-fee root simply collects nothing.
clearRootTableCreationFee()
DbRoot creator only. Removes the per-root pin, letting the global default apply again.
transferDbRootCreator()
Current DbRoot.creator only. Hands ownership of the root — and its 69% share of future tableCreationFee collections — to a new address. Mirrors solana transfer_db_root_creator.
manageTableCreators, setRootTableCreationFee, etc. — the new address has full root authority.
fetchInventoryTransactions()
Walk a user’s inventory tx-chain (everything they uploaded via codeIn()).
tailTx is empty and handle is the data. For chunked uploads, handle is the filename and tailTx points to the linked-list tail (use readCodeIn() to reconstruct).
Encryption
deriveX25519Keypair()
Derive a deterministic X25519 keypair from a wallet signature. The same wallet always produces the same keypair (the message and HKDF parameters are fixed protocol constants).
dhEncrypt()
dhDecrypt()
passwordEncrypt()
passwordDecrypt()
multiEncrypt()
multiDecrypt()
recipientPubHexes are deduplicated automatically.
User Metadata
updateUserMetadata()
Store arbitrary metadata under the caller’s address. Overwrites any previous value.
bytes (UTF-8 encoded if you pass a string).
Environment Settings
setNetwork()
Switch the active network mode. Reader functions immediately resolve to the new chain’s contract and default RPC. Writers use the provider attached to your Signer, so you must also point that signer at a matching RPC. Call this once at app startup (or whenever the user toggles networks).
getNetwork()
Returns the currently active network mode.
| Returns | 'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood' |
assertChainMatches()
Throws if the configured RPC’s chainId doesn’t match the active network mode. Use this defensively before sending a transaction in environments where the user controls the RPC (e.g. injected wallets).
setRpcUrl()
Override only the reader RPC URL, without changing the active network mode. Most users should prefer setNetwork() setRpcUrl exists for cases where you want to switch RPC providers (e.g. fallback to Alchemy) while staying on the same chain.
Writers (
signer-based functions) use the provider attached to the Signer they receive. setRpcUrl does not affect them it only routes reader RPC calls. To make writers target a different chain, give the Signer a provider on that chain (and call setNetwork() so readers stay in sync).getRpcUrl()
Returns the currently active reader RPC URL (resolving env vars and the active mode’s default).
| Returns | string |
Advanced Functions
Writer Functions
manageRowData()
Overwrite or annotate a previously written row by referencing its targetTx hash. The new row joins the table’s tx-chain like any other write, but targetTx records the row this entry refers to.
dbInstructionCodeIn + updateTableTxChainTail — both free. The contract also stamps instructionTableTimestamps[dbRootId][tableSeed] with the current block timestamp. Edits and deletes are gas-only.
prepareUpload() / uploadLinkedList() / toChunks()
Lower-level helpers exposed for building custom upload flows. Most users should use codeIn() instead.
toChunks(data): split a string intoCHUNK_SIZE(850 byte) chunksuploadLinkedList(signer, chunks, onProgress?): upload chunks via batchedsendCodecalls, return tail tx hashprepareUpload(signer, data, onProgress?): decide inline vs linked-list, return{ onChainPath, metadata }
Reader Functions
readUserState()
Read raw on-chain state for a user.
fetchTableMeta()
Read a table’s full schema and current txChainTail.
readSendCodeChain() / walkCalldataChain() / isEnd()
Tx-chain traversal primitives:
readSendCodeChain(tailTx, onProgress?): reconstruct asendCodelinked list, returning the concatenated payloadwalkCalldataChain(tailTx, beforeArg, options?): walk any tx chain backwards by following the named “before” argumentisEnd(tx): returnstrueif the value represents end-of-chain (empty /"Genesis"/ zero hash)
Utility Functions
deriveDmSeed()
Compute the deterministic connection seed for a pair of addresses. Sorts the two addresses lowercase, joins with :, and hashes with keccak256.
Fee getters
Read the live fee values straight from the contract. Cached per contract address with a 10-minute TTL; callclearFeeCache() to force a re-read (e.g. right after the owner ran setFees).
Crypto Utilities
hexToBytes(hex), bytesToHex(bytes), validatePubKey(hex, name) are exported under iqlabs.crypto for convenience.