# 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.
**Robinhood Chain mainnet** is live and supported. Switch with [`setNetwork('robinhood')`](#setnetwork). Requires SDK **v0.3.0 or later**. The Robinhood Chain testnet contract is not deployed yet.
The IQLabs Ethereum SDK works on Robinhood Chain 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. Robinhood Chain is an Ethereum-compatible Layer 2 with ETH as the gas token, so fees stay a fraction of a cent while everything behaves exactly like Ethereum.
## 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 four 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` |
| `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('robinhood'); // call once at app startup
```
The contract is identical across all networks (same ABI, same functions). Only the address, chain, and fees differ. The SDK reads fees on-chain, so you never hardcode them. The Robinhood deployment is [verified on Blockscout](https://robinhoodchain.blockscout.com/address/0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223#code).
***
## Getting ETH on Robinhood Chain
Robinhood Chain uses **ETH** for gas and fees. Bridge ETH (or supported ERC-20s) in via the canonical Arbitrum bridge or partner bridging routes — see the [official Robinhood Chain docs](https://docs.robinhood.com/chain/) for current options.
Fees are tiny: a basic write costs `0.00012 ETH` (about \$0.22). Bridging a few dollars of ETH is enough for many writes.
Robinhood Chain is chain ID `4663`. It is a separate network from Ethereum mainnet and Arbitrum One — ETH must be bridged in before it can be spent 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.mainnet.chain.robinhood.com');
const signer = new Wallet(process.env.PRIVATE_KEY!, provider);
```
### Browser (MetaMask / injected wallet)
Add Robinhood Chain to MetaMask first:
| Field | Value |
| --------------- | ----------------------------------------- |
| Network Name | Robinhood Chain |
| RPC URL | `https://rpc.mainnet.chain.robinhood.com` |
| Chain ID | `4663` |
| Currency Symbol | `ETH` |
| Block Explorer | `https://robinhoodchain.blockscout.com` |
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).
Use [`assertChainMatches(signer)`](#assertchainmatches) after `setNetwork('robinhood')` to catch a signer that's still pointed at the wrong chain before you send a transaction.
***
## 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` (0.00012 ETH) for an inline payload, or `linkedListFee` (0.00036 ETH) 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` (0.00036 ETH) 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 | Default | \~USD | Charged on |
| ------------------ | ----------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `basicFee` | 0.00012 ETH | \~\$0.22 | `dbCodeIn` / `walletConnectionCodeIn` / `userInventoryCodeIn` **when the payload is inline** (≤ 700 bytes) |
| `linkedListFee` | 0.00036 ETH | \~\$0.65 | the same three functions, **when the payload is chunked** via `sendCode` |
| `tableCreationFee` | 0.00036 ETH | \~\$0.65 | `createTable` / `createPrivateTable` |
| `discountFee` | 0.00006 ETH | \~\$0.11 | replaces `basicFee` on the inline path for IQ-token holders |
Defaults target the same dollar values as Monad mainnet (priced at ETH \~\$1,795, July 2026) and are retunable on-chain as price moves.
**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('robinhood');
// Store a short message
const txHash = await iqlabs.writer.codeIn(signer, 'Hello Robinhood Chain!');
// 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('robinhood');
const result = await iqlabs.reader.readCodeIn(txHash);
console.log(result.data); // 'Hello Robinhood Chain!'
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('robinhood');
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('robinhood');
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('robinhood');
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('robinhood');
await iqlabs.writer.writeConnectionRow(
signer,
friendAddress,
'my-app',
JSON.stringify({ message_id: '1', message: 'gm from Robinhood Chain', 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('robinhood');
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('robinhood');
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('robinhood');
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('robinhood');
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` (0.00036 ETH 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('robinhood');
// 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('robinhood');
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` (0.00012 ETH) for inline payloads or `linkedListFee` (0.00036 ETH) 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('robinhood');
await iqlabs.writer.writeRow(signer, 'my-app', 'posts', JSON.stringify({
post_id: '1',
title: 'gm Robinhood Chain',
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('robinhood');
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('robinhood');
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('robinhood');
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('robinhood');
await iqlabs.writer.updateUserMetadata(
signer,
JSON.stringify({ name: 'Alice', bio: 'building on Robinhood Chain' })
);
```
***
### Environment Settings
#### `setNetwork()`
Switch the active network mode. Call once at app startup.
| **Parameters** | `mode`: `'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood'`
`rpcUrl`: optional override (string) |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| **Returns** | void |
```typescript theme={null}
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('robinhood');
// with custom RPC (e.g. QuickNode)
iqlabs.setNetwork('robinhood', 'https://your-robinhood-rpc');
```
#### `getNetwork()`
\| **Returns** | `'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood'` |
```typescript theme={null}
console.log(iqlabs.getNetwork()); // 'robinhood'
```
#### `assertChainMatches()`
Throws if RPC chainId doesn't match active network mode.
```typescript theme={null}
iqlabs.setNetwork('robinhood');
await iqlabs.assertChainMatches(signer); // throws if signer's RPC isn't chainId 4663
```
#### `setRpcUrl()` / `getRpcUrl()`
Override reader RPC without changing network mode.
```typescript theme={null}
iqlabs.setRpcUrl('https://your-robinhood-rpc');
console.log(iqlabs.getRpcUrl());
```
***
## Tutorial: On-Chain Fortune Cookies 🥠
Build a permanent on-chain fortune cookie machine on Robinhood Chain. 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('robinhood');
try {
await iqlabs.writer.initializeDbRoot(signer, DB);
await iqlabs.writer.createTable(
signer, DB, TABLE,
['fortune', 'author', 'ts'],
'id'
);
console.log('Fortune jar created on Robinhood Chain!');
} catch (e) {
console.log('Jar already exists, skipping setup');
}
}
// Submit a fortune (costs 0.00036 ETH)
async function submitFortune(signer: any, fortune: string) {
iqlabs.setNetwork('robinhood');
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('robinhood');
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('robinhood');
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 Robinhood Chain 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
Source: https://iqlabs.mintlify.app/docs-typescript
Core concepts and functions for the IQLabs SDK
This document is in progress and will be refined.
## Installation
```bash theme={null}
npm i @iqlabs-official/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 (\< 900 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
* [`codeIn()`](#codein): upload data and get a transaction ID
* [`readCodeIn()`](#readcodein): 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 [`codeIn()`](#codein). 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
* [`requestConnection()`](#requestconnection): send a friend request (creates pending)
* [`manageConnection()`](#manageconnection): approve/reject/block/unblock a request
* [`readConnection()`](#readconnection): check current relationship status
* [`writeConnectionRow()`](#writeconnectionrow): exchange messages/data with a connected friend
* [`fetchUserConnections()`](#fetchuserconnections): fetch all connections (sent & received friend requests)
***
### Database Tables
Store JSON data in tables like a database.
#### How are tables created?
Use [`createTable()`](#createtable) 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 `dbRootId` and `tableSeed`. The `tableSeed` is internally hashed (keccak256) to derive the on-chain PDA. A human-readable `tableHint` (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
* [`createTable()`](#createtable): create a new table (auto-reallocs DbRoot if needed)
* [`writeRow()`](#writerow): add a new row to an existing table
* [`readTableRows()`](#readtablerows): read rows from a table
* [`getTablelistFromRoot()`](#gettablelistfromroot): list all tables in a database
* [`fetchInventoryTransactions()`](#fetchinventorytransactions): 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 `writeRow()` or `manageRowData()`
* If no gate is set, the table is public (default behavior, no change for existing users)
#### Gate parameter
```typescript theme={null}
gate?: {
mint: PublicKey; // token mint address OR collection address
amount?: number; // minimum token amount (default: 1, ignored for collections)
gateType?: GateType; // 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 `getTablelistFromRoot()` | `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 `toSeedBytes()` 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:
```typescript theme={null}
const ix = iqlabs.contract.manageTableCreatorsInstruction(
builder,
{ signer: wallet.publicKey, db_root: dbRootPda, system_program: SystemProgram.programId },
{
db_root_id: dbRootIdBytes,
table_creators: [adminWallet1, adminWallet2], // who can create public tables
ext_creators: [], // empty = anyone can create private tables
}
);
```
Pass empty arrays 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:
```typescript theme={null}
const ix = iqlabs.contract.onboardTableInstruction(
builder,
{ signer: wallet.publicKey, db_root: dbRootPda },
{ db_root_id: dbRootIdBytes, table_seed: Buffer.from("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).
#### Reading table hints
`getTablelistFromRoot()` returns the hints stored in `table_seeds` and `global_table_seeds` as hex strings. Decode them to get the original readable identifiers:
```typescript theme={null}
const { tableSeeds } = await iqlabs.reader.getTablelistFromRoot(connection, dbRootId);
for (const seedHex of tableSeeds) {
const hint = Buffer.from(seedHex, "hex").toString("utf8");
console.log(hint); // "users", "po", "chatroom:general", etc.
// To access the table, hash the hint to derive the PDA
const tablePda = iqlabs.contract.getTablePda(
dbRoot, iqlabs.utils.toSeedBytes(hint), programId
);
}
```
***
### 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 [`deriveX25519Keypair()`](#derivex25519keypair). This means users don't need to manage separate encryption keys — their wallet is the key.
#### 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
***
## Function Details
### Data Storage and Retrieval
#### `codeIn()`
| **Parameters** | `input`: `{ connection, signer }` object
`data`: data to upload (string or string\[])
`filename`: optional filename (string)
`method`: upload method (number, default: 0)
`filetype`: file type hint (string, default: '')
`onProgress`: optional progress callback `(percent: number) => void` |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Returns** | Transaction signature (string) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Upload data
const signature = await iqlabs.writer.codeIn(
{ connection, signer },
'Hello, blockchain!'
);
// Upload with filename
const sig = await iqlabs.writer.codeIn(
{ connection, signer },
'file contents here',
'hello.txt'
);
```
***
#### `readCodeIn()`
| **Parameters** | `txSignature`: transaction signature (string)
`speed`: rate limit profile (optional, 'light' \| 'medium' \| 'heavy' \| 'extreme')
`onProgress`: optional progress callback `(percent: number) => void` |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | `{ metadata: string, data: string \| null }` |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const result = await iqlabs.reader.readCodeIn('5Xg7...');
console.log(result.data); // 'Hello, blockchain!'
console.log(result.metadata); // JSON string with file metadata
```
***
### Connection Management
#### `requestConnection()`
| **Parameters** | `connection`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`partyA`: first user pubkey (string)
`partyB`: second user pubkey (string)
`tableName`: connection table name (string or Uint8Array)
`columns`: column list (Array\)
`idCol`: ID column (string or Uint8Array)
`extKeys`: extension keys (Array\) |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction signature (string) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
await iqlabs.writer.requestConnection(
connection, signer, 'my-db',
myWalletAddress, friendWalletAddress,
'dm_table', ['message', 'timestamp'], 'message_id', []
);
```
***
#### `manageConnection()`
There is no high-level SDK wrapper for this function. Use the contract-level instruction builder directly.
| **Parameters** | `builder`: InstructionBuilder
`accounts`: `{ db_root, connection_table, signer }` (PublicKey)
`args`: `{ db_root_id, connection_seed, new_status }` |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | TransactionInstruction |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const builder = iqlabs.contract.createInstructionBuilder();
// Approve a friend request
const approveIx = iqlabs.contract.manageConnectionInstruction(
builder,
{ db_root, connection_table, signer: myPubkey },
{ db_root_id, connection_seed, new_status: iqlabs.contract.CONNECTION_STATUS_APPROVED }
);
// Block a user
const blockIx = iqlabs.contract.manageConnectionInstruction(
builder,
{ db_root, connection_table, signer: myPubkey },
{ db_root_id, connection_seed, new_status: iqlabs.contract.CONNECTION_STATUS_BLOCKED }
);
```
***
#### `readConnection()`
| **Parameters** | `dbRootId`: database ID (Uint8Array or 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/solana-sdk';
const { status, requester, blocker } = await iqlabs.reader.readConnection('my-db', partyA, partyB);
console.log(status); // 'pending' | 'approved' | 'blocked'
```
***
#### `writeConnectionRow()`
| **Parameters** | `connection`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`connectionSeed`: connection seed (Uint8Array or string)
`rowJson`: JSON data (string) |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction signature (string) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
await iqlabs.writer.writeConnectionRow(
connection, signer, 'my-db', connectionSeed,
JSON.stringify({ message_id: '123', message: 'Hello friend!', timestamp: Date.now() })
);
```
***
#### `fetchUserConnections()`
Fetch all connections (friend requests) for a user by analyzing their UserState PDA transaction history. Each connection includes its `dbRootId`, identifying which app the connection belongs to.
| **Parameters** | `userPubkey`: user public key (string or PublicKey)
`options`: optional settings |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Options** | `limit`: max number of transactions to fetch
`before`: signature to paginate from
`speed`: rate limit profile ('light' \| 'medium' \| 'heavy' \| 'extreme') |
| **Returns** | Array of `{ dbRootId, connectionPda, partyA, partyB, status, requester, blocker, timestamp }` |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const connections = await iqlabs.reader.fetchUserConnections(myPubkey, {
speed: 'light',
limit: 100
});
// Filter by status
const pendingRequests = connections.filter(c => c.status === 'pending');
const friends = connections.filter(c => c.status === 'approved');
const blocked = connections.filter(c => c.status === 'blocked');
// Check connection details
connections.forEach(conn => {
console.log(`${conn.partyA} <-> ${conn.partyB}, status: ${conn.status}`);
});
```
***
#### `updateUserMetadata()`
| **Parameters** | `connection`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`meta`: metadata to store (Uint8Array or string) |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction signature (string) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Store a codeIn tx signature as user metadata
const txSig = await iqlabs.writer.updateUserMetadata(
connection, signer, 'my-db', metaTxSignature
);
```
***
### Table Management
#### `createTable()`
| **Parameters** | `connection`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`tableSeed`: table identifier — hashed internally via `toSeedBytes()` for PDA derivation
`tableName`: display name (Uint8Array or string)
`columnNames`: column list (Array\)
`idCol`: ID column (Uint8Array or string)
`extKeys`: extension keys (Array\)
`gate`: optional access gate (see [Token & Collection Gating](#token--collection-gating))
`writers`: optional allowed writers (PublicKey\[])
`tableHint`: human-readable identifier stored in DbRoot for discovery (string) |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction signature (string) |
The `tableHint` is stored in `DbRoot.table_seeds` so anyone reading the DbRoot can discover tables. The `tableSeed` is hashed via `toSeedBytes()` 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.
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Basic table — tableHint "users" is stored in DbRoot for discovery
await iqlabs.writer.createTable(
connection, signer, 'my-db', 'users',
'users',
['name', 'email', 'age'],
'name',
[],
undefined, undefined,
'users' // tableHint — readable identifier stored in DbRoot
);
// With token gate
await iqlabs.writer.createTable(
connection, signer, 'my-db', 'vip', 'vip',
['name'], 'user_id', [],
{ mint: tokenMintPubkey, amount: 100, gateType: iqlabs.contract.GateType.Token },
undefined,
'vip'
);
// With NFT collection gate
await iqlabs.writer.createTable(
connection, signer, 'my-db', 'holders', 'holders',
['name'], 'user_id', [],
{ mint: collectionPubkey, gateType: iqlabs.contract.GateType.Collection },
undefined,
'holders'
);
```
***
#### `writeRow()`
| **Parameters** | `connection`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`tableSeed`: table name (Uint8Array or string)
`rowJson`: JSON row data (string)
`skipConfirmation`: skip tx confirmation (boolean, default: false)
`remainingAccounts`: optional array of PublicKey to append as read-only reference accounts (e.g. feed PDA for indexing) |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction signature (string) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Write the first row to create the table
await iqlabs.writer.writeRow(connection, signer, 'my-db', 'users', JSON.stringify({
id: 1, name: 'Alice', email: 'alice@example.com'
}));
// Add another row to the same table
await iqlabs.writer.writeRow(connection, signer, 'my-db', 'users', JSON.stringify({
id: 2, name: 'Bob', email: 'bob@example.com'
}));
```
***
#### `readTableRows()`
| **Parameters** | `account`: table PDA (PublicKey or string)
`options`: optional settings |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Options** | `limit`: max number of rows to fetch
`before`: signature cursor for pagination
`signatures`: pre-collected signature array (skips RPC fetch if provided)
`speed`: rate limit profile ('light', 'medium', 'heavy', 'extreme') |
| **Returns** | `Array>` |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Basic usage
const rows = await iqlabs.reader.readTableRows(tablePda, { limit: 50 });
// Cursor-based pagination
const olderRows = await iqlabs.reader.readTableRows(tablePda, { limit: 50, before: 'sig...' });
// With pre-collected signatures (skips signature fetching, decodes directly)
const sigs = await iqlabs.reader.collectSignatures(tablePda);
const targetIdx = sigs.indexOf('abc123');
const slice = sigs.slice(targetIdx - 25, targetIdx + 25);
const rows = await iqlabs.reader.readTableRows(tablePda, { signatures: slice });
```
***
#### `collectSignatures()`
Collects all (or up to `maxSignatures`) transaction signatures for an account. Lightweight — no transaction decoding, only signature strings. Useful for pagination: fetch the full signature list once, then slice and pass to `readTableRows()`.
| **Parameters** | `account`: table PDA (PublicKey or string)
`maxSignatures`: max number of signatures to collect (optional, fetches all if omitted) |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | `string[]` (signature strings) |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Collect all signatures
const allSigs = await iqlabs.reader.collectSignatures(tablePda);
// Collect up to 3000 signatures
const sigs = await iqlabs.reader.collectSignatures(tablePda, 3000);
// Use with readTableRows to read from the middle
const targetIdx = sigs.indexOf('abc123');
const chunk = sigs.slice(targetIdx - 25, targetIdx + 25);
const rows = await iqlabs.reader.readTableRows(tablePda, { signatures: chunk });
```
***
#### `getTablelistFromRoot()`
| **Parameters** | `connection`: Solana RPC Connection
`dbRootId`: database ID (Uint8Array or string) |
| -------------- | --------------------------------------------------------------------------------------------------- |
| **Returns** | `{ rootPda: PublicKey, creator: string \| null, tableSeeds: string[], globalTableSeeds: string[] }` |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const result = await iqlabs.reader.getTablelistFromRoot(connection, 'my-db');
console.log('Creator:', result.creator);
console.log('Table seeds:', result.tableSeeds);
```
***
#### `fetchInventoryTransactions()`
| **Parameters** | `publicKey`: user public key (PublicKey)
`limit`: max count (number)
`before`: pagination cursor (optional, string) |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Returns** | Transaction array |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const myFiles = await iqlabs.reader.fetchInventoryTransactions(myPubkey, 20);
myFiles.forEach(tx => {
let metadata: { data?: unknown } | null = null;
try {
metadata = JSON.parse(tx.metadata);
} catch {
metadata = null;
}
if (metadata && metadata.data !== undefined) {
const inlineData = typeof metadata.data === 'string'
? metadata.data
: JSON.stringify(metadata.data);
console.log(`Inline data: ${inlineData}`);
} else {
console.log(`Signature: ${tx.signature}`);
}
});
```
***
### Encryption
#### `deriveX25519Keypair()`
Derive a deterministic X25519 keypair from a wallet signature. The same wallet always produces the same keypair.
| **Parameters** | `signMessage`: wallet sign function `(msg: Uint8Array) => Promise` |
| -------------- | ------------------------------------------------------------------------------ |
| **Returns** | `{ privKey: Uint8Array, pubKey: Uint8Array }` |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const { privKey, pubKey } = await iqlabs.crypto.deriveX25519Keypair(
wallet.signMessage
);
```
***
#### `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/solana-sdk';
// Encrypt for a single recipient
const encrypted = await iqlabs.crypto.dhEncrypt(
recipientPubHex,
new TextEncoder().encode('secret message')
);
// Decrypt (recipient side)
const decrypted = await iqlabs.crypto.dhDecrypt(
recipientPrivKey,
encrypted.senderPub,
encrypted.iv,
encrypted.ciphertext
);
console.log(new TextDecoder().decode(decrypted)); // 'secret message'
```
***
#### `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/solana-sdk';
// Encrypt with password
const encrypted = await iqlabs.crypto.passwordEncrypt(
'my-password',
new TextEncoder().encode('secret data')
);
// Decrypt with same password
const decrypted = await iqlabs.crypto.passwordDecrypt(
'my-password',
encrypted.salt,
encrypted.iv,
encrypted.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/solana-sdk';
// Encrypt for multiple recipients
const encrypted = 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, encrypted
);
```
***
### Environment Settings
#### `setRpcUrl()`
| **Parameters** | `url`: Solana RPC URL (string) |
| -------------- | ------------------------------ |
| **Returns** | void |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
iqlabs.utils.setRpcUrl('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
#### `manageRowData()`
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`: Solana RPC Connection
`signer`: Signer
`dbRootId`: database ID (Uint8Array or string)
`seed`: table or connection seed (Uint8Array or string)
`rowJson`: JSON row data (string)
`tableName`: required for table edits (optional, string or Uint8Array)
`targetTx`: reference tx for table edits (optional, string or Uint8Array) |
| **Returns** | Transaction signature (string) |
| **Use Case** | Custom row management, updating existing rows |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
await iqlabs.writer.manageRowData(
connection, signer,
'my-db', // dbRootId
'users', // seed (table seed)
JSON.stringify({ id: 1, name: 'Updated Name' }),
'users', // tableName
originalTxSig // targetTx
);
```
***
### Reader Functions
#### `readUserState()`
Reads the UserState PDA for a given user.
| **Module** | `reader` |
| -------------- | ---------------------------------------------------------------------------------------------- |
| **Parameters** | `userPubkey`: user public key (string) |
| **Returns** | `{ owner: string, metadata: string \| null, totalSessionFiles: bigint, profileData?: string }` |
| **Use Case** | Fetching user profile data, checking upload counts |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const userState = await iqlabs.reader.readUserState(userPubkey);
console.log('Owner:', userState.owner);
console.log('Session files:', userState.totalSessionFiles);
console.log('Profile data:', userState.profileData);
```
***
#### `readInventoryMetadata()`
Reads metadata associated with a user's inventory transaction.
| **Module** | `reader` |
| -------------- | ---------------------------------------------------- |
| **Parameters** | `txSignature`: transaction signature (string) |
| **Returns** | `{ onChainPath: string, metadata: string }` |
| **Use Case** | Extracting file metadata from inventory transactions |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const result = await iqlabs.reader.readInventoryMetadata(txSignature);
console.log('Path:', result.onChainPath);
console.log('Metadata:', result.metadata);
```
***
#### `getSessionPdaList()`
Retrieves a list of session PDA addresses for a user.
| **Module** | `reader` |
| -------------- | ------------------------------------------- |
| **Parameters** | `userPubkey`: user public key (string) |
| **Returns** | `string[]` (session PDA base58 strings) |
| **Use Case** | Session management, active session tracking |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const sessions = await iqlabs.reader.getSessionPdaList(userPubkey);
sessions.forEach(pda => {
console.log(`Session PDA: ${pda}`);
});
```
***
### 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:
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
// Preset name
await iqlabs.writer.writeRow(connection, signer, dbRootId, tableSeed, rowJson, {
speed: 'heavy',
});
// Raw override — any subset works; missing dials inherit the default preset
await iqlabs.writer.writeRow(connection, signer, dbRootId, tableSeed, rowJson, {
speed: { maxRps: 80, maxConcurrencyUpload: 30 },
});
```
| Preset | `maxRps` | `maxConcurrency` | `maxConcurrencyUpload` |
| ----------------- | -------- | ---------------- | ---------------------- |
| `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.maxRps = 200`
will affect every future `speed: 'heavy'` call.
Exports: `SESSION_SPEED_PROFILES`, `DEFAULT_SESSION_SPEED`,
`resolveSessionSpeed`, `resolveSessionConfig`, types `SessionSpeedKey`,
`SessionSpeedConfig`, `SessionSpeedOption`.
***
#### `deriveDmSeed()`
Derives a deterministic seed for direct messaging (DM) between two users. Sorts the two pubkeys alphabetically and hashes with keccak256.
| **Module** | `utils` |
| -------------- | ----------------------------------------------------------------------------- |
| **Parameters** | `userA`: first user pubkey (string)
`userB`: second user pubkey (string) |
| **Returns** | `Uint8Array` (seed bytes) |
| **Use Case** | Creating consistent connection identifiers, DM channel setup |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const seed1 = iqlabs.utils.deriveDmSeed(walletA, walletB);
const seed2 = iqlabs.utils.deriveDmSeed(walletB, walletA);
// seed1 and seed2 are identical — order doesn't matter
```
***
#### `toSeedBytes()`
Converts a seed identifier to bytes. If the input is a 64-character hex string, it returns hex bytes directly. Otherwise, it applies keccak256 hash.
| **Module** | `utils` |
| -------------- | -------------------------------------------------- |
| **Parameters** | `value`: seed string or Uint8Array |
| **Returns** | `Uint8Array` |
| **Use Case** | Custom PDA derivation, low-level seed manipulation |
```typescript theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const seedString = 'my-custom-seed';
const seedBytes = iqlabs.utils.toSeedBytes(seedString);
const [pda, bump] = PublicKey.findProgramAddressSync(
[seedBytes, otherSeed],
programId
);
```
# How to Make a Fully On-Chain NFT
Source: https://iqlabs.mintlify.app/guides/fully-onchain-nft
Inscribe your artwork and metadata on Solana with Code In, and serve them to wallets through an IQ Gateway
Most "on-chain" NFT collections are not actually on-chain. The mint lives on Solana, but the metadata JSON and the artwork usually live on IPFS pins, Arweave, or a plain web2 file host. If that hosting disappears, the NFT becomes a pointer to nothing.
With Code In you can put **every byte** of your collection on Solana, the image and the metadata JSON alike, and serve them to wallets and marketplaces through an [IQ Gateway](https://github.com/IQCoreTeam/iq-gateway). No IPFS, no Arweave, no pinning service.
This guide walks through the full flow for a [Metaplex Candy Machine](https://developers.metaplex.com/core-candy-machine) collection.
## Architecture
```mermaid theme={null}
flowchart LR
subgraph inscribe ["1. Inscription (one-time, per NFT)"]
IMG["Image bytes"] -->|"codeIn()"| SIG1["tx signature A"]
SIG1 --> URL1["gateway/img/{sigA}.png"]
URL1 -->|"image field"| JSON["Metadata JSON"]
JSON -->|"codeIn()"| SIG2["tx signature B"]
SIG2 --> URL2["gateway/data/{sigB}"]
end
subgraph cm ["2. Candy Machine"]
URL2 -->|"config line uri"| CANDY["Core Candy Machine"]
end
subgraph read ["3. Read path"]
WALLET["Wallets / marketplaces / your UI"] -->|"HTTP GET uri"| GW["IQ Gateway"]
GW -->|"readCodeIn() + cache"| CHAIN[("Solana
inscribed txs")]
end
```
Per NFT:
1. Inscribe the image with `codeIn()` → you get a transaction signature. The image is now permanently on Solana, and its HTTP URL is `https:///img/{sig}.png`.
2. Build the Metaplex metadata JSON with that URL in the `image` field, then inscribe the JSON itself → a second signature. Its URL is `https:///data/{sig}` (raw bytes; `/meta/{sig}.json` serves a Metaplex-wrapped view).
3. Register the gateway URL as the item `uri` in your Candy Machine config lines.
The NFT's entire content chain (mint → uri → JSON → image) now resolves from Solana data.
## Why a gateway at all?
The gateway is an ordinary web2 HTTP server, so it is fair to ask why a fully on-chain setup needs one.
Wallets and marketplaces expect an NFT `uri` to be a plain HTTPS URL returning JSON, and an `image` URL returning bytes. They do not know how to reassemble Code In inscriptions from raw Solana transactions. So an NFT project runs a thin translation layer that pulls the inscription out of the chain and serves it over HTTP. That is exactly what the IQ Gateway is: a **read-only cache** that fetches on-chain data through the SDK and serves it with ETag/304 and multi-tier caching.
Why this is still a decentralized setup:
* **The gateway holds no data of its own.** Every byte is recoverable from Solana. The gateway is a cache, not a source of truth.
* **Anyone can run one.** The gateway is open source. If one instance dies, anyone can spin up another from the public repo and serve the exact same data.
* The only trust surface is the domain name in the `uri`. This is the same trade-off every IPFS-gateway-based collection already makes, with a strictly better recovery story.
### If your gateway goes down
Your NFTs do not break. The URLs in the minted metadata are just one door to data that lives on-chain. Every gateway addresses content the same way: `{gatewayBase}/img/{sig}.png`, `{gatewayBase}/data/{sig}`. The signature is the identity; the domain is interchangeable.
So to recover, take **any working gateway** (your own new instance, or someone else's), cut the path off the stored URL, and re-attach it to the new base:
```ts theme={null}
// stored uri on a dead gateway:
// https://gw.dead.xyz/data/5Xg7abc...
const sig = storedUri.split('/').pop(); // "5Xg7abc..."
const uri = `https://gw.alive.xyz/data/${sig}`; // same bytes, different door
```
Your frontend can even do this automatically: keep a small list of gateway bases and fall back to the next one when a fetch fails. The new gateway reconstructs identical content from the chain, because the chain is the source of truth.
## Step-by-step
### 1. Install the SDK
The SDK is published on npm. Install it, do not vendor or clone it:
```bash theme={null}
npm i @iqlabs-official/solana-sdk
```
Use the high-level API only: `iqlabs.writer.codeIn()` to inscribe and `iqlabs.reader.readCodeIn()` to verify. Do not reimplement chunking or session logic with the low-level instruction builders. `codeIn()` already picks the optimal storage path by size (\< 900 B inline, \< 8.5 KB linked-list, ≥ 8.5 KB parallel session upload) and handles the fee accounts.
### 2. Inscribe the image and the metadata
```ts theme={null}
import iqlabs from '@iqlabs-official/solana-sdk';
const GATEWAY = 'https://gw.yourdomain.xyz';
// 1. Inscribe the image (base64 string) → sigA
const sigA = await iqlabs.writer.codeIn(
{ connection, signer },
imageBase64,
'my-nft-001.png',
0,
'png'
);
// 2. Metadata JSON pointing at the gateway-served image → sigB
const json = JSON.stringify({
name: 'My NFT #1',
image: `${GATEWAY}/img/${sigA}.png`,
// attributes, description, ...
});
const sigB = await iqlabs.writer.codeIn(
{ connection, signer },
json,
'my-nft-001.json',
0,
'json'
);
// 3. Candy Machine config line uri:
const uri = `${GATEWAY}/data/${sigB}`;
```
Loop this over your collection. A batch script over a few hundred assets is about 50 lines.
### 3. Register the URIs in your Candy Machine
Insert each `uri` into the Candy Machine config lines as usual (`addConfigLines` with mpl-core-candy-machine, or your deploy tooling). Nothing about the Candy Machine setup changes. It just points at gateway URLs instead of IPFS/Arweave ones.
### 4. Run your own gateway
Clone the gateway and deploy your own instance under your domain, configured for your cluster:
```bash theme={null}
git clone https://github.com/IQCoreTeam/iq-gateway.git
cd iq-gateway && bun install
cp .env.example .env
# IQ_CHAIN=solana
# SOLANA_CLUSTER=mainnet-beta
# SOLANA_RPC_ENDPOINT=
bun run dev
```
Or with Docker:
```bash theme={null}
docker build -t iq-gateway .
docker run -d -p 3000:3000 -v iq-cache:/app/cache --env-file .env --restart unless-stopped iq-gateway
```
Once deployed, the gateway serves interactive API docs at `/docs` and the spec at `/openapi.json`. The routes you need for NFTs are `/img/{sig}.png`, `/data/{sig}`, and `/meta/{sig}.json`.
Test the full loop on devnet first (`SOLANA_CLUSTER=devnet`): inscribe a few assets, open the `/img` and `/data` URLs in a browser and in a wallet, then point a devnet Candy Machine at them before going to mainnet.
## Costs
Two components, both in SOL:
**Inscription cost (network fees).** Data is chunked at 850 bytes per transaction, each paying the 5,000-lamport base fee, roughly **0.006 SOL per MB** of payload. Binary data is base64-encoded (+33%), so budget about **0.008 SOL per MB** of raw image, plus priority fees when the network is busy.
**Protocol fee.** Each `codeIn()` finalize sends a small size-dependent fee to the protocol, handled on-chain, observed at roughly **0.001 to 0.005 SOL per upload** on mainnet.
Illustrative total for a 777-piece PFP collection at \~150 KB per image plus \~1 KB of JSON:
| Component | Rough cost |
| -------------------------------------------------------- | ------------ |
| 777 images (\~117 MB raw, base64 \~155 MB), network fees | \~1 SOL |
| 777 metadata JSONs, network fees | \~0.01 SOL |
| Protocol fee, 1,554 uploads | \~2 to 8 SOL |
A one-time cost for a collection that never needs a pinning service, never expires, and can be served by any gateway anyone ever runs.
# Getting Started
Source: https://iqlabs.mintlify.app/index
2000x cheaper inscription-based blockchain ledger for the complete Web3 experience
## Our Vision
**IQLabs** is building the **complete Web3** that everyone has been waiting for, powered by inscriptions that are **2000x cheaper** than traditional methods.
Join the **complete blockchain ledger internet** with real-time communication using just localhost and this SDK.
## Start Code In.
Build with the TypeScript/Node.js SDK.
Build with the Python SDK.
[Read our article about the vision →](https://x.com/IQLabsOfficial/status/2014131287871627554)