# Ethereum SDK Source: https://iqlabs.mintlify.app/docs-ethereum Core concepts and functions for the IQLabs Ethereum SDK 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()`](#setnetwork). See [Network](#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 ```bash theme={null} npm i @iqlabs-official/ethereum-sdk ``` 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()`](#setnetwork) typically once at app startup. | Mode | Chain ID | Currency | Contract | Default RPC | | ----------- | -------: | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `sepolia` | 11155111 | ETH | [`0x246A08D9fdD9b3990A88eD1f2DF1A87239839F07`](https://sepolia.etherscan.io/address/0x246A08D9fdD9b3990A88eD1f2DF1A87239839F07) | `https://rpc.sepolia.org` | | `monad` | 143 | MON | [`0x7ae06f87Cf93606DA2BD6A281afB28028cAE233D`](https://monadvision.com/address/0x7ae06f87Cf93606DA2BD6A281afB28028cAE233D) | `https://rpc.monad.xyz` | | `robinhood` | 4663 | ETH | [`0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223`](https://robinhoodchain.blockscout.com/address/0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223) | `https://rpc.mainnet.chain.robinhood.com` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); // or 'robinhood', or 'sepolia' (default) ``` See the [Robinhood Chain guide](/docs-robinhood) 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) ```typescript theme={null} import { Wallet, JsonRpcProvider } from 'ethers'; const provider = new JsonRpcProvider('https://rpc.sepolia.org'); const signer = new Wallet(privateKey, provider); ``` ### Browser (MetaMask / injected wallet) ```typescript theme={null} import { BrowserProvider } from 'ethers'; const provider = new BrowserProvider(window.ethereum); await provider.send('eth_requestAccounts', []); const signer = await provider.getSigner(); ``` Reader functions don't need a signer. They use the RPC URL configured via [`setRpcUrl()`](#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 #### Key related functions * [`codeIn()`](#codein): upload data and get a transaction hash * [`readCodeIn()`](#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()`](#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)`](#derivedmseed) (sorted lowercase + keccak256), so either party can recompute it. #### Key related functions * [`requestConnection()`](#requestconnection): send a friend request (creates pending) * [`manageConnection()`](#manageconnection): approve/block/unblock a request * [`readConnection()`](#readconnection): check current relationship status * [`writeConnectionRow()`](#writeconnectionrow): exchange messages/data with a connected party * [`fetchUserConnections()`](#fetchuserconnections): fetch all of a user's connections *** ### Database Tables Store JSON data in tables like a database. #### How are tables created? 1. Call [`initializeDbRoot()`](#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()`](#createtable) (public) or [`createTable(..., isPrivate=true)`](#createtable) (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()`](#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. #### Key related functions * [`initializeDbRoot()`](#initializedbroot): claim a `dbRootId` and become its creator * [`manageTableCreators()`](#managetablecreators): set the public/private creator allowlists * [`createTable()`](#createtable) / [`updateTable()`](#updatetable): create or modify a table's schema/gate * [`writeRow()`](#writerow): append a row * [`readTableRows()`](#readtablerows): read rows from a table * [`getTablelistFromRoot()`](#gettablelistfromroot): 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 (balance >= 1) from the specified collection contract | #### 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 ```typescript theme={null} gate?: { tokenAddress: string; // ERC-20 or ERC-721 contract (ZeroAddress for public) amount: number | bigint; // min balance (raw units, ERC-20 only; ignored for ERC-721) gateType: 0 | 1; // 0 = ERC-20 token, 1 = ERC-721 NFT } ``` 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()`](#initializedbroot)) controls who is allowed to create tables. #### Two levels of table creation | Type | Function | Listed in `tables` array? | Permission field | | ----------------- | ----------------------------------- | ------------------------------------------------------------------------ | ---------------- | | **Public table** | `createTable(..., isPrivate=false)` | yes, appears in [`getTablelistFromRoot().tables`](#gettablelistfromroot) | `tableCreators` | | **Private table** | `createTable(..., isPrivate=true)` | no, only in `globalTables` (you must know the name) | `extCreators` | 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: ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; await iqlabs.writer.manageTableCreators( signer, 'my-db', [adminWallet1, adminWallet2], // tableCreators: who can create public tables [] // extCreators: empty means anyone can create private tables ); ``` 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: ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const c = iqlabs.contract.getContract(signer); const tx = await c.onboardTable(iqlabs.utils.toSeed('my-db'), 'my-board'); await tx.wait(); ``` 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()`](#derivex25519keypair). Their wallet *is* the key, no separate keystore. #### Key related functions * [`deriveX25519Keypair()`](#derivex25519keypair): derive encryption keypair from wallet * [`dhEncrypt()`](#dhencrypt) / [`dhDecrypt()`](#dhdecrypt): single-recipient encryption * [`passwordEncrypt()`](#passwordencrypt) / [`passwordDecrypt()`](#passworddecrypt): password-based encryption * [`multiEncrypt()`](#multiencrypt) / [`multiDecrypt()`](#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. | Fee | Default (Sepolia) | Charged on | | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `basicFee` | 0.0001 ETH | `dbCodeIn` / `walletConnectionCodeIn` / `userInventoryCodeIn` **when the payload is inline** (≤ 700 bytes, no `sendCode` chain) | | `linkedListFee` | 0.0003 ETH | same three functions, but **when the payload is chunked** via a `sendCode` linked list | | `tableCreationFee` | 0.0003 ETH | `createTable` / `createPrivateTable` | | `discountFee` | 0.00005 ETH | replaces `basicFee` on the inline path when the signer holds the configured IQ token | **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()` | **Parameters** | `signer`: `ethers.Signer`
`data`: data to upload (string or string\[])
`filename`: optional filename (string, default: `""`)
`filetype`: file type hint (string, default: `""`; coerced to `"text/plain"` on-chain)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; // Upload inline data (small) const txHash = await iqlabs.writer.codeIn(signer, 'Hello, blockchain!'); // Upload large data with progress + filename const txHash2 = await iqlabs.writer.codeIn( signer, longString, 'hello.txt', 'text/plain', (pct) => console.log(`upload: ${pct.toFixed(1)}%`) ); ``` Internally, this fires: 1. (For large data) a series of `sendCode()` calls forming the linked list 2. `userInventoryCodeIn(handle, tailTx, ...)`: **charges the fee here** — `basicFee` if `tailTx === ""` (inline payload), otherwise `linkedListFee` 3. `updateUserTxChainTail(myTxHash)`: free pointer bump *** #### `readCodeIn()` | **Parameters** | `txHash`: transaction hash (string)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ------------------------------------------------------------------------------------------------------------- | | **Returns** | `{ metadata: { handle, typeField, offset, beforeUserTx }, data: string }` | ```typescript theme={null} 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' ``` 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()` | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: 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) | ```typescript theme={null} 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 computed automatically from `(senderAddress, receiverAddress)` via [`deriveDmSeed()`](#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 | **Parameters** | `signer`: `ethers.Signer`
`otherParty`: counterparty address (string)
`dbRootId`: database ID (string)
`newStatus`: new status (`0` \| `1` \| `2`) | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; // Approve a friend request await iqlabs.writer.manageConnection(signer, friendAddress, 'my-db', 1); // Block 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' }` | ```typescript theme={null} 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' | 'unknown' ``` `'unknown'` means the connection record does not exist on-chain. *** #### `writeConnectionRow()` | **Parameters** | `signer`: `ethers.Signer`
`otherParty`: counterparty address (string)
`dbRootId`: database ID (string)
`rowJson`: JSON data (string)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} 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() }) ); ``` 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. | **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; `data` is parsed JSON when possible) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const messages = await iqlabs.reader.readConnectionRows( 'my-db', myAddress, friendAddress, { limit: 50 } ); messages.forEach(m => console.log(m.txHash, m.data)); ``` *** #### `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). | **Parameters** | `userAddress`: user address (string) | | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ connectionKey: string, partyA: string, partyB: string, status: 'pending' \| 'approved' \| 'blocked' \| 'unknown' }>` | ```typescript theme={null} 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'); ``` Unlike the Solana SDK's `fetchUserConnections`, the result does **not** include `dbRootId`, `requester`, `blocker`, or `timestamp`. To get those, call [`readConnection()`](#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. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string) | | -------------- | --------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} await iqlabs.writer.initializeDbRoot(signer, 'my-db'); ``` Reverts if the `dbRootId` was already initialized. *** #### `manageTableCreators()` Set both creator allowlists. **Caller must be the DbRoot creator.** | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableCreators`: addresses allowed to create public tables (`string[]`)
`extCreators`: addresses allowed to create private tables (`string[]`) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction hash (string) | ```typescript theme={null} await iqlabs.writer.manageTableCreators( signer, 'my-db', [admin1, admin2], // public-table allowlist [] // anyone can create private tables ); ``` 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()`](#setroottablecreationfee)). The fee is split 31% to `feeReceiver` and 69% to `DbRoot.creator`. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: 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 Gating](#token--collection-gating)
`writers`: optional writer whitelist (`string[]`, default: `[]`)
`isPrivate`: create private table (boolean, default: `false`) | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { ZeroAddress, parseEther } from 'ethers'; // Public table: appears in getTablelistFromRoot().tables await iqlabs.writer.createTable( signer, 'my-db', 'users', ['name', 'email'], 'user_id' ); // Private table: only in globalTables, callers must know the name await iqlabs.writer.createTable( signer, 'my-db', 'secret_log', ['entry'], 'entry_id', [], undefined, [], true // <-- isPrivate = true ); // ERC-20 gated table: must hold >= 100 tokens (assuming 18 decimals) await iqlabs.writer.createTable( signer, 'my-db', 'vip', ['name'], 'user_id', [], { tokenAddress: erc20Address, amount: parseEther('100'), gateType: 0 } ); // ERC-721 gated table: must hold any 1 NFT from collection await iqlabs.writer.createTable( signer, 'my-db', 'holders', ['name'], 'user_id', [], { tokenAddress: nftAddress, amount: 0, gateType: 1 } ); // Writer-restricted table (only listed addresses can writeRow) await iqlabs.writer.createTable( signer, 'my-db', 'staff_only', ['note'], 'note_id', [], undefined, [staff1, staff2] ); ``` `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. | **Parameters** | Same as [`createTable()`](#createtable) **minus** `isPrivate`. | | -------------- | -------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} // Tighten the gate on an existing table await iqlabs.writer.updateTable( signer, 'my-db', 'vip', ['name'], 'user_id', [], { tokenAddress: erc20Address, amount: parseEther('500'), gateType: 0 } ); ``` *** #### `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`. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableName`: table name (string)
`rowJson`: JSON row data (string)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} await iqlabs.writer.writeRow(signer, 'my-db', 'users', JSON.stringify({ id: 1, name: 'Alice', email: 'alice@example.com' })); ``` 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. | **Parameters** | `dbRootId`: database ID (string)
`tableName`: table name (string)
`options`: `{ limit?: number }` (optional) | | -------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ txHash: string, data: any }>` (most recent first; `data` is the parsed JSON object, or the raw string if parsing fails) | ```typescript theme={null} const rows = await iqlabs.reader.readTableRows('my-db', 'users', { limit: 50 }); rows.forEach(r => console.log(r.txHash, r.data)); ``` *** #### `getTablelistFromRoot()` Returns the public and global table lists for a DbRoot, plus the root's `tableCreationFee` override state. | **Parameters** | `dbRootId`: database ID (string) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | `{ creator: string, tables: TableEntry[], globalTables: TableEntry[], tableCreationFeeOverride: bigint, tableCreationFeeIsSet: boolean }` where `TableEntry = { name: string, seedHex: string }` | ```typescript theme={null} const root = await iqlabs.reader.getTablelistFromRoot('my-db'); console.log('Creator:', root.creator); root.tables.forEach(t => console.log(`public: ${t.name} (${t.seedHex})`)); root.globalTables.forEach(t => console.log(`all: ${t.name} (${t.seedHex})`)); // Per-root tableCreationFee override. If `tableCreationFeeIsSet` is false, // the contract uses the global default and the IQ owner can change it at // any time. If true, the root creator has pinned the value (including 0). if (root.tableCreationFeeIsSet) { console.log(`Root-pinned fee: ${root.tableCreationFeeOverride} wei`); } else { console.log('Using global tableCreationFee (subject to IQ owner changes)'); } ``` `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. | **Parameters** | `signer`: `ethers.Signer` (must be the contract owner)
`newFee`: new fee in wei (`bigint`) | | -------------- | ----------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import { ethers } from 'ethers'; import iqlabs from '@iqlabs-official/ethereum-sdk'; // Set default to 0.0005 ETH await iqlabs.writer.setTableCreationFee(signer, ethers.parseEther('0.0005')); ``` *** #### `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()`](#clearroottablecreationfee). | **Parameters** | `signer`: `ethers.Signer` (must be `DbRoot.creator`)
`dbRootId`: database ID (string)
`newFee`: pinned fee in wei (`bigint`) | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} // Root creator pins their app's room creation at 0.001 ETH and locks it in await iqlabs.writer.setRootTableCreationFee( signer, 'my-db', ethers.parseEther('0.001'), ); // Or make rooms permanently free under this root, even if IQ raises the default await iqlabs.writer.setRootTableCreationFee(signer, 'my-db', 0n); ``` 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. | **Parameters** | `signer`: `ethers.Signer` (must be `DbRoot.creator`)
`dbRootId`: database ID (string) | | -------------- | ------------------------------------------------------------------------------------------ | | **Returns** | Transaction hash (string) | ```typescript theme={null} await iqlabs.writer.clearRootTableCreationFee(signer, 'my-db'); ``` *** #### `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`. | **Parameters** | `signer`: `ethers.Signer` (must be current `DbRoot.creator`)
`dbRootId`: database ID (string)
`newCreator`: address receiving the role (non-zero) | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} await iqlabs.writer.transferDbRootCreator(signer, 'my-db', '0xNewOwner...'); ``` 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()`](#codein)). | **Parameters** | `userAddress`: user address (string)
`options`: `{ limit?: number }` (optional) | | -------------- | ---------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ txHash: string, handle: string, tailTx: string, typeField: string, offset: string }>` | ```typescript theme={null} const myFiles = await iqlabs.reader.fetchInventoryTransactions(myAddress, { limit: 20 }); myFiles.forEach(tx => { console.log(`${tx.txHash}: ${tx.handle} (${tx.typeField})`); }); ``` 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()`](#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). | **Parameters** | `signMessage`: `(msg: Uint8Array) => Promise` | | -------------- | --------------------------------------------------------- | | **Returns** | `{ privKey: Uint8Array, pubKey: Uint8Array }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { getBytes } from 'ethers'; // ethers `signMessage` returns a hex string; convert it to bytes for the SDK. const sign = async (msg: Uint8Array) => getBytes(await signer.signMessage(msg)); const { privKey, pubKey } = await iqlabs.crypto.deriveX25519Keypair(sign); ``` *** #### `dhEncrypt()` | **Parameters** | `recipientPubHex`: recipient's X25519 public key (hex string)
`plaintext`: data to encrypt (Uint8Array) | | -------------- | ------------------------------------------------------------------------------------------------------------ | | **Returns** | `{ senderPub: string, iv: string, ciphertext: string }` (all hex) | #### `dhDecrypt()` | **Parameters** | `privKey`: recipient's private key (Uint8Array)
`senderPubHex`: sender's ephemeral public key (hex string)
`ivHex`: IV (hex string)
`ciphertextHex`: ciphertext (hex string) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Uint8Array` (decrypted plaintext) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const enc = await iqlabs.crypto.dhEncrypt( recipientPubHex, new TextEncoder().encode('secret message') ); const dec = await iqlabs.crypto.dhDecrypt( myPrivKey, enc.senderPub, enc.iv, enc.ciphertext ); console.log(new TextDecoder().decode(dec)); ``` *** #### `passwordEncrypt()` | **Parameters** | `password`: password (string)
`plaintext`: data to encrypt (Uint8Array) | | -------------- | ---------------------------------------------------------------------------- | | **Returns** | `{ salt: string, iv: string, ciphertext: string }` (all hex) | #### `passwordDecrypt()` | **Parameters** | `password`: password (string)
`saltHex`: salt (hex string)
`ivHex`: IV (hex string)
`ciphertextHex`: ciphertext (hex string) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Uint8Array` (decrypted plaintext) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const enc = await iqlabs.crypto.passwordEncrypt( 'my-password', new TextEncoder().encode('secret data') ); const dec = await iqlabs.crypto.passwordDecrypt( 'my-password', enc.salt, enc.iv, enc.ciphertext ); ``` *** #### `multiEncrypt()` | **Parameters** | `recipientPubHexes`: recipient public keys (string\[])
`plaintext`: data to encrypt (Uint8Array) | | -------------- | ----------------------------------------------------------------------------------------------------- | | **Returns** | `{ recipients: RecipientEntry[], iv: string, ciphertext: string }` | #### `multiDecrypt()` | **Parameters** | `privKey`: your private key (Uint8Array)
`pubKeyHex`: your public key (hex string)
`encrypted`: the `MultiEncryptResult` object | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Uint8Array` (decrypted plaintext) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; // Encrypt for multiple recipients 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); ``` Duplicate recipients in `recipientPubHexes` are deduplicated automatically. *** ### User Metadata #### `updateUserMetadata()` Store arbitrary metadata under the caller's address. Overwrites any previous value. | **Parameters** | `signer`: `ethers.Signer`
`metadata`: `string \| Uint8Array` | | -------------- | ----------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; await iqlabs.writer.updateUserMetadata( signer, JSON.stringify({ name: 'Alice', bio: 'gm' }) ); ``` 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). | **Parameters** | `mode`: `'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood'`
`rpcUrl`: optional override (string). Omitted → use the mode's default RPC. | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | void | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; // Switch to Monad mainnet (uses default RPC https://rpc.monad.xyz) iqlabs.setNetwork('monad'); // With a custom RPC (e.g. Alchemy / your own node) iqlabs.setNetwork('monad', 'https://your-monad-rpc'); ``` #### `getNetwork()` Returns the currently active network mode. \| **Returns** | `'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood'` | ```typescript theme={null} console.log(iqlabs.getNetwork()); // 'sepolia' (default) ``` #### `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). | **Parameters** | `providerOrSigner`: optional `Provider` or `Signer`. Omitted → uses the SDK's reader provider. | | -------------- | ---------------------------------------------------------------------------------------------- | | **Returns** | `Promise` (throws on mismatch) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.assertChainMatches(signer); // throws if signer's RPC isn't chainId 143 ``` #### `setRpcUrl()` Override only the reader RPC URL, without changing the active network mode. Most users should prefer [`setNetwork()`](#setnetwork) `setRpcUrl` exists for cases where you want to switch RPC providers (e.g. fallback to Alchemy) while staying on the same chain. | **Parameters** | `url`: Ethereum RPC URL (string) | | -------------- | -------------------------------- | | **Returns** | void | ```typescript theme={null} iqlabs.setRpcUrl('https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'); ``` 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` | ```typescript theme={null} console.log(iqlabs.getRpcUrl()); ``` *** ## 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. | **Module** | `writer` | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableName`: table name (string)
`rowJson`: JSON row data (string)
`targetTx`: tx hash of the row being managed (string) | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; await iqlabs.writer.manageRowData( signer, 'my-db', 'users', JSON.stringify({ id: 1, name: 'Updated Name' }), originalRowTxHash ); ``` 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()`](#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. | **Module** | `reader` | | -------------- | --------------------------------------------------- | | **Parameters** | `userAddress`: user address (string) | | **Returns** | `{ metadata: string \| null, txChainTail: string }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const state = await iqlabs.reader.readUserState(myAddress); console.log('metadata:', state.metadata); // UTF-8 decoded, or null if empty console.log('chain tail:', state.txChainTail); ``` *** #### `fetchTableMeta()` Read a table's full schema and current `txChainTail`. | **Module** | `reader` | | -------------- | ---------------------------------------------------------------------------------------------------- | | **Parameters** | `dbRootId`: database ID (string)
`tableName`: table name (string) | | **Returns** | Raw table struct (`name`, `columnNames`, `idCol`, `extKeys`, `gate`, `writers`, `txChainTail`, etc.) | *** #### `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. | **Module** | `utils` | | -------------- | --------------------------------------------------------------------- | | **Parameters** | `userA`: first address (string)
`userB`: second address (string) | | **Returns** | `string` (32-byte hex with `0x` prefix) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; const seed1 = iqlabs.utils.deriveDmSeed(walletA, walletB); const seed2 = iqlabs.utils.deriveDmSeed(walletB, walletA); // seed1 === seed2 (order doesn't matter) ``` *** #### 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`). | Function | Returns | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `iqlabs.utils.getBasicFee(signerOrProvider)` | `bigint` — fee for inline code-in writes | | `iqlabs.utils.getLinkedListFee(signerOrProvider)` | `bigint` — fee for chunked code-in writes | | `iqlabs.utils.getTableCreationFee(signerOrProvider)` | `bigint` — contract-wide `tableCreationFee` default | | `iqlabs.utils.resolveCodeInFee(signerOrProvider, onChainPath)` | `bigint` — mirrors the contract's branch: `""` → basicFee, else linkedListFee | | `iqlabs.utils.getEffectiveTableCreationFee(signerOrProvider, dbRootId)` | `bigint` — root override if pinned, else global default | | `iqlabs.utils.clearFeeCache()` | `void` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; // "Will this write cost me anything I should warn the user about?" const myFee = await iqlabs.utils.resolveCodeInFee(signer, '' /* inline */); // "How much will this app's room creation cost a user?" const roomFee = await iqlabs.utils.getEffectiveTableCreationFee(provider, 'my-db'); ``` *** ### Crypto Utilities `hexToBytes(hex)`, `bytesToHex(bytes)`, `validatePubKey(hex, name)` are exported under `iqlabs.crypto` for convenience. # On-Chain Git Source: https://iqlabs.mintlify.app/docs-iqgit GitHub-style repos, sites, and SDK — all stored on Solana On-chain Git stores commit history, file blobs, and per-commit trees inside Solana inscriptions via the [Solana SDK](/docs-typescript). The CLI, the browser frontend, and the embeddable SDK all read and write the same on-chain data — anything one of them can do, the others can read. ## The stack Three pieces, one source of truth: GitHub workflow from your shell: `iqgit init`, `commit`, `push`, `clone`. Browse and deploy repos from [git.iqlabs.dev](https://git.iqlabs.dev) — no install, just a wallet. Drop on-chain Git into any agent, app, or dApp with one import. *** ## CLI: GitHub on your laptop Use [`@iqlabs-official/iq-git-cli`](https://www.npmjs.com/package/@iqlabs-official/iq-git-cli) when you want the familiar `git` flow but every commit lands on Solana instead of a centralized server. ### Install ```bash theme={null} npm install -g @iqlabs-official/iq-git-cli ``` Once installed, `iqgit` is available globally. ### Push your first repo ```bash theme={null} iqgit init # create local .iqgit/ iqgit create my-app --public # register repo on chain iqgit add . iqgit commit -m "first" iqgit push # uploads blobs + tree + commit row ``` The first write command walks you through: 1. **Wallet** — generate a new Solana keypair, or point at an existing keypair JSON. Stored in `~/.iq-git/wallets/default.json`. 2. **RPC URL** — needed for any chain interaction. Helius works great on the free tier. Saved to `~/.iq-git/.env`. Read-only commands (`clone`, `log`, `registry`) don't prompt for a wallet at all — anyone can pull a public repo with zero setup. ### Clone someone else's repo ```bash theme={null} iqgit clone / ``` `` is a Solana wallet address (the repo creator); `` is the human-readable name they registered. The CLI walks the on-chain commit chain, fetches each blob, and writes the files to disk. ### Commands | Command | Purpose | | ---------------------------------------- | ---------------------------------------------------------- | | `iqgit init` | Create local `.iqgit/` (no chain interaction). | | `iqgit create ` | Register a new repo on chain. | | `iqgit add` / `iqgit reset` | Stage / unstage files for the next commit. | | `iqgit commit -m "..."` | Snapshot staged paths locally. No chain write. | | `iqgit push` | Upload pending commits to chain. **Resume-safe.** | | `iqgit clone /` | Pull the latest snapshot. | | `iqgit restore [commitId]` | Restore working tree to a specific commit. | | `iqgit log` | Print commit history. | | `iqgit status` | 4-tier diff: HEAD vs. pending vs. staged vs. working tree. | | `iqgit registry` | Browse the public on-chain repo gallery. | | `iqgit wallet new\|show\|balance\|repos` | Manage your keypair. | ### How `push` works Each push writes three kinds of records: 1. **Blobs** — file contents, one inscription per unique hash. 2. **Tree** — JSON map of `{ path: { txId, hash } }`, one per commit. 3. **Commit row** — `{ id, message, treeTxId, parentCommitId, timestamp, author }`. `commit` builds these locally; `push` uploads them. Splitting the two means you can batch many commits into a single push and amortize Solana fees. ### Resume on failure `push` is checkpointed end-to-end: * Each blob's `{ hash → txId }` is appended to `.iqgit/upload-cache.json` on success, synchronously flushed. * The tree's txId and the commit row's signature are persisted into the pending commit's `meta.json` between steps. If the push dies partway (network blip, RPC error, Ctrl-C), the next `iqgit push` resumes from the last checkpoint. Already-uploaded blobs are reused from cache instead of being re-inscribed. ### Upload speed Default speed is `light` — the Helius free-tier friendly setting. On a paid RPC, dial it up by preset or by raw RPS / concurrency: ```bash theme={null} # preset name iqgit config speed heavy # save as global default iqgit push --speed extreme # one-off override # raw dials (win over the preset; any subset works) iqgit config rps 80 # global default maxRps iqgit push --rps 120 --concurrency-upload 30 ``` Available presets: `light` | `medium` | `heavy` | `extreme`. Raw flags: `--rps`, `--concurrency`, `--concurrency-upload` (and matching `iqgit config` keys: `rps`, `concurrency`, `concurrencyUpload`). ### Gateway routing Read-heavy commands (`log`, `registry`, `clone`, `status`) route through the IQ Gateway HTTP cache by default, with raw RPC as the final fallback. This sidesteps RPC method limits on bulk table reads. | `GATEWAY_URL` | Behavior | | -------------------- | -------------------------------------------- | | (unset, default) | 3-gateway chain → RPC fallback (recommended) | | `https://my.gateway` | Single override → RPC fallback | | `url1,url2,url3` | Comma list, tried in order → RPC | | `off` | Disable gateway, raw RPC only | Anyone can host their own — see [iq-gateway](https://github.com/IQCoreTeam/iq-gateway). *** ## On-Chain GitHub: git.iqlabs.dev A full GitHub-style frontend, running entirely on top of the on-chain state the CLI writes. **No backend, no database** — every screen pulls straight from Solana. Connect a wallet and you can do everything the CLI does: create repos, browse files, view commit history, edit a file in the browser. Without a wallet, the gallery and every public repo are still fully readable. ### What you can do at [git.iqlabs.dev](https://git.iqlabs.dev) * **Browse the public registry** — gallery of every public repo created through the CLI or the frontend. * **View any repo by `/`** — file tree, README, commit history. URL shape mirrors GitHub. * **Edit in the browser** — connect a wallet, open a repo you own, click a file → editor opens → save → commit lands on chain via the SDK. * **Deploy as a website** — see the next section. ### Deploy a repo as a hosted site Any repo with an `iqpages.json` manifest can be deployed as a static site at `browser.iqlabs.dev/.sol`. The workflow: 1. Add `iqpages.json` (and optionally `iqprofile.json`) to your repo locally. 2. Commit and push via the CLI: ```bash theme={null} iqgit add iqpages.json iqgit commit -m "add pages manifest" iqgit push ``` 3. Open `git.iqlabs.dev//`, switch to the **Pages** tab, and click **Deploy**. 4. The frontend writes an `iqpages` registration on chain. Your site is now served via the [iqpages proxy at browser.iqlabs.dev](#solana-www-browser-iqlabs-dev). The original URL stays the same — visitors never see a redirect. Deploys are signed by the repo owner's wallet. There's no "build server" — the proxy reads your latest commit from chain at request time and serves the matching files. *** ## Solana WWW: browser.iqlabs.dev [browser.iqlabs.dev](https://browser.iqlabs.dev) is the on-chain resolver that ties the whole stack together. One URL space, four dispatchers driven entirely by Solana data: * **`browser.iqlabs.dev/.sol`** — SNS name pointing at an iqpages deployment. The proxy serves the site in place; the URL stays the same. * **`browser.iqlabs.dev/`** — wallet, table PDA, or git repo address. The resolver dispatches by shape. * **`browser.iqlabs.dev/`** — transaction inspector with in-browser payload viewer. * **`browser.iqlabs.dev//`** — short link that redirects to [git.iqlabs.dev](https://git.iqlabs.dev) for the repo view. For on-chain Git specifically: * Hand someone `browser.iqlabs.dev/.sol` and they get your site, served from your latest commit. * Hand them `browser.iqlabs.dev//` and they get the repo page on git.iqlabs.dev. * Both URLs are stable as long as your wallet stays the same — no DNS, no hosting bill, no expiry. *** ## Embed on-chain Git: the SDK Use [`@iqlabs-official/git-sdk`](https://www.npmjs.com/package/@iqlabs-official/git-sdk) when you're building your own agent, app, or dApp and want it to read or write the same on-chain Git repos that the CLI and git.iqlabs.dev already use. ### Install ```bash theme={null} npm install @iqlabs-official/git-sdk ``` Peer deps: `@solana/web3.js`, `@iqlabs-official/solana-sdk` (aliased as `iqlabs-sdk`), `buffer`. ### Browser (frontend / dApp) ```ts theme={null} import { GitClient, readRegistryPage } from "@iqlabs-official/git-sdk/browser"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; const { connection } = useConnection(); const wallet = useWallet(); // Read-only — no wallet required. const entries = await readRegistryPage(connection, { limit: 50 }); // Write — needs a connected wallet adapter. const client = new GitClient({ connection, signer: { publicKey: wallet.publicKey!, signTransaction: wallet.signTransaction!, signAllTransactions: wallet.signAllTransactions!, }, }); await client.createRepo({ name: "my-repo", description: "hello on-chain", isPublic: true, timestamp: Date.now(), }); ``` ### Node (CLI / agent / server) ```ts theme={null} import { GitClient } from "@iqlabs-official/git-sdk/node"; import { Connection, Keypair } from "@solana/web3.js"; const connection = new Connection(process.env.SOLANA_RPC_ENDPOINT!); const signer = Keypair.fromSecretKey(/* your secret key */); const client = new GitClient({ connection, signer }); await client.commit("my-repo", "initial", scan); ``` ### Pick the right entry | Import | When | | ---------------------------------- | ----------------------------------------------------------------- | | `@iqlabs-official/git-sdk` | Types and pure functions only. No SHA-256 backend installed. | | `@iqlabs-official/git-sdk/browser` | Installs SubtleCrypto SHA-256. Use this in browser apps. | | `@iqlabs-official/git-sdk/node` | Installs `node:crypto` SHA-256. Use this in CLI / server / agent. | Import the platform entry **exactly once** before calling any function that hashes content (`commit`, `status`, etc.). ### API surface * `GitClient` — high-level workflows: `createRepo`, `commit`, `checkout`, `clone`, `log`, `status`. * `readOwnerRepos`, `readRegistryPage` — owner repo list + public gallery. * `readLatestCommit`, `readCommitHistory` — direct commit-table reads. * `loadTree`, `loadBlob` — pull a stored `tree.json` or file blob by tx signature. * `bootstrapRegistry` — one-time admin call to initialize the global registry table on a fresh network. `SignerInput` from `@iqlabs-official/solana-sdk` is accepted everywhere a signer is needed: a `Keypair`, a web3.js `Signer`, or a wallet adapter object with `signTransaction` / `signAllTransactions`. ### Tuning upload speed Blob and tree uploads forward to `iqlabs.writer.codeIn`, which sets RPS and concurrency from a `SESSION_SPEED_PROFILES` preset. The git-sdk default is `"light"` (Helius free-tier friendly). You can either pick a preset or pass raw dials directly: ```ts theme={null} // 1. Pick a preset name. const client = new GitClient({ connection, signer, speed: "heavy" }); // 2. Or override raw RPS / concurrency. Missing keys fall back to the // default preset values. const client = new GitClient({ connection, signer, speed: { maxRps: 80, maxConcurrencyUpload: 30 }, }); // Per-call override (wins over the client-level default): await client.commit("my-repo", "tweak", scan, { speed: "extreme" }); await client.commit("my-repo", "tweak", scan, { speed: { maxRps: 120, maxConcurrencyUpload: 40 }, }); ``` Available presets: `light` | `medium` | `heavy` | `extreme`. Raw object accepts `maxRps`, `maxConcurrency`, `maxConcurrencyUpload`. ### Use cases * **AI agents** — give an agent a wallet and `GitClient.commit()` and it can push reproducible artifacts on chain after every run. * **dApps** — let users save app state, configs, or content packs in versioned repos they own. * **Backend tools** — replace S3/GitHub uploads in your build pipeline with on-chain inscriptions that anyone can clone without auth. *** ## How the pieces fit together ```mermaid theme={null} flowchart TB chain["Solana inscriptions
(blobs + trees + commit rows)"] cli["iq-git CLI"] web["git.iqlabs.dev"] sdk["git-sdk (npm)"] term["terminal"] browser["browser"] agents["agents / apps / dApps"] chain --- cli chain --- web chain --- sdk cli --> term web --> browser sdk --> agents classDef source fill:#0f1115,stroke:#00ff00,stroke-width:2px,color:#00ff00; classDef surface fill:#0f1115,stroke:#00cc00,stroke-width:1.5px,color:#e6ffe6; classDef sink fill:#0f1115,stroke:#444,stroke-width:1px,color:#bbb; class chain source; class cli,web,sdk surface; class term,browser,agents sink; ``` All three speak the **same on-chain format**. A commit you push from the CLI is browsable on git.iqlabs.dev, deployable as an iqpages site on browser.iqlabs.dev, and readable from any app that imports git-sdk. *** ## Where to next The TypeScript SDK that git-sdk and iq-git-cli are built on. Same on-chain format from Python. # Monad SDK Source: https://iqlabs.mintlify.app/docs-monad Complete guide to building on-chain apps on Monad with the IQLabs Ethereum SDK This document is in progress and will be refined. **Monad mainnet** and **Monad testnet** are both live and supported. Switch with [`setNetwork('monad')`](#setnetwork) or [`setNetwork('monadTestnet')`](#testnet). New to Monad? Start on testnet with free MON see [Testnet](#testnet). The IQLabs Ethereum SDK works on Monad out of the box. Same API, one line to switch networks. Store data on-chain, build databases, send encrypted DMs, gate content by token ownership. Monad is fast and cheap, so everything just works better. ## Installation ```bash theme={null} npm i @iqlabs-official/ethereum-sdk ``` The SDK ships as CommonJS for Node.js and works in browsers via any modern bundler. *** ## Network The SDK ships with three network modes. Set the one you want once at app startup with [`setNetwork()`](#setnetwork). | Mode | Chain ID | Currency | Contract | Default RPC | | -------------- | -------: | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | | `sepolia` | 11155111 | ETH | [`0x246A08D9fdD9b3990A88eD1f2DF1A87239839F07`](https://sepolia.etherscan.io/address/0x246A08D9fdD9b3990A88eD1f2DF1A87239839F07) | `https://rpc.sepolia.org` | | `monad` | 143 | MON | [`0x7ae06f87Cf93606DA2BD6A281afB28028cAE233D`](https://monadvision.com/address/0x7ae06f87Cf93606DA2BD6A281afB28028cAE233D) | `https://rpc.monad.xyz` | | `monadTestnet` | 10143 | MON | [`0x3379883538C068978e199472b5D127055c734867`](https://testnet.monadexplorer.com/address/0x3379883538C068978e199472b5D127055c734867) | `https://testnet-rpc.monad.xyz` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); // mainnet call once at app startup // or, for free testing: iqlabs.setNetwork('monadTestnet'); ``` The contract is identical across all three networks (same ABI, same functions). Only the address, chain, and fees differ. The SDK reads fees on-chain, so you never hardcode them. *** ## Testnet Develop and test for free on **Monad testnet** before touching mainnet. The contract is deployed and verified there with the same fees as mainnet, so testnet is an exact rehearsal. ### 1. Get free testnet MON You need testnet MON to pay fees. Grab some from the official faucet: 1. Go to [faucet.monad.xyz](https://faucet.monad.xyz/) 2. Paste your wallet address 3. Request testnet MON arrives in seconds The faucet hands out a modest amount per request (about **20 MON** at a time, with roughly **25 MON** extra if you connect a social account). Testnet fees are 1/10 of mainnet (`basicFee` 0.65 MON, `linkedListFee` 1.95 MON, `tableCreationFee` 1.95 MON), so one faucet claim is enough for plenty of writes. Testnet MON has no real value and only works on chain ID `10143`. Never send it to a mainnet address. ### 2. Point the SDK at testnet ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monadTestnet'); // resolves the testnet contract + RPC ``` That's the only change. Every reader/writer call now targets the testnet deployment. `codeIn`, `writeRow`, connections, and encryption all behave exactly as on mainnet. ### 3. Wallet setup for testnet For a Node.js signer, point your provider at the testnet RPC: ```typescript theme={null} import { Wallet, JsonRpcProvider } from 'ethers'; const provider = new JsonRpcProvider('https://testnet-rpc.monad.xyz'); const signer = new Wallet(process.env.PRIVATE_KEY!, provider); ``` For MetaMask, add the testnet network: | Field | Value | | --------------- | ----------------------------------- | | Network Name | Monad Testnet | | RPC URL | `https://testnet-rpc.monad.xyz` | | Chain ID | `10143` | | Currency Symbol | `MON` | | Block Explorer | `https://testnet.monadexplorer.com` | Use [`assertChainMatches(signer)`](#assertchainmatches) after `setNetwork('monadTestnet')` to catch a signer that's still pointed at the wrong chain before you send a transaction. ### 4. Going to mainnet When your app works on testnet, switch one line: ```typescript theme={null} iqlabs.setNetwork('monad'); // mainnet uses real MON ``` No other code changes. The same `dbRootId`s and table names will be **separate** on mainnet (different chain, different contract state), so you re-create them there. *** ## Wallet / Signer Setup Every writer function takes an `ethers.Signer`. Two ways to get one: ### Node.js (private key) ```typescript theme={null} import { Wallet, JsonRpcProvider } from 'ethers'; const provider = new JsonRpcProvider('https://rpc.monad.xyz'); const signer = new Wallet(process.env.PRIVATE_KEY!, provider); ``` ### Browser (MetaMask / injected wallet) Add Monad to MetaMask first: | Field | Value | | --------------- | ----------------------- | | Network Name | Monad | | RPC URL | `https://rpc.monad.xyz` | | Chain ID | `143` | | Currency Symbol | `MON` | Then connect: ```typescript theme={null} import { BrowserProvider } from 'ethers'; const provider = new BrowserProvider(window.ethereum); await provider.send('eth_requestAccounts', []); const signer = await provider.getSigner(); ``` Reader functions don't need a signer they use the RPC configured via [`setRpcUrl()`](#setrpcurl). *** ## Core Concepts *** ### Data Storage (Code In) Store any data (files, text, JSON) directly on-chain. Data lives in transaction calldata, not on a centralized server. 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, uploaded via `sendCode()` calls in batches up to \~96 KB each, and the tail tx hash is recorded #### Key related functions * [`codeIn()`](#codein): upload data and get a transaction hash * [`readCodeIn()`](#readcodein): read data back from a transaction hash *** ### User State Each address has an on-chain record managed by the contract. No separate account to initialize. #### What gets stored? * User-set metadata (name, profile, bio, anything you serialize) * `userTxChainTail`: the most recent inventory write, used as the head of the user's tx-chain #### When is it created? No explicit "create user" step. The first [`codeIn()`](#codein) call writes both the inventory entry and advances the chain tail. Each `codeIn()` charges `basicFee` (6.5 MON mainnet / 0.65 MON testnet) for an inline payload, or `linkedListFee` (19.5 / 1.95 MON) when the data is chunked. *** ### Connection State An on-chain relationship between two addresses (friends, DM channels, etc.). #### What states can it have? * **pending** (`0`): request sent but not accepted yet * **approved** (`1`): request accepted, 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)`](#derivedmseed), so either party can recompute it. #### Key related functions * [`requestConnection()`](#requestconnection): send a friend request * [`manageConnection()`](#manageconnection): approve/block/unblock * [`readConnection()`](#readconnection): check relationship status * [`writeConnectionRow()`](#writeconnectionrow): exchange messages/data with a connected party * [`fetchUserConnections()`](#fetchuserconnections): fetch all of a user's connections *** ### Database Tables Store JSON data in tables like a database, all on-chain. #### How are tables created? 1. Call [`initializeDbRoot()`](#initializedbroot) once per `dbRootId`. The caller becomes the **DbRoot creator**. 2. Call [`createTable()`](#createtable) to create a table. `tableCreationFee` (19.5 MON mainnet / 1.95 MON testnet) is charged here and split 31/69 between `feeReceiver` and the DbRoot's creator. 3. Call [`writeRow()`](#writerow) to append rows. A table is uniquely identified by `dbRootId` + `tableName`. Both are hashed with `keccak256` internally, but raw names are also stored on-chain so the SDK can list them without a hardcoded lookup. Tables must exist before `writeRow()` is called. There is no implicit creation. #### Key related functions * [`initializeDbRoot()`](#initializedbroot): claim a `dbRootId` * [`createTable()`](#createtable) / [`updateTable()`](#updatetable): create or modify a table * [`writeRow()`](#writerow): append a row * [`readTableRows()`](#readtablerows): read rows from a table * [`getTablelistFromRoot()`](#gettablelistfromroot): list all tables in a database *** ### Token & Collection Gating Tables can be gated so only users holding a specific ERC-20 token or ERC-721 NFT can write data. #### Gate Types | Type | `gateType` | Description | | ------------------------ | ---------- | ---------------------------------------------------- | | **Token** (ERC-20) | `0` | User must hold >= `amount` of the specified token | | **Collection** (ERC-721) | `1` | User must hold any NFT from the specified collection | ERC-20 `amount` is in **raw token units (wei-style)**. For an 18-decimal token, "100 tokens" = `parseEther("100")`, not `100`. **ERC-1155 is not supported.** #### Gate parameter ```typescript theme={null} gate?: { tokenAddress: string; // ERC-20 or ERC-721 contract (ZeroAddress = public) amount: number | bigint; // min balance (raw units, ERC-20 only; ignored for ERC-721) gateType: 0 | 1; // 0 = ERC-20, 1 = ERC-721 } ``` *** ### Encryption (Crypto) Built-in encryption module (`iqlabs.crypto`) for encrypting data before storing on-chain. Primitives are identical to the Solana SDK, so the same plaintext flows across chains. #### Three encryption modes * **DH Encryption** (single recipient): Ephemeral X25519 ECDH → HKDF-SHA256 → AES-256-GCM * **Password Encryption**: PBKDF2-SHA256 (250k iterations) → AES-256-GCM * **Multi-recipient Encryption**: PGP-style hybrid, one CEK wrapped per recipient via ECDH Users derive a deterministic X25519 keypair from their wallet signature. The wallet *is* the key, no separate keystore. *** ### Fees Fees are owner-mutable per network and read live from the contract (`utils.getBasicFee` / `getLinkedListFee` / `getTableCreationFee`). Defaults shipped at deploy time: | Fee | Mainnet default | Testnet default | Charged on | | ------------------ | --------------- | --------------- | ---------------------------------------------------------------------------------------------------------- | | `basicFee` | 6.5 MON | 0.65 MON | `dbCodeIn` / `walletConnectionCodeIn` / `userInventoryCodeIn` **when the payload is inline** (≤ 700 bytes) | | `linkedListFee` | 19.5 MON | 1.95 MON | the same three functions, **when the payload is chunked** via `sendCode` | | `tableCreationFee` | 19.5 MON | 1.95 MON | `createTable` / `createPrivateTable` | | `discountFee` | 3.25 MON | 0.325 MON | replaces `basicFee` on the inline path for IQ-token holders | Testnet fees are exactly 10× cheaper than mainnet because the official Monad faucet drips only 0.05 MON / 12h — full mainnet pricing would make even one round trip unreachable for devs. **Where the value goes:** * `dbCodeIn` / `walletConnectionCodeIn` / `userInventoryCodeIn`: 100% to `feeReceiver`. * `createTable`: split **31% to `feeReceiver`** / **69% to the DbRoot's `creator`**. Root creators can pin or zero out their own value via [`setRootTableCreationFee()`](#setroottablecreationfee). **Free:** `updateTableTxChainTail`, `updateConnectionTxChainTail`, `updateUserTxChainTail`, `requestConnection`, `manageConnection`, `dbInstructionCodeIn`. Pointer bumps and instruction edits don't cost fee, only gas. *** ## Function Details ### Data Storage and Retrieval #### `codeIn()` | **Parameters** | `signer`: `ethers.Signer`
`data`: data to upload (string or string\[])
`filename`: optional filename (string, default: `""`)
`filetype`: file type hint (string, default: `""`)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); // Store a short message const txHash = await iqlabs.writer.codeIn(signer, 'Hello Monad!'); // Store large data with progress tracking const txHash2 = await iqlabs.writer.codeIn( signer, longString, 'data.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 }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const result = await iqlabs.reader.readCodeIn(txHash); console.log(result.data); // 'Hello Monad!' console.log(result.metadata.typeField); // 'text/plain' ``` *** ### Connection Management #### `requestConnection()` | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: 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) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.requestConnection( signer, 'my-app', friendAddress, 'dm_table', ['message', 'timestamp'], 'message_id' ); ``` `requestConnection` is **free** in this version (gas only). *** #### `manageConnection()` Approve, block, or unblock a connection. * `0`: pending * `1`: approved * `2`: blocked | **Parameters** | `signer`: `ethers.Signer`
`otherParty`: counterparty address (string)
`dbRootId`: database ID (string)
`newStatus`: `0` \| `1` \| `2` | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.manageConnection(signer, friendAddress, 'my-app', 1); // approve await iqlabs.writer.manageConnection(signer, friendAddress, 'my-app', 2); // block ``` *** #### `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' }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const { status } = await iqlabs.reader.readConnection('my-app', addressA, addressB); console.log(status); // 'pending' | 'approved' | 'blocked' | 'unknown' ``` *** #### `writeConnectionRow()` | **Parameters** | `signer`: `ethers.Signer`
`otherParty`: counterparty address (string)
`dbRootId`: database ID (string)
`rowJson`: JSON data (string)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.writeConnectionRow( signer, friendAddress, 'my-app', JSON.stringify({ message_id: '1', message: 'gm from Monad', timestamp: Date.now() }) ); ``` *** #### `readConnectionRows()` | **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) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const messages = await iqlabs.reader.readConnectionRows( 'my-app', myAddress, friendAddress, { limit: 50 } ); messages.forEach(m => console.log(m.data)); ``` *** #### `fetchUserConnections()` | **Parameters** | `userAddress`: user address (string) | | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ connectionKey: string, partyA: string, partyB: string, status: 'pending' \| 'approved' \| 'blocked' \| 'unknown' }>` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const connections = await iqlabs.reader.fetchUserConnections(myAddress); const friends = connections.filter(c => c.status === 'approved'); ``` *** ### Table Management #### `initializeDbRoot()` Claim a `dbRootId`. Only the creator can later modify table-creator allowlists or schema. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string) | | -------------- | --------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.initializeDbRoot(signer, 'my-app'); // reverts if dbRootId already claimed ``` *** #### `manageTableCreators()` Set who can create tables. **Caller must be the DbRoot creator.** | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableCreators`: addresses allowed to create public tables (`string[]`)
`extCreators`: addresses allowed to create private tables (`string[]`) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.manageTableCreators( signer, 'my-app', [admin1, admin2], // public-table allowlist [] // anyone can create private tables ); // pass empty arrays to make creation open to anyone ``` *** #### `createTable()` Create a new table. Charges `tableCreationFee` (19.5 MON mainnet / 1.95 MON testnet by default — or whatever value the DbRoot creator pinned with `setRootTableCreationFee`). Split 31% to `feeReceiver`, 69% to `DbRoot.creator`. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableName`: table name (string)
`columns`: column names (string\[])
`idCol`: ID column (string)
`extKeys`: extension keys (string\[], default: `[]`)
`gate`: optional access gate
`writers`: optional writer whitelist (`string[]`, default: `[]`)
`isPrivate`: create private table (boolean, default: `false`) | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { parseEther } from 'ethers'; iqlabs.setNetwork('monad'); // Public table await iqlabs.writer.createTable( signer, 'my-app', 'posts', ['title', 'body', 'author'], 'post_id' ); // ERC-20 gated table (must hold >= 100 tokens) await iqlabs.writer.createTable( signer, 'my-app', 'vip', ['name'], 'user_id', [], { tokenAddress: erc20Address, amount: parseEther('100'), gateType: 0 } ); // ERC-721 gated table (must hold 1 NFT) await iqlabs.writer.createTable( signer, 'my-app', 'holders', ['name'], 'user_id', [], { tokenAddress: nftAddress, amount: 0, gateType: 1 } ); // Private table (must know the name to access) await iqlabs.writer.createTable( signer, 'my-app', 'internal', ['note'], 'note_id', [], undefined, [], true ); ``` *** #### `updateTable()` Modify an existing table's schema, gate, or writer list. **Caller must be the DbRoot creator.** Existing rows are preserved. | **Parameters** | Same as [`createTable()`](#createtable) minus `isPrivate` | | -------------- | --------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { parseEther } from 'ethers'; iqlabs.setNetwork('monad'); await iqlabs.writer.updateTable( signer, 'my-app', 'vip', ['name'], 'user_id', [], { tokenAddress: erc20Address, amount: parseEther('500'), gateType: 0 } ); ``` *** #### `writeRow()` Append a row to an existing table. Charges `basicFee` (6.5 / 0.65 MON) for inline payloads or `linkedListFee` (19.5 / 1.95 MON) when chunked. | **Parameters** | `signer`: `ethers.Signer`
`dbRootId`: database ID (string)
`tableName`: table name (string)
`rowJson`: JSON row data (string)
`onProgress`: optional progress callback `(percent: number) => void` | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.writeRow(signer, 'my-app', 'posts', JSON.stringify({ post_id: '1', title: 'gm Monad', body: 'first post on-chain', author: await signer.getAddress() })); ``` Table must already exist. `writeRow` will revert if the table was never created. Fires two transactions internally: `dbCodeIn` (data — **fee charged here**, basic or linkedList by payload size) + `updateTableTxChainTail` (free pointer bump). *** #### `readTableRows()` Walk the table's tx-chain and reconstruct rows. | **Parameters** | `dbRootId`: database ID (string)
`tableName`: table name (string)
`options`: `{ limit?: number }` (optional) | | -------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ txHash: string, data: any }>` (most recent first) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const rows = await iqlabs.reader.readTableRows('my-app', 'posts', { limit: 50 }); rows.forEach(r => console.log(r.data)); ``` *** #### `getTablelistFromRoot()` | **Parameters** | `dbRootId`: database ID (string) | | -------------- | ----------------------------------------------------------------------- | | **Returns** | `{ creator: string, tables: TableEntry[], globalTables: TableEntry[] }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const { creator, tables, globalTables } = await iqlabs.reader.getTablelistFromRoot('my-app'); tables.forEach(t => console.log(`${t.name} (${t.seedHex})`)); ``` `tables` = public only. `globalTables` = public + private. *** #### `fetchInventoryTransactions()` Walk a user's inventory tx-chain (everything uploaded via [`codeIn()`](#codein)). | **Parameters** | `userAddress`: user address (string)
`options`: `{ limit?: number }` (optional) | | -------------- | ---------------------------------------------------------------------------------------------- | | **Returns** | `Array<{ txHash: string, handle: string, tailTx: string, typeField: string, offset: string }>` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); const myFiles = await iqlabs.reader.fetchInventoryTransactions(myAddress, { limit: 20 }); myFiles.forEach(tx => console.log(`${tx.txHash}: ${tx.handle}`)); ``` *** ### Encryption #### `deriveX25519Keypair()` Derive a deterministic X25519 keypair from a wallet signature. Same wallet = same keypair every time. | **Parameters** | `signMessage`: `(msg: Uint8Array) => Promise` | | -------------- | --------------------------------------------------------- | | **Returns** | `{ privKey: Uint8Array, pubKey: Uint8Array }` | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { getBytes } from 'ethers'; const sign = async (msg: Uint8Array) => getBytes(await signer.signMessage(msg)); const { privKey, pubKey } = await iqlabs.crypto.deriveX25519Keypair(sign); ``` *** #### `dhEncrypt()` / `dhDecrypt()` Single-recipient encryption via X25519 ECDH. | `dhEncrypt` | `recipientPubHex`: hex string
`plaintext`: Uint8Array → `{ senderPub, iv, ciphertext }` (all hex) | | ----------- | ------------------------------------------------------------------------------------------------------ | | `dhDecrypt` | `privKey`: Uint8Array
`senderPubHex`, `ivHex`, `ciphertextHex`: hex strings → `Uint8Array` | ```typescript theme={null} const enc = await iqlabs.crypto.dhEncrypt( recipientPubHex, new TextEncoder().encode('secret message') ); const dec = await iqlabs.crypto.dhDecrypt(myPrivKey, enc.senderPub, enc.iv, enc.ciphertext); console.log(new TextDecoder().decode(dec)); ``` *** #### `passwordEncrypt()` / `passwordDecrypt()` Password-based encryption via PBKDF2-SHA256. ```typescript theme={null} const enc = await iqlabs.crypto.passwordEncrypt( 'my-password', new TextEncoder().encode('secret data') ); const dec = await iqlabs.crypto.passwordDecrypt('my-password', enc.salt, enc.iv, enc.ciphertext); ``` *** #### `multiEncrypt()` / `multiDecrypt()` Multi-recipient PGP-style hybrid encryption. ```typescript theme={null} 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); ``` *** ### User Metadata #### `updateUserMetadata()` Store arbitrary metadata under the caller's address. Overwrites any previous value. | **Parameters** | `signer`: `ethers.Signer`
`metadata`: `string \| Uint8Array` | | -------------- | ----------------------------------------------------------------- | | **Returns** | Transaction hash (string) | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); await iqlabs.writer.updateUserMetadata( signer, JSON.stringify({ name: 'Alice', bio: 'building on Monad' }) ); ``` *** ### Environment Settings #### `setNetwork()` Switch the active network mode. Call once at app startup. | **Parameters** | `mode`: `'sepolia' \| 'monad' \| 'monadTestnet'`
`rpcUrl`: optional override (string) | | -------------- | ------------------------------------------------------------------------------------------ | | **Returns** | void | ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; iqlabs.setNetwork('monad'); // with custom RPC iqlabs.setNetwork('monad', 'https://your-monad-rpc'); ``` #### `getNetwork()` \| **Returns** | `'sepolia' \| 'monad' \| 'monadTestnet'` | ```typescript theme={null} console.log(iqlabs.getNetwork()); // 'monad' ``` #### `assertChainMatches()` Throws if RPC chainId doesn't match active network mode. ```typescript theme={null} iqlabs.setNetwork('monad'); await iqlabs.assertChainMatches(signer); // throws if signer's RPC isn't chainId 143 ``` #### `setRpcUrl()` / `getRpcUrl()` Override reader RPC without changing network mode. ```typescript theme={null} iqlabs.setRpcUrl('https://your-monad-rpc'); console.log(iqlabs.getRpcUrl()); ``` *** ## Tutorial: On-Chain Fortune Cookies 🥠 Build a permanent on-chain fortune cookie machine on Monad. Anyone can submit a fortune. Anyone can draw a random one. All fortunes live on-chain forever. **What this teaches:** `initializeDbRoot` → `createTable` → `writeRow` → `readTableRows` ### Setup ```bash theme={null} npm i @iqlabs-official/ethereum-sdk ethers ``` ### Full Code ```typescript theme={null} import iqlabs from '@iqlabs-official/ethereum-sdk'; import { BrowserProvider } from 'ethers'; const DB = 'fortune-cookies'; const TABLE = 'fortunes'; // Call once to initialize (only the first caller becomes creator) async function setupFortuneJar(signer: any) { iqlabs.setNetwork('monad'); try { await iqlabs.writer.initializeDbRoot(signer, DB); await iqlabs.writer.createTable( signer, DB, TABLE, ['fortune', 'author', 'ts'], 'id' ); console.log('Fortune jar created on Monad!'); } catch (e) { console.log('Jar already exists, skipping setup'); } } // Submit a fortune (costs 19.5 MON) async function submitFortune(signer: any, fortune: string) { iqlabs.setNetwork('monad'); const author = await signer.getAddress(); const txHash = await iqlabs.writer.writeRow( signer, DB, TABLE, JSON.stringify({ id: `${author}-${Date.now()}`, fortune, author, ts: Date.now() }) ); console.log('Fortune inscribed:', txHash); return txHash; } // Draw a random fortune (free read) async function drawFortune() { iqlabs.setNetwork('monad'); const rows = await iqlabs.reader.readTableRows(DB, TABLE); if (rows.length === 0) return 'The jar is empty. Be the first to add a fortune!'; const pick = rows[Math.floor(Math.random() * rows.length)]; return pick.data.fortune; } // Read all fortunes async function allFortunes() { iqlabs.setNetwork('monad'); return iqlabs.reader.readTableRows(DB, TABLE); } // Usage in a browser app async function main() { const provider = new BrowserProvider(window.ethereum); await provider.send('eth_requestAccounts', []); const signer = await provider.getSigner(); // First time only await setupFortuneJar(signer); // Submit await submitFortune(signer, 'The best time to build on Monad was yesterday. The second best time is now.'); // Draw const fortune = await drawFortune(); console.log('Your fortune:', fortune); } ``` ### How to extend * **NFT gate**: only holders of a specific collection can submit fortunes * **Encrypted fortunes**: use `passwordEncrypt` so only readers with the password see the message * **User profiles**: call `updateUserMetadata` so each author has a name + avatar * **Like counter**: use `manageRowData` to annotate fortunes with reactions # Developer Docs (Python) Source: https://iqlabs.mintlify.app/docs-python Core concepts and functions for the IQLabs Python SDK This document is in progress and will be refined. ## Installation ```bash theme={null} pip install iqlabs-solana-sdk ``` *** ## Core Concepts These are the key concepts to know before using the IQLabs SDK. *** ### Data Storage (Code In) This is how you store any data (files, text, JSON) on-chain. #### How is it stored? Depending on data size, the SDK picks the optimal method: * **Small data (\< 700 bytes)**: store immediately, fastest * **Medium data (\< 8.5 KB)**: split into multiple transactions * **Large data (>= 8.5 KB)**: upload in parallel for speed #### Key related functions * [`code_in()`](#code_in): upload data and get a transaction ID * [`read_code_in()`](#read_code_in): read data back from a transaction ID *** ### User State PDA An on-chain profile account for a user. #### What gets stored? * Profile info (name, profile picture, bio, etc.) * Number of uploaded files * Friend request records Friend requests are not stored as values in the PDA; they are sent as transactions. #### When is it created? It is created automatically the first time you call [`code_in()`](#code_in). No extra setup is required, but the first user may need to sign twice. *** ### Connection PDA An on-chain account that manages relationships between two users (friends, messages, etc.). #### What states can it have? * **pending**: a friend request was sent but not accepted yet * **approved**: the request was accepted and the users are connected * **blocked**: one side blocked the other A blocked connection can only be unblocked by the blocker. #### Key related functions * [`request_connection()`](#request_connection): send a friend request (creates pending) * [`manage_connection()`](#manage_connection): approve/reject/block/unblock a request * [`read_connection()`](#read_connection): check current relationship status * [`write_connection_row()`](#write_connection_row): exchange messages/data with a connected friend * [`fetch_user_connections()`](#fetch_user_connections): fetch all connections (sent & received friend requests) *** ### Database Tables Store JSON data in tables like a database. #### How are tables created? Use [`create_table()`](#create_table) to create a table explicitly. If the DbRoot PDA is running low on space, the SDK automatically expands it in the same transaction — no extra steps needed. A table is uniquely identified by the combination of `db_root_id` and `table_seed`. The `table_seed` is internally hashed (keccak256) to derive the on-chain PDA. A human-readable `table_hint` (e.g. `"users"`, `"chatroom:general"`) is stored in `DbRoot.table_seeds` so that anyone reading the DbRoot can discover tables without a hardcoded lookup. #### Key related functions * [`create_table()`](#create_table): create a new table (auto-reallocs DbRoot if needed) * [`write_row()`](#write_row): add a new row to an existing table * [`read_table_rows()`](#read_table_rows): read rows from a table * [`get_tablelist_from_root()`](#get_tablelist_from_root): list all tables in a database * [`fetch_inventory_transactions()`](#fetch_inventory_transactions): list uploaded files *** ### Token & Collection Gating Tables can be gated so that only users holding a specific token or NFT collection can write data. #### Gate Types | Type | `GateType` | Description | | -------------- | --------------------- | ---------------------------------------------------------------------- | | **Token** | `GateType.TOKEN` | User must hold >= `amount` of the specified SPL token mint | | **Collection** | `GateType.COLLECTION` | User must hold any NFT from the specified Metaplex verified collection | #### How it works * **Table creator** sets the gate when creating or updating a table * **Writers** don't need to do anything special — the SDK automatically resolves the required token account (and metadata account for collections) when calling `write_row()` or `manage_row_data()` * If no gate is set, the table is public (default behavior, no change for existing users) #### Gate parameter ```python theme={null} gate = { "mint": Pubkey, # token mint address OR collection address "amount": int, # minimum token amount (default: 1, ignored for collections) "gate_type": int, # GateType.TOKEN (default) or GateType.COLLECTION } ``` For **collection gates**, the user can present any NFT from that collection. `amount` is ignored since NFTs always have amount=1. *** ### Table Creation Permissions The database owner (DbRoot creator) can control who is allowed to create tables. #### Two levels of table creation | Type | Stored in | Visibility | Permission field | | ----------------- | ------------------------------------ | ------------------------------------- | ---------------- | | **Public table** | `table_seeds` + `global_table_seeds` | Listed in `get_tablelist_from_root()` | `table_creators` | | **Private table** | `global_table_seeds` only | Only accessible if you know the PDA | `ext_creators` | `table_seeds` and `global_table_seeds` store human-readable hints (e.g. `"users"`), not hashed seeds. To derive the table PDA from a hint, hash it with `to_seed_bytes()` first. * If the permission list is **empty**, anyone can create tables (default, backward-compatible) * If the permission list has wallets, only those wallets + the DbRoot creator can create tables #### Managing permissions The DbRoot creator can set both lists in a single call: ```python theme={null} from iqlabs.contract import manage_table_creators_instruction ix = manage_table_creators_instruction( builder, {"signer": wallet.pubkey(), "db_root": db_root_pda, "system_program": SYSTEM_PROGRAM_ID}, { "db_root_id": db_root_id_bytes, "table_creators": [admin_wallet_1, admin_wallet_2], "ext_creators": [], }, ) ``` Pass empty lists to make creation public again. #### Onboarding (private to public) A private table (exists in `global_table_seeds` only) can be promoted to public (`table_seeds`) by anyone in the `table_creators` list: ```python theme={null} from iqlabs.contract import onboard_table_instruction ix = onboard_table_instruction( builder, {"signer": wallet.pubkey(), "db_root": db_root_pda}, {"db_root_id": db_root_id_bytes, "table_seed": b"my-board"}, # table_seed here matches the hint stored in global_table_seeds ) ``` This is useful when you want anyone to create content (e.g. threads) but only admins decide which ones are publicly listed (e.g. boards). *** ### Encryption (Crypto) The SDK includes a built-in encryption module (`iqlabs.crypto`) for encrypting data before storing it on-chain. #### 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 [`derive_x25519_keypair()`](#derive_x25519_keypair). This means users don't need to manage separate encryption keys — their wallet is the key. #### Key related functions * [`derive_x25519_keypair()`](#derive_x25519_keypair): derive encryption keypair from wallet * [`dh_encrypt()`](#dh_encrypt) / [`dh_decrypt()`](#dh_decrypt): single-recipient encryption * [`password_encrypt()`](#password_encrypt) / [`password_decrypt()`](#password_decrypt): password-based encryption * [`multi_encrypt()`](#multi_encrypt) / [`multi_decrypt()`](#multi_decrypt): multi-recipient encryption *** ## Function Details ### Data Storage and Retrieval #### `code_in()` | **Parameters** | `connection`: Solana RPC AsyncClient
`signer`: Keypair or WalletSigner
`chunks`: data to upload (list\[str])
`filename`: optional filename (str or None)
`method`: upload method (int, default: 0)
`filetype`: file type hint (str, default: '')
`on_progress`: optional progress callback (Callable\[\[int], None]) | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction signature (str) | ```python theme={null} from iqlabs import writer from solana.rpc.async_api import AsyncClient from solders.keypair import Keypair # Upload data signature = await writer.code_in(connection, signer, ['Hello, blockchain!']) # Upload with filename signature = await writer.code_in(connection, signer, ['file contents here'], filename='hello.txt') ``` *** #### `read_code_in()` | **Parameters** | `tx_signature`: transaction signature (str)
`speed`: rate limit profile (optional, str)
`on_progress`: optional progress callback (Callable\[\[int], None]) | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | dict with `metadata` (str) and `data` (str or None) | ```python theme={null} from iqlabs import reader result = await reader.read_code_in('5Xg7...') print(result['data']) # 'Hello, blockchain!' print(result['metadata']) # JSON string with file metadata ``` *** ### Connection Management #### `request_connection()` | **Parameters** | `connection`: AsyncClient
`signer`: Keypair or WalletSigner
`db_root_id`: database ID (bytes or str)
`party_a`: first user pubkey (str)
`party_b`: second user pubkey (str)
`table_name`: connection table name (str or bytes)
`columns`: column list (list\[str or bytes])
`id_col`: ID column (str or bytes)
`ext_keys`: extension keys (list\[str or bytes]) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction signature (str) | ```python theme={null} from iqlabs import writer await writer.request_connection( connection, signer, 'my-db', my_wallet_address, friend_wallet_address, 'dm_table', ['message', 'timestamp'], 'message_id', [] ) ``` *** #### `manage_connection()` There is no high-level SDK wrapper for this function. Use the contract-level instruction builder directly. | **Parameters** | `builder`: InstructionBuilder
`accounts`: dict with `db_root`, `connection_table`, `signer`
`args`: dict with `db_root_id`, `connection_seed`, `new_status` | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Instruction | ```python theme={null} from iqlabs import contract # Create an instruction builder builder = contract.create_instruction_builder() # Approve a friend request approve_ix = contract.manage_connection_instruction( builder, {"db_root": db_root, "connection_table": connection_table, "signer": my_pubkey}, {"db_root_id": db_root_id, "connection_seed": connection_seed, "new_status": contract.CONNECTION_STATUS_APPROVED} ) # Block a user block_ix = contract.manage_connection_instruction( builder, {"db_root": db_root, "connection_table": connection_table, "signer": my_pubkey}, {"db_root_id": db_root_id, "connection_seed": connection_seed, "new_status": contract.CONNECTION_STATUS_BLOCKED} ) ``` *** #### `read_connection()` | **Parameters** | `db_root_id`: database ID (bytes or str)
`party_a`: first wallet (str)
`party_b`: second wallet (str) | | -------------- | --------------------------------------------------------------------------------------------------------------- | | **Returns** | dict with `status`, `requester`, `blocker` | ```python theme={null} from iqlabs import reader conn_info = await reader.read_connection('my-db', party_a, party_b) print(conn_info['status']) # 'pending' | 'approved' | 'blocked' ``` *** #### `write_connection_row()` | **Parameters** | `connection`: AsyncClient
`signer`: Keypair or WalletSigner
`db_root_id`: database ID (bytes or str)
`connection_seed`: connection seed (bytes or str)
`row_json`: JSON data (str) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction signature (str) | ```python theme={null} from iqlabs import writer import json await writer.write_connection_row( connection, signer, 'my-db', connection_seed, json.dumps({"message_id": "123", "message": "Hello friend!", "timestamp": 1234567890}) ) ``` *** #### `fetch_user_connections()` Fetch all connections (friend requests) for a user by analyzing their UserState PDA transaction history. Each connection includes its `db_root_id`, identifying which app the connection belongs to. | **Parameters** | `user_pubkey`: user public key (str or Pubkey)
`limit`: max number of transactions to fetch (optional)
`before`: signature to paginate from (optional)
`speed`: rate limit profile (optional) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | List of connection dicts with db\_root\_id, connection\_pda, party\_a, party\_b, status, requester, blocker, timestamp | ```python theme={null} from iqlabs import reader # Fetch all connections (across all apps!) connections = await reader.fetch_user_connections( my_pubkey, speed="light", limit=100 ) # Filter by status pending_requests = [c for c in connections if c['status'] == 'pending'] friends = [c for c in connections if c['status'] == 'approved'] blocked = [c for c in connections if c['status'] == 'blocked'] # Check connection details for conn in connections: print(f"Party A: {conn['party_a']} <-> Party B: {conn['party_b']}, status: {conn['status']}") ``` *** ### Table Management #### `create_table()` | **Parameters** | `connection`: AsyncClient
`signer`: Keypair or WalletSigner
`db_root_id`: database ID (bytes or str)
`table_seed`: table identifier — hashed internally via `to_seed_bytes()` for PDA derivation
`table_name`: display name (bytes or str)
`column_names`: column list (list\[bytes or str])
`id_col`: ID column (bytes or str)
`ext_keys`: extension keys (list\[bytes or str])
`gate`: optional access gate (see [Token & Collection Gating](#token--collection-gating))
`writers`: optional allowed writers (list\[Pubkey])
`table_hint`: human-readable identifier stored in DbRoot for discovery (str) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction signature (str) | The `table_hint` is stored in `DbRoot.table_seeds` so anyone reading the DbRoot can discover tables. The `table_seed` is hashed via `to_seed_bytes()` to derive the on-chain PDA — it is **not** stored. If the DbRoot PDA is running low on space, a realloc instruction is automatically prepended. ```python theme={null} from iqlabs import writer from iqlabs.contract import GateType # Basic table — table_hint "users" is stored in DbRoot for discovery await writer.create_table( connection, signer, 'my-db', 'users', 'users', ['name', 'email', 'age'], 'name', [], table_hint='users' ) # With token gate await writer.create_table( connection, signer, 'my-db', 'vip', 'vip', ['name'], 'user_id', [], gate={"mint": token_mint_pubkey, "amount": 100, "gate_type": GateType.TOKEN}, table_hint='vip' ) # With NFT collection gate await writer.create_table( connection, signer, 'my-db', 'holders', 'holders', ['name'], 'user_id', [], gate={"mint": collection_pubkey, "gate_type": GateType.COLLECTION}, table_hint='holders' ) ``` *** #### `write_row()` | **Parameters** | `connection`: AsyncClient
`signer`: Keypair or WalletSigner
`db_root_id`: database ID (bytes or str)
`table_seed`: table name (bytes or str)
`row_json`: JSON row data (str)
`skip_confirmation`: skip tx confirmation (default: False)
`remaining_accounts`: optional list of Pubkey to append as read-only reference accounts (e.g. feed PDA for indexing) | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Returns** | Transaction signature (str) | ```python theme={null} from iqlabs import writer import json # Write the first row to create the table await writer.write_row(connection, signer, 'my-db', 'users', json.dumps({ "id": 1, "name": "Alice", "email": "alice@example.com" })) # Add another row to the same table await writer.write_row(connection, signer, 'my-db', 'users', json.dumps({ "id": 2, "name": "Bob", "email": "bob@example.com" })) ``` *** #### `read_table_rows()` | **Parameters** | `account`: table PDA (Pubkey or str)
`before`: signature cursor for pagination (optional)
`limit`: max number of rows to fetch (optional)
`speed`: rate limit profile (optional) | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | `list[dict]` | ```python theme={null} from iqlabs import reader # Basic usage rows = await reader.read_table_rows(table_pda, limit=50) # Cursor-based pagination older_rows = await reader.read_table_rows(table_pda, limit=50, before="sig...") ``` *** #### `get_tablelist_from_root()` | **Parameters** | `connection`: AsyncClient
`db_root_id`: database ID (bytes or str) | | -------------- | ----------------------------------------------------------------------- | | **Returns** | dict with `root_pda`, `creator`, `table_seeds`, `global_table_seeds` | ```python theme={null} from iqlabs import reader result = await reader.get_tablelist_from_root(connection, 'my-db') print('Creator:', result['creator']) print('Table seeds:', result['table_seeds']) ``` *** #### `fetch_inventory_transactions()` | **Parameters** | `public_key`: user public key (Pubkey)
`limit`: max count (int)
`before`: pagination cursor (optional, str) | | -------------- | --------------------------------------------------------------------------------------------------------------------- | | **Returns** | Transaction list | ```python theme={null} from iqlabs import reader from solders.pubkey import Pubkey import json my_files = await reader.fetch_inventory_transactions(my_pubkey, 20) for tx in my_files: metadata = None try: metadata = json.loads(tx['metadata']) except: metadata = None if metadata and 'data' in metadata: inline_data = metadata['data'] if isinstance(metadata['data'], str) else json.dumps(metadata['data']) print(f"Inline data: {inline_data}") else: print(f"Signature: {tx['signature']}") ``` *** ### Encryption #### `derive_x25519_keypair()` Derive a deterministic X25519 keypair from a wallet signature. The same wallet always produces the same keypair. | **Parameters** | `sign_message`: async sign function `Callable[[bytes], Awaitable[bytes]]` | | -------------- | ------------------------------------------------------------------------- | | **Returns** | dict with `priv_key` (bytes) and `pub_key` (bytes) | ```python theme={null} from iqlabs import crypto keypair = await crypto.derive_x25519_keypair(wallet.sign_message) pub_hex = keypair['pub_key'].hex() ``` *** #### `dh_encrypt()` | **Parameters** | `recipient_pub_hex`: recipient's X25519 public key (hex str)
`plaintext`: data to encrypt (bytes) | | -------------- | ------------------------------------------------------------------------------------------------------ | | **Returns** | dict with `sender_pub`, `iv`, `ciphertext` (all hex str) | #### `dh_decrypt()` | **Parameters** | `priv_key`: recipient's private key (bytes)
`sender_pub_hex`: sender's ephemeral public key (hex str)
`iv_hex`: IV (hex str)
`ciphertext_hex`: ciphertext (hex str) | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | bytes (decrypted plaintext) | ```python theme={null} from iqlabs import crypto # Encrypt for a single recipient encrypted = crypto.dh_encrypt(recipient_pub_hex, b'secret message') # Decrypt (recipient side) decrypted = crypto.dh_decrypt( recipient_priv_key, encrypted['sender_pub'], encrypted['iv'], encrypted['ciphertext'] ) print(decrypted.decode()) # 'secret message' ``` *** #### `password_encrypt()` | **Parameters** | `password`: password (str)
`plaintext`: data to encrypt (bytes) | | -------------- | -------------------------------------------------------------------- | | **Returns** | dict with `salt`, `iv`, `ciphertext` (all hex str) | #### `password_decrypt()` | **Parameters** | `password`: password (str)
`salt_hex`: salt (hex str)
`iv_hex`: IV (hex str)
`ciphertext_hex`: ciphertext (hex str) | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | bytes (decrypted plaintext) | ```python theme={null} from iqlabs import crypto # Encrypt with password encrypted = crypto.password_encrypt('my-password', b'secret data') # Decrypt with same password decrypted = crypto.password_decrypt( 'my-password', encrypted['salt'], encrypted['iv'], encrypted['ciphertext'] ) ``` *** #### `multi_encrypt()` | **Parameters** | `recipient_pub_hexes`: recipient public keys (list\[str])
`plaintext`: data to encrypt (bytes) | | -------------- | --------------------------------------------------------------------------------------------------- | | **Returns** | dict with `recipients` (list\[RecipientEntry]), `iv`, `ciphertext` | #### `multi_decrypt()` | **Parameters** | `priv_key`: your private key (bytes)
`pub_key_hex`: your public key (hex str)
`encrypted`: the MultiEncryptResult dict | | -------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Returns** | bytes (decrypted plaintext) | ```python theme={null} from iqlabs import crypto # Encrypt for multiple recipients encrypted = crypto.multi_encrypt( [alice_pub_hex, bob_pub_hex, carol_pub_hex], b'group secret' ) # Each recipient decrypts with their own key plaintext = crypto.multi_decrypt(alice_priv_key, alice_pub_hex, encrypted) ``` *** ### Environment Settings #### `set_rpc_url()` | **Parameters** | `url`: Solana RPC URL (str) | | -------------- | --------------------------- | | **Returns** | None | ```python theme={null} from iqlabs import set_rpc_url set_rpc_url('https://your-rpc.example.com') ``` *** ## Advanced Functions These are low-level SDK functions. Not needed for typical usage, but useful when building custom features or debugging. ### Writer Functions #### `manage_row_data()` Unified function that handles both table row writes and connection row writes. Auto-detects whether to write to a table or connection based on existing PDAs. | **Module** | `writer` | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Parameters** | `connection`: AsyncClient
`signer`: Keypair or WalletSigner
`db_root_id`: database ID (bytes or str)
`seed`: table or connection seed (bytes or str)
`row_json`: JSON row data (str)
`table_name`: required for table edits (optional)
`target_tx`: reference tx for table edits (optional) | | **Returns** | Transaction signature (str) | | **Use Case** | Custom row management, updating existing rows | ```python theme={null} from iqlabs import writer import json await writer.manage_row_data( connection, signer, 'my-db', # db_root_id 'users', # seed (table seed) json.dumps({"id": 1, "name": "Updated Name"}), table_name='users', target_tx=original_tx_sig ) ``` *** ### Reader Functions #### `read_user_state()` Reads the UserState PDA for a given user. | **Module** | `reader` | | -------------- | -------------------------------------------------------------------- | | **Parameters** | `user_pubkey`: user public key (str) | | **Returns** | dict with `owner`, `metadata`, `total_session_files`, `profile_data` | | **Use Case** | Fetching user profile data, checking upload counts | ```python theme={null} from iqlabs import reader user_state = await reader.read_user_state(user_pubkey) print('Owner:', user_state['owner']) print('Session files:', user_state['total_session_files']) print('Profile data:', user_state['profile_data']) ``` *** #### `read_inventory_metadata()` Reads metadata associated with a user's inventory transaction. | **Module** | `reader` | | -------------- | ---------------------------------------------------- | | **Parameters** | `tx_signature`: transaction signature (str) | | **Returns** | dict with metadata | | **Use Case** | Extracting file metadata from inventory transactions | ```python theme={null} from iqlabs import reader result = await reader.read_inventory_metadata(tx_signature) print('Metadata:', result) ``` *** #### `get_session_pda_list()` Retrieves a list of session PDA addresses for a user. | **Module** | `reader` | | -------------- | ------------------------------------------- | | **Parameters** | `user_pubkey`: user public key (str) | | **Returns** | `list[str]` (session PDA base58 strings) | | **Use Case** | Session management, active session tracking | ```python theme={null} from iqlabs import reader sessions = await reader.get_session_pda_list(user_pubkey) for pda in sessions: print(f"Session PDA: {pda}") ``` *** #### Contract PDA Functions Low-level PDA derivation functions available in the `contract` module. | **Module** | `contract` | | ------------ | -------------------------------------- | | **Use Case** | Custom PDA derivation, account lookups | ```python theme={null} from iqlabs import contract from solders.pubkey import Pubkey program_id = contract.get_program_id() db_root_pda = contract.get_db_root_pda(db_root_id, program_id) table_pda = contract.get_table_pda(db_root_pda, table_seed, program_id) user_pda = contract.get_user_pda(user_pubkey, program_id) session_pda = contract.get_session_pda(user_pubkey, seq=0, program_id=program_id) connection_pda = contract.get_connection_table_pda(db_root_pda, connection_seed, program_id) ``` *** ### Utility Functions #### Session speed profiles Many writer/reader functions accept a `speed` parameter that controls RPS and concurrency for the call. The SDK ships four presets and lets you override raw values directly: ```python theme={null} import iqlabs # Preset name await iqlabs.writer.write_row(connection, signer, db_root_id, table_seed, row_json, speed="heavy") # Raw override — any subset of keys; missing dials inherit the default preset. await iqlabs.writer.write_row( connection, signer, db_root_id, table_seed, row_json, speed={"max_rps": 80, "max_concurrency_upload": 30}, ) ``` | Preset | `max_rps` | `max_concurrency` | `max_concurrency_upload` | | ----------------- | --------- | ----------------- | ------------------------ | | `light` (default) | 2 | 5 | 1 | | `medium` | 50 | 50 | 5 | | `heavy` | 100 | 100 | 50 | | `extreme` | 250 | 250 | 100 | Profiles are mutable — `iqlabs.utils.SESSION_SPEED_PROFILES["heavy"]["max_rps"] = 200` affects every future `speed="heavy"` call. Exports: `SESSION_SPEED_PROFILES`, `DEFAULT_SESSION_SPEED`, `resolve_session_speed`, `resolve_session_config`, type `SessionSpeedOption`. *** #### `derive_dm_seed()` Derives a deterministic seed for direct messaging (DM) between two users. Sorts the two pubkeys alphabetically and hashes `"lower:upper"` with Keccak-256. | **Module** | `utils` | | -------------- | ------------------------------------------------------------------------- | | **Parameters** | `user_a`: first user pubkey (str)
`user_b`: second user pubkey (str) | | **Returns** | bytes (32-byte seed) | | **Use Case** | Creating consistent connection identifiers, DM channel setup | ```python theme={null} from iqlabs.sdk.utils.seed import derive_dm_seed seed1 = derive_dm_seed(wallet_a, wallet_b) seed2 = derive_dm_seed(wallet_b, wallet_a) print(seed1 == seed2) # True — order doesn't matter ``` *** #### `to_seed_bytes()` Converts a seed identifier to bytes. If the input is a 64-character hex string, it passes through as-is. Otherwise, it applies Keccak-256 hash. | **Module** | `utils` | | -------------- | -------------------------------------------------- | | **Parameters** | `value`: seed string or bytes | | **Returns** | bytes | | **Use Case** | Custom PDA derivation, low-level seed manipulation | ```python theme={null} from iqlabs.sdk.utils.seed import to_seed_bytes from solders.pubkey import Pubkey seed_string = 'my-custom-seed' seed_bytes = to_seed_bytes(seed_string) pda, bump = Pubkey.find_program_address( [seed_bytes, other_seed], program_id ) ``` *** # Robinhood Chain SDK Source: https://iqlabs.mintlify.app/docs-robinhood Complete guide to building on-chain apps on Robinhood Chain with the IQLabs Ethereum SDK This document is in progress and will be refined.