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-sdk2.1. Advanced Functions (list only)
These are the key concepts to know before using the IQLabs Ethereum SDK.
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.
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
codeIn(): upload data and get a transaction hashreadCodeIn(): read data back from a transaction hash
An on-chain record per user address, managed by the contract, not a separate account/PDA.
- 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
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).
An on-chain relationship between two addresses (friends, DM channels, etc.).
- 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.
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
Store JSON data in tables like a database.
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.
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
Tables can be gated so that only users holding a specific ERC-20 token or ERC-721 collection can write data.
| 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 |
- 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?: {
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
}- 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
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.
- 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.
Users can derive a deterministic X25519 keypair from their wallet signature using deriveX25519Keypair(). Their wallet is the key, no separate keystore.
deriveX25519Keypair(): derive encryption keypair from walletdhEncrypt()/dhDecrypt(): single-recipient encryptionpasswordEncrypt()/passwordDecrypt(): password-based encryptionmultiEncrypt()/multiDecrypt(): multi-recipient encryption
| 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)}%`),
);| 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'| 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.
| 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);| 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'| 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() }),
);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));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');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');| 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 },
);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) |
| 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: 'alice@example.com',
}));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.
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));| 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));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})`);
});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);| Parameters | recipientPubHex: recipient X25519 pubkey (hex)plaintext: data to encrypt (Uint8Array) |
|---|---|
| Returns | { senderPub, iv, ciphertext } (all hex) |
| 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);| 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);| 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);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.
| Returns | string, the currently configured RPC URL |
console.log(iqlabs.getRpcUrl());| Parameters | signer: ethers.Signermetadata: string | Uint8Array |
|---|---|
| Returns | Transaction hash (string) |
Example:
await iqlabs.writer.updateUserMetadata(signer, JSON.stringify({ name: 'Alice', bio: 'gm' }));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)
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.
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" | "monadTestnet"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');
// Monad testnet (free MON from faucet.monad.xyz)
iqlabs.setNetwork('monadTestnet');
// Or with a custom RPC (e.g. Alchemy)
iqlabs.setNetwork('monad', 'https://your-alchemy-monad-url');| Returns | "sepolia" \| "monad" \| "monadTestnet", the currently active network |
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);| Mode | Chain ID | Currency | Default RPC |
|---|---|---|---|
sepolia |
11155111 | ETH | https://rpc.sepolia.org |
monad |
143 | MON | https://rpc.monad.xyz |
monadTestnet |
10143 | MON | https://testnet-rpc.monad.xyz |
For local development on Monad without spending real MON, use monadTestnet and fund your wallet from faucet.monad.xyz. The testnet contract has the same ABI and fees as mainnet, so it's an exact rehearsal. When ready, switch to monad with no other code changes.
Deployed addresses (resolved automatically from setNetwork()):
| Network | Address |
|---|---|
| Sepolia | 0xB1C16271954c7238672c3666FD22Ee14C6d065Db |
| Monad | 0xeFd9376835076Bf8d83826F6A2277BB5362Cd893 |
| Monad Testnet | 0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223 |
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().