@iqlabs-official/ethereum-sdk
v0.1.3
Published
IQLabs Ethereum SDK
Readme
IQLabs Ethereum SDK
Draft: This document is in progress and will be refined.
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.
npm install @iqlabs-official/ethereum-sdkTable of Contents
2.1. Advanced Functions (list only)
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 chunks (
CHUNK_SIZE), 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
An on-chain record per user address — managed by the contract, not a separate account/PDA.
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 tx-chain write costs a small BASIC_FEE for the pointer-update step (the second of the two transactions).
Connection State
An on-chain relationship between two addresses (friends, DM channels, etc.).
What states can it have?
- pending: a request was sent but not accepted yet
- approved: the request was accepted and the users are connected
- blocked: one side blocked the other
Important: A blocked connection can only be unblocked by the blocker.
Key related functions
requestConnection(): send a friend request (creates pending)manageConnection(): approve/reject/block/unblock a requestreadConnection(): check current relationship statuswriteConnectionRow(): exchange messages/data with a connected friendfetchUserConnections(): fetch all of a user's connections
Database Tables
Store JSON data in tables like a database.
How are tables created?
Use createTable() before writing rows. (Unlike the Solana SDK, tables must exist before writeRow() is called — the row write reads the table's txChainTail for staleness check.)
Note: A table is uniquely identified by the combination of
dbRootIdandtableName. Both are hashed withkeccak256internally to form mapping keys.
Key related functions
initializeDbRoot(): create the database root for adbRootIdcreateTable()/updateTable(): create or modify a table's schema/gatewriteRow(): add a new 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
| Type | gateType | Description |
|------|-----------|-------------|
| Token (ERC-20) | 0 | User must hold >= amount of the specified token contract |
| Collection (ERC-721) | 1 | User must hold any NFT from the specified collection contract |
How it works
- Table creator sets the gate when creating or updating a table
- Writers only need to hold the required asset — the contract checks balance on-chain when
writeRow()is called - If
tokenAddressisZeroAddress, the table is public (default behavior)
Gate parameter
gate?: {
tokenAddress: string; // ERC-20 or ERC-721 contract address (ZeroAddress for public)
amount: number; // minimum balance (ignored for collection gate)
gateType: 0 | 1; // 0 = token, 1 = collection
}Notes
- For token gates,
amountis the minimum balance required (e.g., 100 means "must hold >= 100 tokens", in raw units — apply your own decimal scaling) - For collection gates, the user can present any NFT from that collection.
amountis ignored - Omitting
gate(or passing{ tokenAddress: ZeroAddress, amount: 0, gateType: 0 }) creates a public table
Encryption (Crypto)
The SDK includes a built-in encryption module (iqlabs.crypto) for encrypting data before storing it on-chain. Identical primitives 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.
Key related functions
deriveX25519Keypair(): derive encryption keypair from walletdhEncrypt()/dhDecrypt(): single-recipient encryptionpasswordEncrypt()/passwordDecrypt(): password-based encryptionmultiEncrypt()/multiDecrypt(): multi-recipient encryption
Function Details
Data Storage and Retrieval
codeIn()
| Parameters | signer: ethers.Signerdata: data to upload (string or string[])filename: optional filename (string, default: "")filetype: file type hint (string, default: "" → "text/plain")onProgress: optional progress callback (percent: number) => void |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { Wallet, JsonRpcProvider } from 'ethers';
const signer = new Wallet(privateKey, new JsonRpcProvider(rpcUrl));
// Upload inline data
const txHash = await iqlabs.writer.codeIn(signer, 'Hello, blockchain!');
// Upload with filename + type (large data is chunked automatically)
const txHash2 = await iqlabs.writer.codeIn(
signer,
longString,
'hello.txt',
'text/plain',
(pct) => console.log(`upload: ${pct.toFixed(1)}%`),
);readCodeIn()
| Parameters | txHash: transaction hash (string)onProgress: optional progress callback (percent: number) => void |
|----------|--------------------------|
| Returns | { metadata: { handle, typeField, offset, beforeUserTx }, data: string } |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
const result = await iqlabs.reader.readCodeIn('0x5Xg7...');
console.log(result.data); // 'Hello, blockchain!'
console.log(result.metadata.typeField); // 'text/plain'Connection Management
requestConnection()
| Parameters | signer: ethers.SignerdbRootId: database ID (string)receiver: counterparty address (string)tableName: connection table name (string)columns: column list (string[])idCol: ID column (string)extKeys: extension keys (string[], default: []) |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
await iqlabs.writer.requestConnection(
signer, 'my-db', friendAddress,
'dm_table', ['message', 'timestamp'], 'message_id',
);The connection seed is derived deterministically from the two addresses via
deriveDmSeed(sender, receiver), so either party can recompute it.
manageConnection()
| Parameters | signer: ethers.SignerotherParty: counterparty address (string)dbRootId: database ID (string)newStatus: new status (number — 0 pending, 1 approved, 2 blocked) |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
// Approve a friend request
await iqlabs.writer.manageConnection(signer, friendAddress, 'my-db', 1);
// Block a user
await iqlabs.writer.manageConnection(signer, friendAddress, 'my-db', 2);readConnection()
| Parameters | dbRootId: database ID (string)partyA: first wallet (string)partyB: second wallet (string) |
|----------|--------------------------|
| Returns | { status: 'pending' \| 'approved' \| 'blocked' \| 'unknown', requester: 'a' \| 'b', blocker: 'a' \| 'b' \| 'none' } |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
const { status, requester, blocker } = await iqlabs.reader.readConnection(
'my-db', addressA, addressB,
);
console.log(status); // 'pending' | 'approved' | 'blocked'writeConnectionRow()
| Parameters | signer: ethers.SignerotherParty: counterparty address (string)dbRootId: database ID (string)rowJson: JSON data (string)onProgress: optional progress callback (percent: number) => void |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
await iqlabs.writer.writeConnectionRow(
signer, friendAddress, 'my-db',
JSON.stringify({ message_id: '123', message: 'Hello friend!', timestamp: Date.now() }),
);readConnectionRows()
Read all rows written between two parties on the connection's tx-chain.
| Parameters | dbRootId: database ID (string)partyA: first wallet (string)partyB: second wallet (string)options: { limit?: number } (optional) |
|----------|--------------------------|
| Returns | Array<{ txHash: string, data: any }> (most recent first) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
const messages = await iqlabs.reader.readConnectionRows('my-db', myAddr, friendAddr, { limit: 50 });
messages.forEach(m => console.log(m.data));fetchUserConnections()
Fetch all connection records for a user. Unlike the Solana SDK (which scans tx history), the Ethereum contract maintains an indexed list of connection keys per address, so this is a direct on-chain read.
| Parameters | userAddress: user address (string) |
|----------|--------------------------|
| Returns | Array<{ connectionKey, partyA, partyB, status }> |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
const connections = await iqlabs.reader.fetchUserConnections(myAddress);
const pending = connections.filter(c => c.status === 'pending');
const friends = connections.filter(c => c.status === 'approved');
const blocked = connections.filter(c => c.status === 'blocked');Table Management
initializeDbRoot()
Initialize a database root before any tables are created under it. Required once per dbRootId.
| Parameters | signer: ethers.SignerdbRootId: database ID (string) |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
await iqlabs.writer.initializeDbRoot(signer, 'my-db');createTable()
| Parameters | signer: ethers.SignerdbRootId: database ID (string)tableName: table name (string)columns: column names (string[])idCol: ID column (string)extKeys: extension keys (string[], default: [])gate: optional access gate, see Token & Collection Gatingwriters: optional writer whitelist (string[], default: [])isPrivate: create private table (boolean, default: false) |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { ZeroAddress } from 'ethers';
// Public table
await iqlabs.writer.createTable(
signer, 'my-db', 'users', ['name', 'email'], 'user_id',
);
// Token-gated table (ERC-20, must hold >= 100)
await iqlabs.writer.createTable(
signer, 'my-db', 'vip', ['name'], 'user_id', [],
{ tokenAddress: erc20Address, amount: 100, gateType: 0 },
);
// NFT-collection-gated table (ERC-721)
await iqlabs.writer.createTable(
signer, 'my-db', 'holders', ['name'], 'user_id', [],
{ tokenAddress: nftAddress, amount: 0, gateType: 1 },
);updateTable()
Modify an existing table's schema/gate/writers. Same parameters as createTable() minus isPrivate.
| Parameters | signer, dbRootId, tableName, columns, idCol, extKeys, gate, writers |
|----------|--------------------------|
| Returns | Transaction hash (string) |
writeRow()
| Parameters | signer: ethers.SignerdbRootId: database ID (string)tableName: table name (string)rowJson: JSON row data (string)onProgress: optional progress callback (percent: number) => void |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
await iqlabs.writer.writeRow(signer, 'my-db', 'users', JSON.stringify({
id: 1, name: 'Alice', email: '[email protected]',
}));Each
writeRowcall is a 2-tx flow: the data write (dbCodeIn) and a pointer-update (updateTableTxChainTail) which chargesLINKED_LIST_FEE. The SDK awaits both before returning.
readTableRows()
Walk the table's tx-chain backwards from the tail and reconstruct each row.
| Parameters | dbRootId: database ID (string)tableName: table name (string)options: { limit?: number } (optional) |
|----------|--------------------------|
| Returns | Array<{ txHash: string, data: any }> (most recent first; data is parsed JSON when possible) |
Example:
const rows = await iqlabs.reader.readTableRows('my-db', 'users', { limit: 50 });
rows.forEach(r => console.log(r.data));getTablelistFromRoot()
| Parameters | dbRootId: database ID (string) |
|----------|--------------------------|
| Returns | { creator: string, tables: TableEntry[], globalTables: TableEntry[] } where TableEntry = { name, seedHex } |
Example:
const result = await iqlabs.reader.getTablelistFromRoot('my-db');
console.log('Creator:', result.creator);
result.tables.forEach(t => console.log(t.name, t.seedHex));fetchInventoryTransactions()
Walk a user's inventory tx-chain (everything they've uploaded via codeIn).
| Parameters | userAddress: user address (string)options: { limit?: number } (optional) |
|----------|--------------------------|
| Returns | Array<{ txHash, handle, tailTx, typeField, offset }> |
Example:
const myFiles = await iqlabs.reader.fetchInventoryTransactions(myAddress, { limit: 20 });
myFiles.forEach(tx => {
console.log(`${tx.txHash}: ${tx.handle} (${tx.typeField})`);
});Encryption
deriveX25519Keypair()
Derive a deterministic X25519 keypair from a wallet signature. The same wallet always produces the same keypair.
| Parameters | signMessage: function (msg: Uint8Array) => Promise<Uint8Array> |
|----------|--------------------------|
| Returns | { privKey: Uint8Array, pubKey: Uint8Array } |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { getBytes } from 'ethers';
// Adapter: ethers signMessage returns a hex string; convert to bytes.
const sign = async (msg: Uint8Array) => getBytes(await signer.signMessage(msg));
const { privKey, pubKey } = await iqlabs.crypto.deriveX25519Keypair(sign);dhEncrypt()
| Parameters | recipientPubHex: recipient X25519 pubkey (hex)plaintext: data to encrypt (Uint8Array) |
|----------|--------------------------|
| Returns | { senderPub, iv, ciphertext } (all hex) |
dhDecrypt()
| Parameters | privKey: recipient private key (Uint8Array)senderPubHex: sender pubkey (hex)ivHex, ciphertextHex (hex) |
|----------|--------------------------|
| Returns | Uint8Array (decrypted plaintext) |
Example:
const enc = await iqlabs.crypto.dhEncrypt(recipientPubHex, new TextEncoder().encode('secret'));
const dec = await iqlabs.crypto.dhDecrypt(myPrivKey, enc.senderPub, enc.iv, enc.ciphertext);passwordEncrypt() / passwordDecrypt()
| Parameters (encrypt) | password: stringplaintext: Uint8Array |
|----------|--------------------------|
| Returns (encrypt) | { salt, iv, ciphertext } (all hex) |
| Parameters (decrypt) | password, saltHex, ivHex, ciphertextHex |
| Returns (decrypt) | Uint8Array |
Example:
const enc = await iqlabs.crypto.passwordEncrypt('my-password', new TextEncoder().encode('secret'));
const dec = await iqlabs.crypto.passwordDecrypt('my-password', enc.salt, enc.iv, enc.ciphertext);multiEncrypt() / multiDecrypt()
| Parameters (encrypt) | recipientPubHexes: string[]plaintext: Uint8Array |
|----------|--------------------------|
| Returns (encrypt) | { recipients: RecipientEntry[], iv, ciphertext } |
| Parameters (decrypt) | privKey: Uint8ArraypubKeyHex: your pubkey (hex)encrypted: the MultiEncryptResult |
| Returns (decrypt) | Uint8Array |
Example:
const enc = await iqlabs.crypto.multiEncrypt(
[alicePubHex, bobPubHex, carolPubHex],
new TextEncoder().encode('group secret'),
);
// Each recipient decrypts with their own key
const plaintext = await iqlabs.crypto.multiDecrypt(alicePrivKey, alicePubHex, enc);Environment Settings
setRpcUrl()
Override the RPC URL used by all reader functions. Default: https://rpc.sepolia.org. Falls back to env vars IQLABS_RPC_ENDPOINT, ETHEREUM_RPC_URL, or RPC_URL if unset.
| Parameters | url: Ethereum RPC URL (string) |
|----------|--------------------------|
| Returns | void |
Example:
iqlabs.setRpcUrl('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');Note: Writers (
signer-based functions) use the provider attached to theSigner, not this URL.setRpcUrlonly affects readers.
getRpcUrl()
| Returns | string — the currently configured RPC URL |
console.log(iqlabs.getRpcUrl());User Metadata
updateUserMetadata()
| Parameters | signer: ethers.Signermetadata: string \| Uint8Array |
|----------|--------------------------|
| Returns | Transaction hash (string) |
Example:
await iqlabs.writer.updateUserMetadata(signer, JSON.stringify({ name: 'Alice', bio: 'gm' }));Advanced Functions
These are advanced/internal helpers; this doc lists them only. See API docs (in progress) for details.
prepareUpload()/uploadLinkedList()/toChunks()(writer)manageRowData()(writer) — overwrite a specific row bytargetTxmanageTableCreators()(writer)readUserState()(reader)fetchTableMeta()(reader)readSendCodeChain()/walkCalldataChain()/isEnd()(reader)deriveDmSeed()(utils)hexToBytes()/bytesToHex()/validatePubKey()(crypto)
Network Selection
The SDK supports multiple EVM networks. The active network determines both the RPC endpoint and the deployed contract address that readers/writers talk to. Default is sepolia.
setNetwork()
Switch the active network mode. Call this once at app startup (e.g. on page load) before any reader/writer call.
| Parameters | mode: "sepolia" \| "monad"rpcUrl: optional override (string) — defaults to the chain's public RPC |
|----------|--------------------------|
| Returns | void |
Example:
import iqlabs from '@iqlabs-official/ethereum-sdk';
// Sepolia (default — usually no call needed)
iqlabs.setNetwork('sepolia');
// Monad mainnet
iqlabs.setNetwork('monad');
// Or with a custom RPC (e.g. Alchemy)
iqlabs.setNetwork('monad', 'https://your-alchemy-monad-url');getNetwork()
| Returns | "sepolia" \| "monad" — the currently active network |
assertChainMatches()
Throws if the configured RPC's chainId doesn't match the active network mode. Use defensively before sending a transaction if you don't fully control the user's RPC.
await iqlabs.assertChainMatches(signer);Supported Networks
| Mode | Chain ID | Currency | Default RPC |
|------|---------:|----------|-------------|
| sepolia | 11155111 | ETH | https://rpc.sepolia.org |
| monad | 143 | MON | https://rpc.monad.xyz |
Contract
Deployed addresses (resolved automatically from setNetwork()):
| Network | Address |
|---------|---------|
| Sepolia | 0xB1C16271954c7238672c3666FD22Ee14C6d065Db |
| Monad | 0xeFd9376835076Bf8d83826F6A2277BB5362Cd893 |
To pin a specific deployment manually, pass the address directly:
import { getContract } from '@iqlabs-official/ethereum-sdk/dist/contract';
const c = getContract(signer, '0xYourDeployment');Migrating from v0.1.0: the default Sepolia address changed in v0.1.1 (new deployment with IQ-discount support and per-network fee setters). If you need to read data from the v0.1.0 contract (
0xa580a977a3103565993531d19536220A54783397), pass it explicitly togetContract().
