Skip to main content
This document is in progress and will be refined.
Sepolia testnet (default), Monad mainnet, and Robinhood Chain mainnet are supported. Ethereum mainnet is not deployed yet. Switch networks with setNetwork(). See Network.
The Ethereum port of the IQLabs SDK. Same primitives (on-chain data storage, IQDB tables, friend connections, and end-to-end encryption) built on ethers v6 and a single deployed contract.

Installation

The SDK ships as CommonJS for Node.js and works in browsers via any modern bundler.

Network

The SDK ships with multiple network modes. Default is sepolia. Switch with setNetwork() typically once at app startup.
See the Robinhood Chain guide for chain-specific setup (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 an ethers.Signer. The two common ways to obtain one:

Node.js (private key)

Browser (MetaMask / injected wallet)

Reader functions don’t need a signer. They use the RPC URL configured via 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_SIZE chunks, uploaded via sendCode() calls in batches up to ~96 KB each, and the tail tx hash is recorded
  • codeIn(): upload data and get a transaction hash
  • readCodeIn(): 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 first codeIn() 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
A blocked connection can only be unblocked by the blocker.
The connection seed is derived deterministically from the two addresses via deriveDmSeed(userA, userB) (sorted lowercase + keccak256), so either party can recompute it.

Database Tables

Store JSON data in tables like a database.

How are tables created?

  1. Call initializeDbRoot() once per dbRootId. Only the address that calls this becomes the DbRoot creator (the only one allowed to update permissions or schema).
  2. Call createTable() (public) or createTable(..., isPrivate=true) (private) to create a table. tableCreationFee (0.0003 ETH default on Sepolia) is charged here and split 31/69 between feeReceiver and the DbRoot’s creator.
  3. 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.
Unlike the Solana SDK, tables must exist before writeRow() is called. writeRow reads the table’s txChainTail for a staleness check, so there is no implicit creation.

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 (amount is ignored)
ERC-20 amount is in raw token units (wei-style), not human-readable units. For an 18-decimal token, “100 tokens” must be passed as parseEther("100"), not 100. If amount == 0, the contract treats it as 1.
ERC-1155 is not supported. The contract only knows the balanceOf(address) shape used by ERC-20/ERC-721. ERC-1155’s balanceOf(address, uint256) will not be called.

Gate parameter

If the 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 called initializeDbRoot()) 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:
Pass empty arrays to make creation public again.

Onboarding (private → public)

The contract supports promoting a private table (in globalTables 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 using deriveX25519Keypair(). Their wallet is the key, no separate keystore.

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% to feeReceiver.
  • For createTable: split 31% to feeReceiver / 69% to the DbRoot’s creator. Root creators can pin their own value (including 0) via setRootTableCreationFee — see below.
What does not cost anything: 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()

Internally, this fires:
  1. (For large data) a series of sendCode() calls forming the linked list
  2. userInventoryCodeIn(handle, tailTx, ...): charges the fee herebasicFee if tailTx === "" (inline payload), otherwise linkedListFee
  3. updateUserTxChainTail(myTxHash): free pointer bump

readCodeIn()

For inline uploads, 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()

The connection seed is computed automatically from (senderAddress, receiverAddress) via deriveDmSeed(). requestConnection is free in this version.

manageConnection()

Approve, block, or unblock a connection. Status semantics:
  • 0: pending (initial state, set by requestConnection)
  • 1: approved
  • 2: blocked

readConnection()

'unknown' means the connection record does not exist on-chain.

writeConnectionRow()

The connection seed is derived automatically from the sender (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.
Reverts if the dbRootId was already initialized.

manageTableCreators()

Set both creator allowlists. Caller must be the DbRoot creator.
Pass empty arrays to make creation open to anyone.

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.
The table must already exist. There is no implicit creation. writeRow reads txChainTail for staleness check and reverts if the table was never created.
This call fires two transactions: 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().
Per-write split is still 31% 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.
After the transfer the previous creator can no longer call manageTableCreators, setRootTableCreationFee, etc. — the new address has full root authority.

fetchInventoryTransactions()

Walk a user’s inventory tx-chain (everything they uploaded via codeIn()).
For inline uploads, 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()

Duplicate recipients in recipientPubHexes are deduplicated automatically.

User Metadata

updateUserMetadata()

Store arbitrary metadata under the caller’s address. Overwrites any previous value.
The data is stored on-chain as raw 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

These are low-level SDK functions. Not needed for typical usage, but useful when building custom features or debugging.

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.
Internally fires 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 into CHUNK_SIZE (850 byte) chunks
  • uploadLinkedList(signer, chunks, onProgress?): upload chunks via batched sendCode calls, return tail tx hash
  • prepareUpload(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 a sendCode linked list, returning the concatenated payload
  • walkCalldataChain(tailTx, beforeArg, options?): walk any tx chain backwards by following the named “before” argument
  • isEnd(tx): returns true if 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; call clearFeeCache() 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.